Skip to content

Commit e400c61

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

17 files changed

+291
-45
lines changed

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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 6 additions & 3 deletions
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",
@@ -337,5 +338,7 @@ private sealed trait YSettings:
337338
val YinstrumentDefs: Setting[Boolean] = BooleanSetting("-Yinstrument-defs", "Add instrumentation code that counts method calls; needs -Yinstrument to be set, too.")
338339

339340
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.")
341+
342+
val YrewriteImports: Setting[Boolean] = BooleanSetting("-Yrewrite-imports", "Rewrite unused imports. Requires -Wunused:imports.")
340343
end YSettings
341344

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

Lines changed: 112 additions & 10 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._
@@ -19,12 +20,14 @@ import Nullables._
1920
import Implicits.ContextualImplicits
2021
import config.Settings._
2122
import config.Config
23+
import config.SourceVersion.allSourceVersionNames
2224
import reporting._
2325
import io.{AbstractFile, NoAbstractFile, PlainFile, Path}
2426
import scala.io.Codec
2527
import collection.mutable
28+
import parsing.Parsers
2629
import printing._
27-
import config.{JavaPlatform, SJSPlatform, Platform, ScalaSettings}
30+
import config.{JavaPlatform, SJSPlatform, Platform, ScalaSettings, ScalaRelease}
2831
import classfile.ReusableDataReader
2932
import StdNames.nme
3033

@@ -39,7 +42,9 @@ import plugins._
3942
import java.util.concurrent.atomic.AtomicInteger
4043
import java.nio.file.InvalidPathException
4144

42-
object Contexts {
45+
import scala.util.chaining.given
46+
47+
object Contexts:
4348

4449
private val (compilerCallbackLoc, store1) = Store.empty.newLocation[CompilerCallback]()
4550
private val (sbtCallbackLoc, store2) = store1.newLocation[AnalysisCallback]()
@@ -51,8 +56,9 @@ object Contexts {
5156
private val (notNullInfosLoc, store8) = store7.newLocation[List[NotNullInfo]]()
5257
private val (importInfoLoc, store9) = store8.newLocation[ImportInfo | Null]()
5358
private val (typeAssignerLoc, store10) = store9.newLocation[TypeAssigner](TypeAssigner)
59+
private val (usagesLoc, store11) = store10.newLocation[Usages]()
5460

55-
private val initialStore = store10
61+
private val initialStore = store11
5662

5763
/** The current context */
5864
inline def ctx(using ctx: Context): Context = ctx
@@ -238,6 +244,9 @@ object Contexts {
238244
/** The current type assigner or typer */
239245
def typeAssigner: TypeAssigner = store(typeAssignerLoc)
240246

247+
/** Tracker for usages of elements such as import selectors. */
248+
def usages: Usages = store(usagesLoc)
249+
241250
/** The new implicit references that are introduced by this scope */
242251
protected var implicitsCache: ContextualImplicits | Null = null
243252
def implicits: ContextualImplicits = {
@@ -246,9 +255,7 @@ object Contexts {
246255
val implicitRefs: List[ImplicitRef] =
247256
if (isClassDefContext)
248257
try owner.thisType.implicitMembers
249-
catch {
250-
case ex: CyclicReference => Nil
251-
}
258+
catch case ex: CyclicReference => Nil
252259
else if (isImportContext) importInfo.nn.importedImplicits
253260
else if (isNonEmptyScopeContext) scope.implicitDecls
254261
else Nil
@@ -474,8 +481,24 @@ object Contexts {
474481
else fresh.setOwner(exprOwner)
475482

476483
/** A new context that summarizes an import statement */
477-
def importContext(imp: Import[?], sym: Symbol): FreshContext =
478-
fresh.setImportInfo(ImportInfo(sym, imp.selectors, imp.expr))
484+
def importContext(imp: Import[?], sym: Symbol, enteringSyms: Boolean = false): FreshContext =
485+
fresh.setImportInfo(ImportInfo(sym, imp.selectors, imp.expr).tap(ii => if enteringSyms && ctx.settings.WunusedHas.imports then usages += ii))
486+
487+
def scalaRelease: ScalaRelease =
488+
val releaseName = base.settings.scalaOutputVersion.value
489+
if releaseName.nonEmpty then ScalaRelease.parse(releaseName).get else ScalaRelease.latest
490+
491+
def tastyVersion: TastyVersion =
492+
import math.Ordered.orderingToOrdered
493+
val latestRelease = ScalaRelease.latest
494+
val specifiedRelease = scalaRelease
495+
if specifiedRelease < latestRelease then
496+
// This is needed to make -scala-output-version a no-op when set to the latest release for unstable versions of the compiler
497+
// (which might have the tasty format version numbers set to higher values before they're decreased during a release)
498+
TastyVersion.fromStableScalaRelease(specifiedRelease.majorVersion, specifiedRelease.minorVersion)
499+
else
500+
TastyVersion.compilerVersion
501+
>>>>>>> Warn unused imports
479502

480503
/** Is the debug option set? */
481504
def debug: Boolean = base.settings.Ydebug.value
@@ -811,6 +834,7 @@ object Contexts {
811834
store = initialStore
812835
.updated(settingsStateLoc, settingsGroup.defaultState)
813836
.updated(notNullInfosLoc, Nil)
837+
.updated(usagesLoc, Usages())
814838
.updated(compilationUnitLoc, NoCompilationUnit)
815839
searchHistory = new SearchRoot
816840
gadt = EmptyGadtConstraint
@@ -938,7 +962,7 @@ object Contexts {
938962
private[dotc] var stopInlining: Boolean = false
939963

940964
/** A variable that records that some error was reported in a globally committable context.
941-
* The error will not necessarlily be emitted, since it could still be that
965+
* The error will not necessarily be emitted, since it could still be that
942966
* the enclosing context will be aborted. The variable is used as a smoke test
943967
* to turn off assertions that might be wrong if the program is erroneous. To
944968
* just test for `ctx.reporter.errorsReported` is not always enough, since it
@@ -995,4 +1019,82 @@ object Contexts {
9951019
if (thread == null) thread = Thread.currentThread()
9961020
else assert(thread == Thread.currentThread(), "illegal multithreaded access to ContextBase")
9971021
}
998-
}
1022+
end ContextState
1023+
1024+
/** Collect information about the run for purposes of additional diagnostics.
1025+
*/
1026+
class Usages:
1027+
import rewrites.Rewrites.patch
1028+
private val selectors = mutable.Map.empty[ImportInfo, Set[untpd.ImportSelector]].withDefaultValue(Set.empty)
1029+
private val importInfos = mutable.Map.empty[CompilationUnit, List[(ImportInfo, Symbol)]].withDefaultValue(Nil)
1030+
1031+
// register an import
1032+
def +=(info: ImportInfo)(using Context): Unit =
1033+
def isLanguageImport = info.isLanguageImport && allSourceVersionNames.exists(info.forwardMapping.contains)
1034+
if ctx.settings.WunusedHas.imports && !isLanguageImport && !ctx.owner.is(Enum) && !ctx.compilationUnit.isJava then
1035+
importInfos(ctx.compilationUnit) ::= ((info, ctx.owner))
1036+
1037+
// mark a selector as used
1038+
def use(info: ImportInfo, selector: untpd.ImportSelector)(using Context): Unit =
1039+
if ctx.settings.WunusedHas.imports && !info.isRootImport then
1040+
selectors(info) += selector
1041+
1042+
// unused import, owner, which selector
1043+
def unused(using Context): List[(ImportInfo, Symbol, untpd.ImportSelector)] =
1044+
var unusages = List.empty[(ImportInfo, Symbol, untpd.ImportSelector)]
1045+
if ctx.settings.WunusedHas.imports && !ctx.compilationUnit.isJava then
1046+
//if ctx.settings.Ydebug.value then
1047+
// println(importInfos.get(ctx.compilationUnit).map(iss => iss.map((ii, s) => s"${ii.show} ($ii)")).getOrElse(Nil).mkString("Registered ImportInfos\n", "\n", ""))
1048+
// println(selectors.toList.flatMap((k,v) => v.toList.map(sel => s"${k.show} -> $sel")).mkString("Used selectors\n", "\n", ""))
1049+
def checkUsed(info: ImportInfo, owner: Symbol): Unit =
1050+
val used = selectors(info)
1051+
var needsPatch = false
1052+
def cull(toCheck: List[untpd.ImportSelector]): Unit =
1053+
toCheck match
1054+
case selector :: rest =>
1055+
cull(rest) // reverse
1056+
if !selector.isMask && !used(selector) then
1057+
unusages ::= ((info, owner, selector))
1058+
needsPatch = true
1059+
case _ =>
1060+
cull(info.selectors)
1061+
if needsPatch && ctx.settings.YrewriteImports.value then
1062+
val src = ctx.compilationUnit.source
1063+
val infoPos = info.qualifier.sourcePos
1064+
val lineSource = SourceFile.virtual(name = "import-line.scala", content = infoPos.lineContent)
1065+
val PackageDef(_, pieces) = Parsers.Parser(lineSource).parse(): @unchecked
1066+
// patch if there's just one import on the line, i.e., not import a.b, c.d
1067+
if pieces.length == 1 then
1068+
val retained = info.selectors.filter(sel => sel.isMask || used(sel))
1069+
val selectorSpan = info.selectors.map(_.span).reduce(_ union _)
1070+
val lineSpan = src.lineSpan(infoPos.start)
1071+
if retained.isEmpty then
1072+
patch(src, lineSpan, "") // line deletion
1073+
else if retained.size == 1 && info.selectors.size > 1 then
1074+
var starting = info.selectors.head.span.start
1075+
while starting > lineSpan.start && src.content()(starting) != '{' do starting -= 1
1076+
var ending = info.selectors.last.span.end
1077+
while ending <= lineSpan.end && src.content()(ending) != '}' do ending += 1
1078+
if ending < lineSpan.end then ending += 1 // past the close brace
1079+
val widened = selectorSpan.withStart(starting).withEnd(ending)
1080+
patch(src, widened, toText(retained)) // try to remove braces
1081+
else
1082+
patch(src, selectorSpan, toText(retained))
1083+
end checkUsed
1084+
importInfos.remove(ctx.compilationUnit).foreach(_.foreach(checkUsed))
1085+
unusages
1086+
end unused
1087+
1088+
// just the selectors, no need to add braces
1089+
private def toText(retained: List[untpd.ImportSelector])(using Context): String =
1090+
def selected(sel: untpd.ImportSelector) =
1091+
if sel.isGiven then "given"
1092+
else if sel.isWildcard then "*"
1093+
else if sel.name == sel.rename then sel.name.show
1094+
else s"${sel.name.show} as ${sel.rename.show}"
1095+
retained.map(selected).mkString(", ")
1096+
1097+
def clear()(using Context): Unit =
1098+
importInfos.clear()
1099+
selectors.clear()
1100+
end Usages

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

Lines changed: 1 addition & 1 deletion
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/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/typer/Implicits.scala

Lines changed: 17 additions & 4 deletions
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
@@ -1136,8 +1143,12 @@ trait Implicits:
11361143
SearchFailure(adapted.withType(new MismatchedImplicit(ref, pt, argument)))
11371144
}
11381145
else
1146+
cand match
1147+
case Candidate(k: ImportedImplicitRef, _, _) => ctx.usages.use(k.importInfo, k.importInfo.selectors(k.selector))
1148+
case _ =>
11391149
SearchSuccess(adapted, ref, cand.level, cand.isExtension)(ctx.typerState, ctx.gadt)
11401150
}
1151+
end typedImplicit
11411152

11421153
/** An implicit search; parameters as in `inferImplicit` */
11431154
class ImplicitSearch(protected val pt: Type, protected val argument: Tree, span: Span)(using Context):
@@ -1258,6 +1269,7 @@ trait Implicits:
12581269
else if diff > 0 then alt1
12591270
else SearchFailure(new AmbiguousImplicits(alt1, alt2, pt, argument), span)
12601271
case _: SearchFailure => alt2
1272+
end disambiguate
12611273

12621274
/** Try to find a best matching implicit term among all the candidates in `pending`.
12631275
* @param pending The list of candidates that remain to be tested
@@ -1327,6 +1339,7 @@ trait Implicits:
13271339
if (rfailures.isEmpty) found
13281340
else found.recoverWith(_ => rfailures.reverse.maxBy(_.tree.treeSize))
13291341
}
1342+
end rank
13301343

13311344
def negateIfNot(result: SearchResult) =
13321345
if (isNotGiven)

0 commit comments

Comments
 (0)