Skip to content

Commit d40f892

Browse files
committed
Warn unused imports
Handle language feature imports. Skip Java sources. Support rewrite. Disable test.
1 parent fb618ad commit d40f892

18 files changed

+269
-45
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

+4-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

@@ -151,12 +151,13 @@ private sealed trait WarningSettings:
151151
name = "-Wunused",
152152
helpArg = "warning",
153153
descr = "Enable or disable specific `unused` warnings",
154-
choices = List("nowarn", "all"),
154+
choices = List("nowarn", "all", "imports"),
155155
default = Nil
156156
)
157157
object WunusedHas:
158158
def allOr(s: String)(using Context) = Wunused.value.pipe(us => us.contains("all") || us.contains(s))
159159
def nowarn(using Context) = allOr("nowarn")
160+
def imports(using Context) = allOr("imports")
160161

161162
val Wconf: Setting[List[String]] = MultiStringSetting(
162163
"-Wconf",

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

+97-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._
@@ -23,7 +24,9 @@ import reporting._
2324
import io.{AbstractFile, NoAbstractFile, PlainFile, Path}
2425
import scala.io.Codec
2526
import collection.mutable
27+
import parsing.Parsers
2628
import printing._
29+
import config.Printers.{implicits, implicitsDetailed}
2730
import config.{JavaPlatform, SJSPlatform, Platform, ScalaSettings, ScalaRelease}
2831
import classfile.ReusableDataReader
2932
import StdNames.nme
@@ -42,7 +45,9 @@ import dotty.tools.tasty.TastyFormat
4245
import dotty.tools.dotc.config.{ NoScalaVersion, SpecificScalaVersion, AnyScalaVersion, ScalaBuild }
4346
import dotty.tools.dotc.core.tasty.TastyVersion
4447

45-
object Contexts {
48+
import scala.util.chaining.given
49+
50+
object Contexts:
4651

4752
private val (compilerCallbackLoc, store1) = Store.empty.newLocation[CompilerCallback]()
4853
private val (sbtCallbackLoc, store2) = store1.newLocation[AnalysisCallback]()
@@ -54,8 +59,9 @@ object Contexts {
5459
private val (notNullInfosLoc, store8) = store7.newLocation[List[NotNullInfo]]()
5560
private val (importInfoLoc, store9) = store8.newLocation[ImportInfo | Null]()
5661
private val (typeAssignerLoc, store10) = store9.newLocation[TypeAssigner](TypeAssigner)
62+
private val (usagesLoc, store11) = store10.newLocation[Usages](Usages())
5763

58-
private val initialStore = store10
64+
private val initialStore = store11
5965

6066
/** The current context */
6167
inline def ctx(using ctx: Context): Context = ctx
@@ -241,6 +247,9 @@ object Contexts {
241247
/** The current type assigner or typer */
242248
def typeAssigner: TypeAssigner = store(typeAssignerLoc)
243249

250+
/** Tracker for usages of elements such as import selectors. */
251+
def usages: Usages = store(usagesLoc)
252+
244253
/** The new implicit references that are introduced by this scope */
245254
protected var implicitsCache: ContextualImplicits | Null = null
246255
def implicits: ContextualImplicits = {
@@ -249,9 +258,7 @@ object Contexts {
249258
val implicitRefs: List[ImplicitRef] =
250259
if (isClassDefContext)
251260
try owner.thisType.implicitMembers
252-
catch {
253-
case ex: CyclicReference => Nil
254-
}
261+
catch case ex: CyclicReference => Nil
255262
else if (isImportContext) importInfo.nn.importedImplicits
256263
else if (isNonEmptyScopeContext) scope.implicitDecls
257264
else Nil
@@ -477,8 +484,8 @@ object Contexts {
477484
else fresh.setOwner(exprOwner)
478485

479486
/** 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))
487+
def importContext(imp: Import[?], sym: Symbol, enteringSyms: Boolean = false): FreshContext =
488+
fresh.setImportInfo(ImportInfo(sym, imp.selectors, imp.expr).tap(ii => if enteringSyms && ctx.settings.WunusedHas.imports then usages += ii))
482489

483490
def scalaRelease: ScalaRelease =
484491
val releaseName = base.settings.scalaOutputVersion.value
@@ -956,7 +963,7 @@ object Contexts {
956963
private[dotc] var stopInlining: Boolean = false
957964

958965
/** 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
966+
* The error will not necessarily be emitted, since it could still be that
960967
* the enclosing context will be aborted. The variable is used as a smoke test
961968
* to turn off assertions that might be wrong if the program is erroneous. To
962969
* just test for `ctx.reporter.errorsReported` is not always enough, since it
@@ -1013,4 +1020,85 @@ object Contexts {
10131020
if (thread == null) thread = Thread.currentThread()
10141021
else assert(thread == Thread.currentThread(), "illegal multithreaded access to ContextBase")
10151022
}
1016-
}
1023+
end ContextState
1024+
1025+
/** Collect information about the run for purposes of additional diagnostics.
1026+
*/
1027+
class Usages:
1028+
import rewrites.Rewrites.patch
1029+
private val selectors = mutable.Map.empty[ImportInfo, Set[untpd.ImportSelector]].withDefaultValue(Set.empty)
1030+
private val importInfos = mutable.Map.empty[CompilationUnit, List[(ImportInfo, Symbol)]].withDefaultValue(Nil)
1031+
1032+
// register an import
1033+
def +=(info: ImportInfo)(using Context): Unit =
1034+
if ctx.settings.WunusedHas.imports && !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+
val decidingFactor = false // whether to apply patches
1062+
if decidingFactor && needsPatch && !ctx.settings.rewrite.value.isEmpty then
1063+
val retained = info.selectors.filter(sel => sel.isMask || used(sel))
1064+
val infoPos = info.qualifier.sourcePos
1065+
val lineText = infoPos.lineContent
1066+
val src = ctx.compilationUnit.source
1067+
val lineSpan = src.lineSpan(infoPos.start)
1068+
val selectorSpan = info.selectors.map(_.span).reduce(_ union _)
1069+
val lineSource = SourceFile.virtual(name = "import-line.scala", content = lineText)
1070+
val parser = Parsers.Parser(lineSource)
1071+
val lineTree = parser.parse()
1072+
val PackageDef(_, pieces) = lineTree: @unchecked
1073+
// patch if there's just one import on the line, i.e., not import a.b, c.d
1074+
if pieces.length == 1 then
1075+
if retained.isEmpty then
1076+
patch(src, lineSpan, "") // line deletion
1077+
else if retained.size == 1 && info.selectors.size > 1 then
1078+
var starting = info.selectors.head.span.start
1079+
while starting > lineSpan.start && src.content()(starting) != '{' do starting -= 1
1080+
var ending = info.selectors.last.span.end
1081+
while ending <= lineSpan.end && src.content()(ending) != '}' do ending += 1
1082+
if ending < lineSpan.end then ending += 1 // past the close brace
1083+
val widened = selectorSpan.withStart(starting).withEnd(ending)
1084+
patch(src, widened, toText(retained)) // try to remove braces
1085+
else
1086+
patch(src, selectorSpan, toText(retained))
1087+
end checkUsed
1088+
importInfos.remove(ctx.compilationUnit).foreach(_.foreach(checkUsed))
1089+
unusages
1090+
end unused
1091+
1092+
// just the selectors, no need to add braces
1093+
private def toText(retained: List[untpd.ImportSelector])(using Context): String =
1094+
def selected(sel: untpd.ImportSelector) =
1095+
if sel.isGiven then "given"
1096+
else if sel.isWildcard then "*"
1097+
else if sel.name == sel.rename then sel.name.show
1098+
else s"${sel.name.show} as ${sel.rename.show}"
1099+
retained.map(selected).mkString(", ")
1100+
1101+
def clear()(using Context): Unit =
1102+
importInfos.clear()
1103+
selectors.clear()
1104+
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
@@ -1117,8 +1124,12 @@ trait Implicits:
11171124
SearchFailure(adapted.withType(new MismatchedImplicit(ref, pt, argument)))
11181125
}
11191126
else
1127+
cand match
1128+
case Candidate(k: ImportedImplicitRef, _, _) => ctx.usages.use(k.importInfo, k.importInfo.selectors(k.selector))
1129+
case _ =>
11201130
SearchSuccess(adapted, ref, cand.level, cand.isExtension)(ctx.typerState, ctx.gadt)
11211131
}
1132+
end typedImplicit
11221133

11231134
/** An implicit search; parameters as in `inferImplicit` */
11241135
class ImplicitSearch(protected val pt: Type, protected val argument: Tree, span: Span)(using Context):
@@ -1237,6 +1248,7 @@ trait Implicits:
12371248
else if diff > 0 then alt1
12381249
else SearchFailure(new AmbiguousImplicits(alt1, alt2, pt, argument), span)
12391250
case _: SearchFailure => alt2
1251+
end disambiguate
12401252

12411253
/** Try to find a best matching implicit term among all the candidates in `pending`.
12421254
* @param pending The list of candidates that remain to be tested
@@ -1306,6 +1318,7 @@ trait Implicits:
13061318
if (rfailures.isEmpty) found
13071319
else found.recoverWith(_ => rfailures.reverse.maxBy(_.tree.treeSize))
13081320
}
1321+
end rank
13091322

13101323
def negateIfNot(result: SearchResult) =
13111324
if (isNotGiven)

0 commit comments

Comments
 (0)