Skip to content

Commit 6dd9d1f

Browse files
committed
Warn unused imports
1 parent 4f3d767 commit 6dd9d1f

17 files changed

+231
-50
lines changed

compiler/src/dotty/tools/dotc/Run.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Scopes._
1010
import Names.Name
1111
import Denotations.Denotation
1212
import typer.{Typer, PrepareInlineable}
13-
import typer.ImportInfo._
13+
import typer.ImportInfo.withRootImports
1414
import Decorators._
1515
import io.{AbstractFile, PlainFile, VirtualFile}
1616
import Phases.unfusedPhases

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
137137
val rename: TermName = renamed match
138138
case Ident(rename: TermName) => rename
139139
case _ => name
140+
141+
def isMask: Boolean = !isWildcard && (rename == nme.WILDCARD)
140142
}
141143

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

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ package config
33

44
import dotty.tools.dotc.config.PathResolver.Defaults
55
import dotty.tools.dotc.config.Settings.{Setting, SettingGroup}
6-
import dotty.tools.dotc.core.Contexts._
6+
import dotty.tools.dotc.core.Contexts.*
77
import dotty.tools.dotc.rewrites.Rewrites
88
import dotty.tools.io.{AbstractFile, Directory, JDK9Reflectors, PlainDirectory}
99

10-
import scala.util.chaining._
10+
import scala.util.chaining.*
1111

1212
class ScalaSettings extends SettingGroup with AllScalaSettings
1313

@@ -146,12 +146,13 @@ private sealed trait WarningSettings:
146146
name = "-Wunused",
147147
helpArg = "warning",
148148
descr = "Enable or disable specific `unused` warnings",
149-
choices = List("nowarn", "all"),
149+
choices = List("nowarn", "all", "imports"),
150150
default = Nil
151151
)
152152
object WunusedHas:
153153
def allOr(s: String)(using Context) = Wunused.value.pipe(us => us.contains("all") || us.contains(s))
154154
def nowarn(using Context) = allOr("nowarn")
155+
def imports(using Context) = allOr("imports")
155156

156157
val Wconf: Setting[List[String]] = MultiStringSetting(
157158
"-Wconf",

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

Lines changed: 73 additions & 19 deletions
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._
@@ -24,6 +25,7 @@ import io.{AbstractFile, NoAbstractFile, PlainFile, Path}
2425
import scala.io.Codec
2526
import collection.mutable
2627
import printing._
28+
import config.Printers.{implicits, implicitsDetailed}
2729
import config.{JavaPlatform, SJSPlatform, Platform, ScalaSettings, ScalaRelease}
2830
import classfile.ReusableDataReader
2931
import StdNames.nme
@@ -42,20 +44,27 @@ import dotty.tools.tasty.TastyFormat
4244
import dotty.tools.dotc.config.{ NoScalaVersion, SpecificScalaVersion, AnyScalaVersion, ScalaBuild }
4345
import dotty.tools.dotc.core.tasty.TastyVersion
4446

45-
object Contexts {
47+
import scala.util.chaining.given
4648

47-
private val (compilerCallbackLoc, store1) = Store.empty.newLocation[CompilerCallback]()
48-
private val (sbtCallbackLoc, store2) = store1.newLocation[AnalysisCallback]()
49-
private val (printerFnLoc, store3) = store2.newLocation[Context => Printer](new RefinedPrinter(_))
50-
private val (settingsStateLoc, store4) = store3.newLocation[SettingsState]()
51-
private val (compilationUnitLoc, store5) = store4.newLocation[CompilationUnit]()
52-
private val (runLoc, store6) = store5.newLocation[Run]()
53-
private val (profilerLoc, store7) = store6.newLocation[Profiler]()
54-
private val (notNullInfosLoc, store8) = store7.newLocation[List[NotNullInfo]]()
55-
private val (importInfoLoc, store9) = store8.newLocation[ImportInfo]()
56-
private val (typeAssignerLoc, store10) = store9.newLocation[TypeAssigner](TypeAssigner)
49+
object Contexts:
5750

58-
private val initialStore = store10
51+
private var _initialStore = Store.empty
52+
private def newLocation[A]() = { val (loc, s) = _initialStore.newLocation[A]() ; _initialStore = s ; loc }
53+
private def newLocation[A](a: A) = { val (loc, s) = _initialStore.newLocation[A](a) ; _initialStore = s ; loc }
54+
55+
private val compilerCallbackLoc = newLocation[CompilerCallback]()
56+
private val sbtCallbackLoc = newLocation[AnalysisCallback]()
57+
private val printerFnLoc = newLocation[Context => Printer](new RefinedPrinter(_))
58+
private val settingsStateLoc = newLocation[SettingsState]()
59+
private val compilationUnitLoc = newLocation[CompilationUnit]()
60+
private val runLoc = newLocation[Run]()
61+
private val profilerLoc = newLocation[Profiler]()
62+
private val notNullInfosLoc = newLocation[List[NotNullInfo]]()
63+
private val importInfoLoc = newLocation[ImportInfo]()
64+
private val typeAssignerLoc = newLocation[TypeAssigner](TypeAssigner)
65+
private val usagesLoc = newLocation[Usages](Usages())
66+
67+
private val initialStore = _initialStore
5968

6069
/** The current context */
6170
inline def ctx(using ctx: Context): Context = ctx
@@ -241,6 +250,9 @@ object Contexts {
241250
/** The current type assigner or typer */
242251
def typeAssigner: TypeAssigner = store(typeAssignerLoc)
243252

253+
/** Tracker for usages of elements such as import selectors. */
254+
def usages: Usages = store(usagesLoc)
255+
244256
/** The new implicit references that are introduced by this scope */
245257
protected var implicitsCache: ContextualImplicits = null
246258
def implicits: ContextualImplicits = {
@@ -249,9 +261,7 @@ object Contexts {
249261
val implicitRefs: List[ImplicitRef] =
250262
if (isClassDefContext)
251263
try owner.thisType.implicitMembers
252-
catch {
253-
case ex: CyclicReference => Nil
254-
}
264+
catch case ex: CyclicReference => Nil
255265
else if (isImportContext) importInfo.importedImplicits
256266
else if (isNonEmptyScopeContext) scope.implicitDecls
257267
else Nil
@@ -482,8 +492,8 @@ object Contexts {
482492
else fresh.setOwner(exprOwner)
483493

484494
/** A new context that summarizes an import statement */
485-
def importContext(imp: Import[?], sym: Symbol): FreshContext =
486-
fresh.setImportInfo(ImportInfo(sym, imp.selectors, imp.expr))
495+
def importContext(imp: Import[?], sym: Symbol, enteringSyms: Boolean = false): FreshContext =
496+
fresh.setImportInfo(ImportInfo(sym, imp.selectors, imp.expr).tap(ii => if enteringSyms && ctx.settings.WunusedHas.imports then usages += ii))
487497

488498
def scalaRelease: ScalaRelease =
489499
val releaseName = base.settings.YscalaRelease.value
@@ -957,7 +967,7 @@ object Contexts {
957967
private[dotc] var stopInlining: Boolean = false
958968

959969
/** A variable that records that some error was reported in a globally committable context.
960-
* The error will not necessarlily be emitted, since it could still be that
970+
* The error will not necessarily be emitted, since it could still be that
961971
* the enclosing context will be aborted. The variable is used as a smoke test
962972
* to turn off assertions that might be wrong if the program is erroneous. To
963973
* just test for `ctx.reporter.errorsReported` is not always enough, since it
@@ -1014,4 +1024,48 @@ object Contexts {
10141024
if (thread == null) thread = Thread.currentThread()
10151025
else assert(thread == Thread.currentThread(), "illegal multithreaded access to ContextBase")
10161026
}
1017-
}
1027+
end ContextState
1028+
1029+
/** Collect information about the run for purposes of additional diagnostics.
1030+
*/
1031+
class Usages:
1032+
private val selectors = mutable.Map.empty[ImportInfo, Set[untpd.ImportSelector]].withDefaultValue(Set.empty)
1033+
private val importInfos = mutable.Map.empty[CompilationUnit, List[(ImportInfo, Symbol)]].withDefaultValue(Nil)
1034+
1035+
// register an import
1036+
def +=(info: ImportInfo)(using Context): Unit =
1037+
if ctx.settings.WunusedHas.imports && !ctx.owner.is(Enum) then
1038+
importInfos(ctx.compilationUnit) ::= ((info, ctx.owner))
1039+
1040+
// mark a selector as used
1041+
def use(info: ImportInfo, selector: untpd.ImportSelector)(using Context): Unit =
1042+
if ctx.settings.WunusedHas.imports && !info.isRootImport then
1043+
selectors(info) += selector
1044+
1045+
// unused import, owner, which selector
1046+
def unused(using Context): List[(ImportInfo, Symbol, untpd.ImportSelector)] =
1047+
var unusages = List.empty[(ImportInfo, Symbol, untpd.ImportSelector)]
1048+
if ctx.settings.WunusedHas.imports then
1049+
if ctx.settings.Ydebug.value then
1050+
println(importInfos.get(ctx.compilationUnit).map(iss => iss.map((ii, s) => s"${ii.show} ($ii)")).getOrElse(Nil).mkString("Registered ImportInfos\n", "\n", ""))
1051+
println(selectors.toList.flatMap((k,v) => v.toList.map(sel => s"${k.show} -> $sel")).mkString("Used selectors\n", "\n", ""))
1052+
end if
1053+
def checkUsed(info: ImportInfo, owner: Symbol): Unit =
1054+
val used = selectors(info)
1055+
def cull(toCheck: List[untpd.ImportSelector]): Unit =
1056+
toCheck match
1057+
case selector :: rest =>
1058+
cull(rest) // reverse
1059+
if !selector.isMask && !used(selector) then
1060+
unusages ::= ((info, owner, selector))
1061+
case _ =>
1062+
cull(info.selectors)
1063+
end checkUsed
1064+
importInfos.remove(ctx.compilationUnit).foreach(_.foreach(checkUsed))
1065+
unusages
1066+
end unused
1067+
1068+
def clear()(using Context): Unit =
1069+
importInfos.clear()
1070+
selectors.clear()
1071+
end Usages

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ class InteractiveDriver(val settings: List[String]) extends Driver {
146146
def run(uri: URI, sourceCode: String): List[Diagnostic] = run(uri, toSource(uri, sourceCode))
147147

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

151151
val previousCtx = myCtx
152152
try {

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

Lines changed: 9 additions & 3 deletions
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
@@ -611,12 +612,17 @@ class PlainPrinter(_ctx: Context) extends Printer {
611612
}
612613

613614
def toText(importInfo: ImportInfo): Text =
615+
def selected(sel: untpd.ImportSelector) =
616+
if sel.isGiven then "given"
617+
else if sel.isWildcard then "*"
618+
else if sel.name == sel.rename then sel.name.show
619+
else s"${sel.name.show} as ${sel.rename.show}"
614620
val siteStr = importInfo.site.show
615621
val exprStr = if siteStr.endsWith(".type") then siteStr.dropRight(5) else siteStr
616622
val selectorStr = importInfo.selectors match
617-
case sel :: Nil if sel.renamed.isEmpty && sel.bound.isEmpty =>
618-
if sel.isGiven then "given" else sel.name.show
619-
case _ => "{...}"
623+
case sel :: Nil if sel.renamed.isEmpty && sel.bound.isEmpty => selected(sel)
624+
case sels => sels.map(selected).mkString("{", ", ", "}")
625+
//case _ => "{...}"
620626
s"import $exprStr.$selectorStr"
621627

622628
def toText(c: OrderingConstraint): Text =

compiler/src/dotty/tools/dotc/reporting/WConf.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ object WConf:
7070
case ErrorId(num) =>
7171
ErrorMessageID.fromErrorNumber(num.toInt) match
7272
case Some(errId) => Right(MessageID(errId))
73-
case _ => Left(s"unknonw error message number: E$num")
73+
case _ => Left(s"unknown error message number: E$num")
7474
case _ =>
7575
Left(s"invalid error message id: $conf")
7676
case "name" =>

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,17 @@ import scala.annotation.threadUnsafe
4545
object Implicits:
4646
import tpd._
4747

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

5560
/** Both search candidates and successes are references with a specific nesting level. */
5661
sealed trait RefAndLevel {
@@ -265,7 +270,9 @@ object Implicits:
265270
refs.foreach(tryCandidate(extensionOnly = false))
266271
candidates.toList
267272
}
273+
end filterMatching
268274
}
275+
end ImplicitRefs
269276

270277
/** The implicit references coming from the implicit scope of a type.
271278
* @param tp the type determining the implicit scope
@@ -1123,8 +1130,12 @@ trait Implicits:
11231130
SearchFailure(adapted.withType(new MismatchedImplicit(ref, pt, argument)))
11241131
}
11251132
else
1133+
cand match
1134+
case Candidate(k: ImportedImplicitRef, _, _) => ctx.usages.use(k.importInfo, k.importInfo.selectors(k.selector))
1135+
case _ =>
11261136
SearchSuccess(adapted, ref, cand.level, cand.isExtension)(ctx.typerState, ctx.gadt)
11271137
}
1138+
end typedImplicit
11281139

11291140
/** An implicit search; parameters as in `inferImplicit` */
11301141
class ImplicitSearch(protected val pt: Type, protected val argument: Tree, span: Span)(using Context):
@@ -1243,6 +1254,7 @@ trait Implicits:
12431254
else if diff > 0 then alt1
12441255
else SearchFailure(new AmbiguousImplicits(alt1, alt2, pt, argument), span)
12451256
case _: SearchFailure => alt2
1257+
end disambiguate
12461258

12471259
/** Try to find a best matching implicit term among all the candidates in `pending`.
12481260
* @param pending The list of candidates that remain to be tested
@@ -1312,6 +1324,7 @@ trait Implicits:
13121324
if (rfailures.isEmpty) found
13131325
else found.recoverWith(_ => rfailures.reverse.maxBy(_.tree.treeSize))
13141326
}
1327+
end rank
13151328

13161329
def negateIfNot(result: SearchResult) =
13171330
if (isNotGiven)

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

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ import core._
88
import printing.{Printer, Showable}
99
import util.SimpleIdentityMap
1010
import Symbols._, Names._, Types._, Contexts._, StdNames._, Flags._
11-
import Implicits.RenamedImplicitRef
11+
import Implicits.{ImportedImplicitRef, RenamedImplicitRef}
1212
import config.SourceVersion
1313
import StdNames.nme
1414
import printing.Texts.Text
1515
import NameKinds.QualifiedName
1616
import Decorators._
1717

18+
import annotation.*
19+
import compiletime.uninitialized
20+
1821
object ImportInfo {
1922

2023
case class RootRef(refFn: () => TermRef, isPredef: Boolean = false)
@@ -69,7 +72,7 @@ class ImportInfo(symf: Context ?=> Symbol,
6972
}
7073
mySym
7174
}
72-
private var mySym: Symbol = _
75+
private var mySym: Symbol = uninitialized
7376

7477
/** The (TermRef) type of the qualifier of the import clause */
7578
def site(using Context): Type = importSym.info match {
@@ -128,6 +131,11 @@ class ImportInfo(symf: Context ?=> Symbol,
128131
myWildcardBound = ctx.typer.importBound(selectors, isGiven = false)
129132
myWildcardBound
130133

134+
private def givenSelector = selectors.indexWhere(_.isGiven)
135+
private def wildSelector = selectors.indexWhere(_.isWildcard)
136+
private def selectorOf(name: Name) = selectors.indexWhere(_.name == name)
137+
private def selectorOf(original: Name, rename: Name) = selectors.indexWhere(s => s.name == original && s.rename == rename)
138+
131139
/** The implicit references imported by this import clause */
132140
def importedImplicits(using Context): List[ImplicitRef] =
133141
val pre = site
@@ -143,10 +151,11 @@ class ImportInfo(symf: Context ?=> Symbol,
143151
|| ctx.mode.is(Mode.FindHiddenImplicits) // consider both implicits and givens for error reporting
144152
|| ref.symbol.is(Implicit) // a wildcard `_` import only pulls in implicits
145153
val bound = if isGivenImport then givenBound else wildcardBound
146-
if isEligible && ref.denot.asSingleDenotation.matchesImportBound(bound) then ref :: Nil
154+
val selector = if isGivenImport then givenSelector else wildSelector
155+
if isEligible && ref.denot.asSingleDenotation.matchesImportBound(bound) then ImportedImplicitRef(ref, this, selector) :: Nil
147156
else Nil
148-
else if renamed == ref.name then ref :: Nil
149-
else RenamedImplicitRef(ref, renamed) :: Nil
157+
else if renamed == ref.name then ImportedImplicitRef(ref, this, selectorOf(name)) :: Nil
158+
else RenamedImplicitRef(ref, this, selectorOf(name, renamed), renamed) :: Nil
150159
}
151160
else
152161
for
@@ -155,8 +164,8 @@ class ImportInfo(symf: Context ?=> Symbol,
155164
yield
156165
val original = reverseMapping(renamed)
157166
val ref = TermRef(pre, original, denot)
158-
if renamed == original then ref
159-
else RenamedImplicitRef(ref, renamed)
167+
if renamed == original then ImportedImplicitRef(ref, this, selectorOf(original))
168+
else RenamedImplicitRef(ref, this, selectorOf(original, renamed), renamed)
160169

161170
/** The root import symbol hidden by this symbol, or NoSymbol if no such symbol is hidden.
162171
* Note: this computation needs to work even for un-initialized import infos, and
@@ -181,6 +190,7 @@ class ImportInfo(symf: Context ?=> Symbol,
181190
assert(myUnimported != null)
182191
myUnimported
183192

193+
@unused
184194
private val isLanguageImport: Boolean = untpd.languageImport(qualifier).isDefined
185195

186196
private var myUnimported: Symbol = _
@@ -227,4 +237,9 @@ class ImportInfo(symf: Context ?=> Symbol,
227237
featureCache(feature)
228238

229239
def toText(printer: Printer): Text = printer.toText(this)
240+
241+
override def hashCode = qualifier.##
242+
override def equals(other: Any) = other match
243+
case that: ImportInfo => qualifier == that.qualifier
244+
case _ => false
230245
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ class Namer { typer: Typer =>
389389
setDocstring(pkg, stat)
390390
ctx
391391
case imp: Import =>
392-
ctx.importContext(imp, createSymbol(imp))
392+
ctx.importContext(imp, createSymbol(imp), enteringSyms = true)
393393
case mdef: DefTree =>
394394
val sym = createSymbol(mdef)
395395
enterSymbol(sym)

0 commit comments

Comments
 (0)