Skip to content

Commit 9fb4a7d

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

18 files changed

+278
-46
lines changed

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

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

141143
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
@@ -5,11 +5,11 @@ import scala.language.unsafeNulls
55

66
import dotty.tools.dotc.config.PathResolver.Defaults
77
import dotty.tools.dotc.config.Settings.{Setting, SettingGroup}
8-
import dotty.tools.dotc.core.Contexts._
8+
import dotty.tools.dotc.core.Contexts.*
99
import dotty.tools.dotc.rewrites.Rewrites
1010
import dotty.tools.io.{AbstractFile, Directory, JDK9Reflectors, PlainDirectory}
1111

12-
import scala.util.chaining._
12+
import scala.util.chaining.*
1313

1414
class ScalaSettings extends SettingGroup with AllScalaSettings
1515

@@ -154,12 +154,13 @@ private sealed trait WarningSettings:
154154
name = "-Wunused",
155155
helpArg = "warning",
156156
descr = "Enable or disable specific `unused` warnings",
157-
choices = List("nowarn", "all"),
157+
choices = List("nowarn", "all", "imports"),
158158
default = Nil
159159
)
160160
object WunusedHas:
161161
def allOr(s: String)(using Context) = Wunused.value.pipe(us => us.contains("all") || us.contains(s))
162162
def nowarn(using Context) = allOr("nowarn")
163+
def imports(using Context) = allOr("imports")
163164

164165
val Wconf: Setting[List[String]] = MultiStringSetting(
165166
"-Wconf",
@@ -330,4 +331,6 @@ private sealed trait YSettings:
330331
val YinstrumentDefs: Setting[Boolean] = BooleanSetting("-Yinstrument-defs", "Add instrumentation code that counts method calls; needs -Yinstrument to be set, too.")
331332

332333
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.")
334+
335+
val YrewriteImports: Setting[Boolean] = BooleanSetting("-Yrewrite-imports", "Rewrite unused imports. Requires -Wunused:imports.")
333336
end YSettings

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

+96-9
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,11 +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._
30+
import config.Printers.{implicits, implicitsDetailed}
2731
import config.{JavaPlatform, SJSPlatform, Platform, ScalaSettings, ScalaRelease}
2832
import classfile.ReusableDataReader
2933
import StdNames.nme
@@ -42,7 +46,9 @@ import dotty.tools.tasty.TastyFormat
4246
import dotty.tools.dotc.config.{ NoScalaVersion, SpecificScalaVersion, AnyScalaVersion, ScalaBuild }
4347
import dotty.tools.dotc.core.tasty.TastyVersion
4448

45-
object Contexts {
49+
import scala.util.chaining.given
50+
51+
object Contexts:
4652

4753
private val (compilerCallbackLoc, store1) = Store.empty.newLocation[CompilerCallback]()
4854
private val (sbtCallbackLoc, store2) = store1.newLocation[AnalysisCallback]()
@@ -54,8 +60,9 @@ object Contexts {
5460
private val (notNullInfosLoc, store8) = store7.newLocation[List[NotNullInfo]]()
5561
private val (importInfoLoc, store9) = store8.newLocation[ImportInfo | Null]()
5662
private val (typeAssignerLoc, store10) = store9.newLocation[TypeAssigner](TypeAssigner)
63+
private val (usagesLoc, store11) = store10.newLocation[Usages]()
5764

58-
private val initialStore = store10
65+
private val initialStore = store11
5966

6067
/** The current context */
6168
inline def ctx(using ctx: Context): Context = ctx
@@ -241,6 +248,9 @@ object Contexts {
241248
/** The current type assigner or typer */
242249
def typeAssigner: TypeAssigner = store(typeAssignerLoc)
243250

251+
/** Tracker for usages of elements such as import selectors. */
252+
def usages: Usages = store(usagesLoc)
253+
244254
/** The new implicit references that are introduced by this scope */
245255
protected var implicitsCache: ContextualImplicits | Null = null
246256
def implicits: ContextualImplicits = {
@@ -249,9 +259,7 @@ object Contexts {
249259
val implicitRefs: List[ImplicitRef] =
250260
if (isClassDefContext)
251261
try owner.thisType.implicitMembers
252-
catch {
253-
case ex: CyclicReference => Nil
254-
}
262+
catch case ex: CyclicReference => Nil
255263
else if (isImportContext) importInfo.nn.importedImplicits
256264
else if (isNonEmptyScopeContext) scope.implicitDecls
257265
else Nil
@@ -477,8 +485,8 @@ object Contexts {
477485
else fresh.setOwner(exprOwner)
478486

479487
/** A new context that summarizes an import statement */
480-
def importContext(imp: Import[?], sym: Symbol): FreshContext =
481-
fresh.setImportInfo(ImportInfo(sym, imp.selectors, imp.expr))
488+
def importContext(imp: Import[?], sym: Symbol, enteringSyms: Boolean = false): FreshContext =
489+
fresh.setImportInfo(ImportInfo(sym, imp.selectors, imp.expr).tap(ii => if enteringSyms && ctx.settings.WunusedHas.imports then usages += ii))
482490

483491
def scalaRelease: ScalaRelease =
484492
val releaseName = base.settings.scalaOutputVersion.value
@@ -829,6 +837,7 @@ object Contexts {
829837
store = initialStore
830838
.updated(settingsStateLoc, settingsGroup.defaultState)
831839
.updated(notNullInfosLoc, Nil)
840+
.updated(usagesLoc, Usages())
832841
.updated(compilationUnitLoc, NoCompilationUnit)
833842
searchHistory = new SearchRoot
834843
gadt = EmptyGadtConstraint
@@ -956,7 +965,7 @@ object Contexts {
956965
private[dotc] var stopInlining: Boolean = false
957966

958967
/** A variable that records that some error was reported in a globally committable context.
959-
* The error will not necessarlily be emitted, since it could still be that
968+
* The error will not necessarily be emitted, since it could still be that
960969
* the enclosing context will be aborted. The variable is used as a smoke test
961970
* to turn off assertions that might be wrong if the program is erroneous. To
962971
* just test for `ctx.reporter.errorsReported` is not always enough, since it
@@ -1013,4 +1022,82 @@ object Contexts {
10131022
if (thread == null) thread = Thread.currentThread()
10141023
else assert(thread == Thread.currentThread(), "illegal multithreaded access to ContextBase")
10151024
}
1016-
}
1025+
end ContextState
1026+
1027+
/** Collect information about the run for purposes of additional diagnostics.
1028+
*/
1029+
class Usages:
1030+
import rewrites.Rewrites.patch
1031+
private val selectors = mutable.Map.empty[ImportInfo, Set[untpd.ImportSelector]].withDefaultValue(Set.empty)
1032+
private val importInfos = mutable.Map.empty[CompilationUnit, List[(ImportInfo, Symbol)]].withDefaultValue(Nil)
1033+
1034+
// register an import
1035+
def +=(info: ImportInfo)(using Context): Unit =
1036+
def isLanguageImport = info.isLanguageImport && allSourceVersionNames.exists(info.forwardMapping.contains)
1037+
if ctx.settings.WunusedHas.imports && !isLanguageImport && !ctx.owner.is(Enum) && !ctx.compilationUnit.isJava 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 && !ctx.compilationUnit.isJava 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+
def checkUsed(info: ImportInfo, owner: Symbol): Unit =
1053+
val used = selectors(info)
1054+
var needsPatch = false
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+
needsPatch = true
1062+
case _ =>
1063+
cull(info.selectors)
1064+
if needsPatch && ctx.settings.YrewriteImports.value then
1065+
val src = ctx.compilationUnit.source
1066+
val infoPos = info.qualifier.sourcePos
1067+
val lineSource = SourceFile.virtual(name = "import-line.scala", content = infoPos.lineContent)
1068+
val PackageDef(_, pieces) = Parsers.Parser(lineSource).parse(): @unchecked
1069+
// patch if there's just one import on the line, i.e., not import a.b, c.d
1070+
if pieces.length == 1 then
1071+
val retained = info.selectors.filter(sel => sel.isMask || used(sel))
1072+
val selectorSpan = info.selectors.map(_.span).reduce(_ union _)
1073+
val lineSpan = src.lineSpan(infoPos.start)
1074+
if retained.isEmpty then
1075+
patch(src, lineSpan, "") // line deletion
1076+
else if retained.size == 1 && info.selectors.size > 1 then
1077+
var starting = info.selectors.head.span.start
1078+
while starting > lineSpan.start && src.content()(starting) != '{' do starting -= 1
1079+
var ending = info.selectors.last.span.end
1080+
while ending <= lineSpan.end && src.content()(ending) != '}' do ending += 1
1081+
if ending < lineSpan.end then ending += 1 // past the close brace
1082+
val widened = selectorSpan.withStart(starting).withEnd(ending)
1083+
patch(src, widened, toText(retained)) // try to remove braces
1084+
else
1085+
patch(src, selectorSpan, toText(retained))
1086+
end checkUsed
1087+
importInfos.remove(ctx.compilationUnit).foreach(_.foreach(checkUsed))
1088+
unusages
1089+
end unused
1090+
1091+
// just the selectors, no need to add braces
1092+
private def toText(retained: List[untpd.ImportSelector])(using Context): String =
1093+
def selected(sel: untpd.ImportSelector) =
1094+
if sel.isGiven then "given"
1095+
else if sel.isWildcard then "*"
1096+
else if sel.name == sel.rename then sel.name.show
1097+
else s"${sel.name.show} as ${sel.rename.show}"
1098+
retained.map(selected).mkString(", ")
1099+
1100+
def clear()(using Context): Unit =
1101+
importInfos.clear()
1102+
selectors.clear()
1103+
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/printing/PlainPrinter.scala

+9-3
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

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ object WConf:
7171
case ErrorId(num) =>
7272
ErrorMessageID.fromErrorNumber(num.toInt) match
7373
case Some(errId) => Right(MessageID(errId))
74-
case _ => Left(s"unknonw error message number: E$num")
74+
case _ => Left(s"unknown error message number: E$num")
7575
case _ =>
7676
Left(s"invalid error message id: $conf")
7777
case "name" =>

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
@@ -1119,8 +1126,12 @@ trait Implicits:
11191126
SearchFailure(adapted.withType(new MismatchedImplicit(ref, pt, argument)))
11201127
}
11211128
else
1129+
cand match
1130+
case Candidate(k: ImportedImplicitRef, _, _) => ctx.usages.use(k.importInfo, k.importInfo.selectors(k.selector))
1131+
case _ =>
11221132
SearchSuccess(adapted, ref, cand.level, cand.isExtension)(ctx.typerState, ctx.gadt)
11231133
}
1134+
end typedImplicit
11241135

11251136
/** An implicit search; parameters as in `inferImplicit` */
11261137
class ImplicitSearch(protected val pt: Type, protected val argument: Tree, span: Span)(using Context):
@@ -1239,6 +1250,7 @@ trait Implicits:
12391250
else if diff > 0 then alt1
12401251
else SearchFailure(new AmbiguousImplicits(alt1, alt2, pt, argument), span)
12411252
case _: SearchFailure => alt2
1253+
end disambiguate
12421254

12431255
/** Try to find a best matching implicit term among all the candidates in `pending`.
12441256
* @param pending The list of candidates that remain to be tested
@@ -1308,6 +1320,7 @@ trait Implicits:
13081320
if (rfailures.isEmpty) found
13091321
else found.recoverWith(_ => rfailures.reverse.maxBy(_.tree.treeSize))
13101322
}
1323+
end rank
13111324

13121325
def negateIfNot(result: SearchResult) =
13131326
if (isNotGiven)

0 commit comments

Comments
 (0)