Skip to content

Commit 481d1f9

Browse files
committed
Warn unused imports
Handle language feature imports. Ignore language version imports. Skip Java sources. Support rewrite.
1 parent b1b1dfd commit 481d1f9

24 files changed

+710
-56
lines changed

compiler/src/dotty/tools/dotc/ast/untpd.scala

+2
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
135135
val rename: TermName = renamed match
136136
case Ident(rename: TermName) => rename
137137
case _ => name
138+
139+
def isMask: Boolean = !isWildcard && rename == nme.WILDCARD
138140
}
139141

140142
case class Number(digits: String, kind: NumberKind)(implicit @constructorOnly src: SourceFile) extends TermTree

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

+6-3
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import scala.language.unsafeNulls
66
import dotty.tools.dotc.config.PathResolver.Defaults
77
import dotty.tools.dotc.config.Settings.{Setting, SettingGroup}
88
import dotty.tools.dotc.config.SourceVersion
9-
import dotty.tools.dotc.core.Contexts._
9+
import dotty.tools.dotc.core.Contexts.*
1010
import dotty.tools.dotc.rewrites.Rewrites
1111
import dotty.tools.io.{AbstractFile, Directory, JDK9Reflectors, PlainDirectory}
1212

13-
import scala.util.chaining._
13+
import scala.util.chaining.*
1414

1515
class ScalaSettings extends SettingGroup with AllScalaSettings
1616

@@ -161,12 +161,13 @@ private sealed trait WarningSettings:
161161
name = "-Wunused",
162162
helpArg = "warning",
163163
descr = "Enable or disable specific `unused` warnings",
164-
choices = List("nowarn", "all"),
164+
choices = List("nowarn", "all", "imports"),
165165
default = Nil
166166
)
167167
object WunusedHas:
168168
def allOr(s: String)(using Context) = Wunused.value.pipe(us => us.contains("all") || us.contains(s))
169169
def nowarn(using Context) = allOr("nowarn")
170+
def imports(using Context) = allOr("imports")
170171

171172
val Wconf: Setting[List[String]] = MultiStringSetting(
172173
"-Wconf",
@@ -341,5 +342,7 @@ private sealed trait YSettings:
341342
val YinstrumentDefs: Setting[Boolean] = BooleanSetting("-Yinstrument-defs", "Add instrumentation code that counts method calls; needs -Yinstrument to be set, too.")
342343

343344
val YforceInlineWhileTyping: Setting[Boolean] = BooleanSetting("-Yforce-inline-while-typing", "Make non-transparent inline methods inline when typing. Emulates the old inlining behavior of 3.0.0-M3.")
345+
346+
val YrewriteImports: Setting[Boolean] = BooleanSetting("-Yrewrite-imports", "Rewrite unused imports. Requires -Wunused:imports.")
344347
end YSettings
345348

compiler/src/dotty/tools/dotc/core/Contexts.scala

+64-10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import interfaces.CompilerCallback
66
import Decorators._
77
import Periods._
88
import Names._
9+
import Flags.*
910
import Phases._
1011
import Types._
1112
import Symbols._
@@ -20,14 +21,17 @@ import Nullables._
2021
import Implicits.ContextualImplicits
2122
import config.Settings._
2223
import config.Config
24+
import config.SourceVersion.allSourceVersionNames
2325
import reporting._
2426
import io.{AbstractFile, NoAbstractFile, PlainFile, Path}
2527
import scala.io.Codec
2628
import collection.mutable
2729
import printing._
28-
import config.{JavaPlatform, SJSPlatform, Platform, ScalaSettings}
30+
import config.{JavaPlatform, SJSPlatform, Platform, ScalaSettings, ScalaRelease}
2931
import classfile.ReusableDataReader
3032
import StdNames.nme
33+
import parsing.Parsers.EnclosingSpan
34+
import util.Spans.NoSpan
3135

3236
import scala.annotation.internal.sharable
3337

@@ -40,7 +44,9 @@ import plugins._
4044
import java.util.concurrent.atomic.AtomicInteger
4145
import java.nio.file.InvalidPathException
4246

43-
object Contexts {
47+
import scala.util.chaining.given
48+
49+
object Contexts:
4450

4551
private val (compilerCallbackLoc, store1) = Store.empty.newLocation[CompilerCallback]()
4652
private val (sbtCallbackLoc, store2) = store1.newLocation[AnalysisCallback]()
@@ -52,8 +58,9 @@ object Contexts {
5258
private val (notNullInfosLoc, store8) = store7.newLocation[List[NotNullInfo]]()
5359
private val (importInfoLoc, store9) = store8.newLocation[ImportInfo | Null]()
5460
private val (typeAssignerLoc, store10) = store9.newLocation[TypeAssigner](TypeAssigner)
61+
private val (usagesLoc, store11) = store10.newLocation[Usages]()
5562

56-
private val initialStore = store10
63+
private val initialStore = store11
5764

5865
/** The current context */
5966
inline def ctx(using ctx: Context): Context = ctx
@@ -239,6 +246,9 @@ object Contexts {
239246
/** The current type assigner or typer */
240247
def typeAssigner: TypeAssigner = store(typeAssignerLoc)
241248

249+
/** Tracker for usages of elements such as import selectors. */
250+
def usages: Usages = store(usagesLoc)
251+
242252
/** The new implicit references that are introduced by this scope */
243253
protected var implicitsCache: ContextualImplicits | Null = null
244254
def implicits: ContextualImplicits = {
@@ -247,9 +257,7 @@ object Contexts {
247257
val implicitRefs: List[ImplicitRef] =
248258
if (isClassDefContext)
249259
try owner.thisType.implicitMembers
250-
catch {
251-
case ex: CyclicReference => Nil
252-
}
260+
catch case ex: CyclicReference => Nil
253261
else if (isImportContext) importInfo.nn.importedImplicits
254262
else if (isNonEmptyScopeContext) scope.implicitDecls
255263
else Nil
@@ -475,8 +483,8 @@ object Contexts {
475483
else fresh.setOwner(exprOwner)
476484

477485
/** A new context that summarizes an import statement */
478-
def importContext(imp: Import[?], sym: Symbol): FreshContext =
479-
fresh.setImportInfo(ImportInfo(sym, imp.selectors, imp.expr))
486+
def importContext(imp: Import[?], sym: Symbol, enteringSyms: Boolean = false): FreshContext =
487+
fresh.setImportInfo(ImportInfo(sym, imp.selectors, imp.expr, imp.attachmentOrElse(EnclosingSpan, NoSpan)).tap(importInfo => if enteringSyms && ctx.settings.WunusedHas.imports then usages += importInfo))
480488

481489
/** Is the debug option set? */
482490
def debug: Boolean = base.settings.Ydebug.value
@@ -812,6 +820,7 @@ object Contexts {
812820
store = initialStore
813821
.updated(settingsStateLoc, settingsGroup.defaultState)
814822
.updated(notNullInfosLoc, Nil)
823+
.updated(usagesLoc, Usages())
815824
.updated(compilationUnitLoc, NoCompilationUnit)
816825
searchHistory = new SearchRoot
817826
gadt = EmptyGadtConstraint
@@ -939,7 +948,7 @@ object Contexts {
939948
private[dotc] var stopInlining: Boolean = false
940949

941950
/** A variable that records that some error was reported in a globally committable context.
942-
* The error will not necessarlily be emitted, since it could still be that
951+
* The error will not necessarily be emitted, since it could still be that
943952
* the enclosing context will be aborted. The variable is used as a smoke test
944953
* to turn off assertions that might be wrong if the program is erroneous. To
945954
* just test for `ctx.reporter.errorsReported` is not always enough, since it
@@ -996,4 +1005,49 @@ object Contexts {
9961005
if (thread == null) thread = Thread.currentThread()
9971006
else assert(thread == Thread.currentThread(), "illegal multithreaded access to ContextBase")
9981007
}
999-
}
1008+
end ContextState
1009+
1010+
/** Collect information about the run for purposes of additional diagnostics.
1011+
*/
1012+
class Usages:
1013+
private val selectors = mutable.Map.empty[ImportInfo, Set[untpd.ImportSelector]].withDefaultValue(Set.empty)
1014+
private val importInfos = mutable.Map.empty[CompilationUnit, List[(ImportInfo, Symbol)]].withDefaultValue(Nil)
1015+
1016+
// register an import
1017+
def +=(info: ImportInfo)(using Context): Unit =
1018+
def isLanguageImport = info.isLanguageImport && allSourceVersionNames.exists(info.forwardMapping.contains)
1019+
if ctx.settings.WunusedHas.imports && !isLanguageImport && !ctx.owner.is(Enum) && !ctx.compilationUnit.isJava then
1020+
importInfos(ctx.compilationUnit) ::= ((info, ctx.owner))
1021+
1022+
// mark a selector as used
1023+
def use(info: ImportInfo, selector: untpd.ImportSelector)(using Context): Unit =
1024+
if ctx.settings.WunusedHas.imports && !info.isRootImport then
1025+
selectors(info) += selector
1026+
1027+
// unused import, owner, which selector
1028+
def unused(using Context): List[(ImportInfo, Symbol, List[untpd.ImportSelector])] =
1029+
if ctx.settings.WunusedHas.imports && !ctx.compilationUnit.isJava then
1030+
var unusages = List.empty[(ImportInfo, Symbol, List[untpd.ImportSelector])]
1031+
def checkUsed(info: ImportInfo, owner: Symbol): Unit =
1032+
val usedSelectors = selectors.remove(info).getOrElse(Set.empty)
1033+
var unusedSelectors = List.empty[untpd.ImportSelector]
1034+
def cull(toCheck: List[untpd.ImportSelector]): Unit =
1035+
toCheck match
1036+
case selector :: rest =>
1037+
cull(rest) // reverse
1038+
if !selector.isMask && !usedSelectors(selector) then
1039+
unusedSelectors ::= selector
1040+
case _ =>
1041+
cull(info.selectors)
1042+
if unusedSelectors.nonEmpty then unusages ::= (info, owner, unusedSelectors)
1043+
end checkUsed
1044+
importInfos.remove(ctx.compilationUnit).foreach(_.foreach(checkUsed))
1045+
unusages
1046+
else
1047+
Nil
1048+
end unused
1049+
1050+
def clear()(using Context): Unit =
1051+
importInfos.clear()
1052+
selectors.clear()
1053+
end Usages

compiler/src/dotty/tools/dotc/interactive/InteractiveDriver.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ class InteractiveDriver(val settings: List[String]) extends Driver {
148148
def run(uri: URI, sourceCode: String): List[Diagnostic] = run(uri, toSource(uri, sourceCode))
149149

150150
def run(uri: URI, source: SourceFile): List[Diagnostic] = {
151-
import typer.ImportInfo._
151+
import typer.ImportInfo.withRootImports
152152

153153
val previousCtx = myCtx
154154
try {

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

+8-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import scala.language.unsafeNulls
77
import scala.annotation.internal.sharable
88
import scala.collection.mutable.ListBuffer
99
import scala.collection.immutable.BitSet
10-
import util.{ SourceFile, SourcePosition, NoSourcePosition }
10+
import util.{Property, SourceFile, SourcePosition, NoSourcePosition}
1111
import Tokens._
1212
import Scanners._
1313
import xml.MarkupParsers.MarkupParser
@@ -64,6 +64,8 @@ object Parsers {
6464
val QuotedPattern = 1 << 2
6565
}
6666

67+
val EnclosingSpan: Property.Key[Span] = Property.Key()
68+
6769
extension (buf: ListBuffer[Tree])
6870
def +++=(x: Tree) = x match {
6971
case x: Thicket => buf ++= x.trees
@@ -3269,18 +3271,20 @@ object Parsers {
32693271
/** Import ::= `import' ImportExpr {‘,’ ImportExpr}
32703272
* Export ::= `export' ImportExpr {‘,’ ImportExpr}
32713273
*/
3272-
def importOrExportClause(leading: Token, mkTree: ImportConstr): List[Tree] = {
3274+
def importOrExportClause(leading: Token, mkTree: ImportConstr): List[Tree] =
32733275
val offset = accept(leading)
32743276
commaSeparated(importExpr(mkTree)) match {
32753277
case t :: rest =>
32763278
// The first import should start at the start offset of the keyword.
32773279
val firstPos =
32783280
if (t.span.exists) t.span.withStart(offset)
32793281
else Span(offset, in.lastOffset)
3280-
t.withSpan(firstPos) :: rest
3282+
val imports = t.withSpan(firstPos) :: rest
3283+
val enclosing = imports.head.span union imports.last.span
3284+
imports.foreach(_.putAttachment(EnclosingSpan, enclosing))
3285+
imports
32813286
case nil => nil
32823287
}
3283-
}
32843288

32853289
def exportClause() =
32863290
importOrExportClause(EXPORT, Export(_,_))

compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala

+10-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Contexts._
88
import Scopes.Scope, Denotations.Denotation, Annotations.Annotation
99
import StdNames.nme
1010
import ast.Trees._
11+
import ast.untpd
1112
import typer.Implicits._
1213
import typer.ImportInfo
1314
import Variances.varianceSign
@@ -644,12 +645,17 @@ class PlainPrinter(_ctx: Context) extends Printer {
644645
}
645646

646647
def toText(importInfo: ImportInfo): Text =
648+
def selected(sel: untpd.ImportSelector) =
649+
if sel.isGiven then "given"
650+
else if sel.isWildcard then "*"
651+
else if sel.name == sel.rename then sel.name.show
652+
else s"${sel.name.show} as ${sel.rename.show}"
647653
val siteStr = importInfo.site.show
648-
val exprStr = if siteStr.endsWith(".type") then siteStr.dropRight(5) else siteStr
654+
val exprStr = siteStr.stripSuffix(".type")
649655
val selectorStr = importInfo.selectors match
650-
case sel :: Nil if sel.renamed.isEmpty && sel.bound.isEmpty =>
651-
if sel.isGiven then "given" else sel.name.show
652-
case _ => "{...}"
656+
case sel :: Nil if sel.renamed.isEmpty && sel.bound.isEmpty => selected(sel)
657+
case sels => sels.map(selected).mkString("{", ", ", "}")
658+
//case _ => "{...}"
653659
s"import $exprStr.$selectorStr"
654660

655661
def toText(c: OrderingConstraint): Text =

compiler/src/dotty/tools/dotc/typer/Implicits.scala

+17-4
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,17 @@ import scala.annotation.threadUnsafe
4040
object Implicits:
4141
import tpd._
4242

43-
/** An implicit definition `implicitRef` that is visible under a different name, `alias`.
43+
/** Pairs an imported `ImplicitRef` with its `ImportInfo` for diagnostic bookkeeping.
44+
*/
45+
class ImportedImplicitRef(val underlyingRef: TermRef, val importInfo: ImportInfo, val selector: Int) extends ImplicitRef:
46+
def implicitName(using Context): TermName = underlyingRef.implicitName
47+
48+
/** An implicit definition `ImplicitRef` that is visible under a different name, `alias`.
4449
* Gets generated if an implicit ref is imported via a renaming import.
4550
*/
46-
class RenamedImplicitRef(val underlyingRef: TermRef, val alias: TermName) extends ImplicitRef {
47-
def implicitName(using Context): TermName = alias
48-
}
51+
class RenamedImplicitRef(underlyingRef: TermRef, importInfo: ImportInfo, selector: Int, val alias: TermName)
52+
extends ImportedImplicitRef(underlyingRef, importInfo, selector):
53+
override def implicitName(using Context): TermName = alias
4954

5055
/** Both search candidates and successes are references with a specific nesting level. */
5156
sealed trait RefAndLevel {
@@ -260,7 +265,9 @@ object Implicits:
260265
refs.foreach(tryCandidate(extensionOnly = false))
261266
candidates.toList
262267
}
268+
end filterMatching
263269
}
270+
end ImplicitRefs
264271

265272
/** The implicit references coming from the implicit scope of a type.
266273
* @param tp the type determining the implicit scope
@@ -1147,8 +1154,12 @@ trait Implicits:
11471154
SearchFailure(adapted.withType(new MismatchedImplicit(ref, pt, argument)))
11481155
}
11491156
else
1157+
cand match
1158+
case Candidate(k: ImportedImplicitRef, _, _) => ctx.usages.use(k.importInfo, k.importInfo.selectors(k.selector))
1159+
case _ =>
11501160
SearchSuccess(adapted, ref, cand.level, cand.isExtension)(ctx.typerState, ctx.gadt)
11511161
}
1162+
end typedImplicit
11521163

11531164
/** An implicit search; parameters as in `inferImplicit` */
11541165
class ImplicitSearch(protected val pt: Type, protected val argument: Tree, span: Span)(using Context):
@@ -1269,6 +1280,7 @@ trait Implicits:
12691280
else if diff > 0 then alt1
12701281
else SearchFailure(new AmbiguousImplicits(alt1, alt2, pt, argument), span)
12711282
case _: SearchFailure => alt2
1283+
end disambiguate
12721284

12731285
/** Try to find a best matching implicit term among all the candidates in `pending`.
12741286
* @param pending The list of candidates that remain to be tested
@@ -1338,6 +1350,7 @@ trait Implicits:
13381350
if (rfailures.isEmpty) found
13391351
else found.recoverWith(_ => rfailures.reverse.maxBy(_.tree.treeSize))
13401352
}
1353+
end rank
13411354

13421355
def negateIfNot(result: SearchResult) =
13431356
if (isNotGiven)

0 commit comments

Comments
 (0)