Skip to content

Commit 2097657

Browse files
committed
Warn about unused imports.
Hidden behind -Xlint as usual. This commit also includes further simplification of the symbol lookup logic which I unearthed on the way to reporting unused imports. Plus unusually comprehensive documentation of same.
1 parent 36edc79 commit 2097657

File tree

5 files changed

+319
-58
lines changed

5 files changed

+319
-58
lines changed

src/compiler/scala/tools/nsc/symtab/classfile/Pickler.scala

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,21 @@ abstract class Pickler extends SubComponent {
2828

2929
val phaseName = "pickler"
3030

31-
currentRun
32-
3331
def newPhase(prev: Phase): StdPhase = new PicklePhase(prev)
3432

3533
class PicklePhase(prev: Phase) extends StdPhase(prev) {
34+
override def run() {
35+
super.run()
36+
// This is run here rather than after typer because I found
37+
// some symbols - usually annotations, possibly others - had not
38+
// yet performed the necessary symbol lookup, leading to
39+
// spurious claims of unusedness.
40+
if (settings.lint.value) {
41+
log("Clearing recorded import selectors.")
42+
analyzer.clearUnusedImports()
43+
}
44+
}
45+
3646
def apply(unit: CompilationUnit) {
3747
def pickle(tree: Tree) {
3848
def add(sym: Symbol, pickle: Pickle) = {
@@ -77,6 +87,8 @@ abstract class Pickler extends SubComponent {
7787
}
7888

7989
pickle(unit.body)
90+
if (settings.lint.value)
91+
analyzer.warnUnusedImports(unit)
8092
}
8193
}
8294

src/compiler/scala/tools/nsc/typechecker/Contexts.scala

Lines changed: 135 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
package scala.tools.nsc
77
package typechecker
88

9-
import scala.collection.mutable.{LinkedHashSet, Set}
9+
import scala.collection.mutable
1010
import scala.annotation.tailrec
1111

1212
/**
@@ -15,6 +15,7 @@ import scala.annotation.tailrec
1515
*/
1616
trait Contexts { self: Analyzer =>
1717
import global._
18+
import definitions.{ JavaLangPackage, ScalaPackage, PredefModule }
1819

1920
object NoContext extends Context {
2021
outer = this
@@ -27,7 +28,6 @@ trait Contexts { self: Analyzer =>
2728
override def toString = "NoContext"
2829
}
2930
private object RootImports {
30-
import definitions._
3131
// Possible lists of root imports
3232
val javaList = JavaLangPackage :: Nil
3333
val javaAndScalaList = JavaLangPackage :: ScalaPackage :: Nil
@@ -46,6 +46,28 @@ trait Contexts { self: Analyzer =>
4646
rootMirror.RootClass.info.decls)
4747
}
4848

49+
private lazy val allUsedSelectors =
50+
mutable.Map[ImportInfo, Set[ImportSelector]]() withDefaultValue Set()
51+
private lazy val allImportInfos =
52+
mutable.Map[CompilationUnit, List[ImportInfo]]() withDefaultValue Nil
53+
54+
def clearUnusedImports() {
55+
allUsedSelectors.clear()
56+
allImportInfos.clear()
57+
}
58+
def warnUnusedImports(unit: CompilationUnit) = {
59+
val imps = allImportInfos(unit).reverse.distinct
60+
61+
for (imp <- imps) {
62+
val used = allUsedSelectors(imp)
63+
def isMask(s: ImportSelector) = s.name != nme.WILDCARD && s.rename == nme.WILDCARD
64+
65+
imp.tree.selectors filterNot (s => isMask(s) || used(s)) foreach { sel =>
66+
unit.warning(imp posOf sel, "Unused import")
67+
}
68+
}
69+
}
70+
4971
var lastAccessCheckDetails: String = ""
5072

5173
/** List of symbols to import from in a root context. Typically that
@@ -146,7 +168,7 @@ trait Contexts { self: Analyzer =>
146168
var typingIndentLevel: Int = 0
147169
def typingIndent = " " * typingIndentLevel
148170

149-
var buffer: Set[AbsTypeError] = _
171+
var buffer: mutable.Set[AbsTypeError] = _
150172

151173
def enclClassOrMethod: Context =
152174
if ((owner eq NoSymbol) || (owner.isClass) || (owner.isMethod)) this
@@ -185,13 +207,13 @@ trait Contexts { self: Analyzer =>
185207
def setThrowErrors() = mode &= (~AllMask)
186208
def setAmbiguousErrors(report: Boolean) = if (report) mode |= AmbiguousErrors else mode &= notThrowMask
187209

188-
def updateBuffer(errors: Set[AbsTypeError]) = buffer ++= errors
210+
def updateBuffer(errors: mutable.Set[AbsTypeError]) = buffer ++= errors
189211
def condBufferFlush(removeP: AbsTypeError => Boolean) {
190212
val elems = buffer.filter(removeP)
191213
buffer --= elems
192214
}
193215
def flushBuffer() { buffer.clear() }
194-
def flushAndReturnBuffer(): Set[AbsTypeError] = {
216+
def flushAndReturnBuffer(): mutable.Set[AbsTypeError] = {
195217
val current = buffer.clone()
196218
buffer.clear()
197219
current
@@ -284,7 +306,7 @@ trait Contexts { self: Analyzer =>
284306
c.checking = this.checking
285307
c.retyping = this.retyping
286308
c.openImplicits = this.openImplicits
287-
c.buffer = if (this.buffer == null) LinkedHashSet[AbsTypeError]() else this.buffer // need to initialize
309+
c.buffer = if (this.buffer == null) mutable.LinkedHashSet[AbsTypeError]() else this.buffer // need to initialize
288310
registerContext(c.asInstanceOf[analyzer.Context])
289311
debuglog("[context] ++ " + c.unit + " / " + tree.summaryString)
290312
c
@@ -302,8 +324,13 @@ trait Contexts { self: Analyzer =>
302324
def makeNewImport(sym: Symbol): Context =
303325
makeNewImport(gen.mkWildcardImport(sym))
304326

305-
def makeNewImport(imp: Import): Context =
306-
make(unit, imp, owner, scope, new ImportInfo(imp, depth) :: imports)
327+
def makeNewImport(imp: Import): Context = {
328+
val impInfo = new ImportInfo(imp, depth)
329+
if (settings.lint.value && imp.pos.isDefined) // pos.isDefined excludes java.lang/scala/Predef imports
330+
allImportInfos(unit) ::= impInfo
331+
332+
make(unit, imp, owner, scope, impInfo :: imports)
333+
}
307334

308335
def make(tree: Tree, owner: Symbol, scope: Scope): Context =
309336
if (tree == this.tree && owner == this.owner && scope == this.scope) this
@@ -326,7 +353,7 @@ trait Contexts { self: Analyzer =>
326353
val c = make(newtree)
327354
c.setBufferErrors()
328355
c.setAmbiguousErrors(reportAmbiguousErrors)
329-
c.buffer = new LinkedHashSet[AbsTypeError]()
356+
c.buffer = mutable.LinkedHashSet[AbsTypeError]()
330357
c
331358
}
332359

@@ -672,10 +699,13 @@ trait Contexts { self: Analyzer =>
672699
* package object foo { type InputStream = java.io.InputStream }
673700
* import foo._, java.io._
674701
*/
675-
def isAmbiguousImport(imp1: ImportInfo, imp2: ImportInfo, name: Name): Boolean = {
676-
// The imported symbols from each import.
677-
def imp1Symbol = importedAccessibleSymbol(imp1, name)
678-
def imp2Symbol = importedAccessibleSymbol(imp2, name)
702+
private def resolveAmbiguousImport(name: Name, imp1: ImportInfo, imp2: ImportInfo): Option[ImportInfo] = {
703+
val imp1Explicit = imp1 isExplicitImport name
704+
val imp2Explicit = imp2 isExplicitImport name
705+
val ambiguous = if (imp1.depth == imp2.depth) imp1Explicit == imp2Explicit else !imp1Explicit && imp2Explicit
706+
val imp1Symbol = (imp1 importedSymbol name).initialize filter (s => isAccessible(s, imp1.qual.tpe, superAccess = false))
707+
val imp2Symbol = (imp2 importedSymbol name).initialize filter (s => isAccessible(s, imp2.qual.tpe, superAccess = false))
708+
679709
// The types of the qualifiers from which the ambiguous imports come.
680710
// If the ambiguous name is a value, these must be the same.
681711
def t1 = imp1.qual.tpe
@@ -691,35 +721,39 @@ trait Contexts { self: Analyzer =>
691721
s"member type 2: $mt2"
692722
).mkString("\n ")
693723

694-
imp1Symbol.exists && imp2Symbol.exists && (
724+
if (!ambiguous || !imp2Symbol.exists) Some(imp1)
725+
else if (!imp1Symbol.exists) Some(imp2)
726+
else (
695727
// The symbol names are checked rather than the symbols themselves because
696728
// each time an overloaded member is looked up it receives a new symbol.
697729
// So foo.member("x") != foo.member("x") if x is overloaded. This seems
698730
// likely to be the cause of other bugs too...
699731
if (t1 =:= t2 && imp1Symbol.name == imp2Symbol.name) {
700732
log(s"Suppressing ambiguous import: $t1 =:= $t2 && $imp1Symbol == $imp2Symbol")
701-
false
733+
Some(imp1)
702734
}
703735
// Monomorphism restriction on types is in part because type aliases could have the
704736
// same target type but attach different variance to the parameters. Maybe it can be
705737
// relaxed, but doesn't seem worth it at present.
706738
else if (mt1 =:= mt2 && name.isTypeName && imp1Symbol.isMonomorphicType && imp2Symbol.isMonomorphicType) {
707739
log(s"Suppressing ambiguous import: $mt1 =:= $mt2 && $imp1Symbol and $imp2Symbol are equivalent")
708-
false
740+
Some(imp1)
709741
}
710742
else {
711743
log(s"Import is genuinely ambiguous:\n " + characterize)
712-
true
744+
None
713745
}
714746
)
715747
}
716748

717749
/** The symbol with name `name` imported via the import in `imp`,
718750
* if any such symbol is accessible from this context.
719751
*/
720-
def importedAccessibleSymbol(imp: ImportInfo, name: Name) = {
721-
imp importedSymbol name filter (s => isAccessible(s, imp.qual.tpe, superAccess = false))
722-
}
752+
def importedAccessibleSymbol(imp: ImportInfo, name: Name): Symbol =
753+
importedAccessibleSymbol(imp, name, requireExplicit = false)
754+
755+
private def importedAccessibleSymbol(imp: ImportInfo, name: Name, requireExplicit: Boolean): Symbol =
756+
imp.importedSymbol(name, requireExplicit) filter (s => isAccessible(s, imp.qual.tpe, superAccess = false))
723757

724758
/** Is `sym` defined in package object of package `pkg`?
725759
* Since sym may be defined in some parent of the package object,
@@ -814,11 +848,15 @@ trait Contexts { self: Analyzer =>
814848
var imports = Context.this.imports
815849
def imp1 = imports.head
816850
def imp2 = imports.tail.head
851+
def sameDepth = imp1.depth == imp2.depth
817852
def imp1Explicit = imp1 isExplicitImport name
818853
def imp2Explicit = imp2 isExplicitImport name
819854

820-
while (!qualifies(impSym) && imports.nonEmpty && imp1.depth > symbolDepth) {
821-
impSym = importedAccessibleSymbol(imp1, name)
855+
def lookupImport(imp: ImportInfo, requireExplicit: Boolean) =
856+
importedAccessibleSymbol(imp, name, requireExplicit) filter qualifies
857+
858+
while (!impSym.exists && imports.nonEmpty && imp1.depth > symbolDepth) {
859+
impSym = lookupImport(imp1, requireExplicit = false)
822860
if (!impSym.exists)
823861
imports = imports.tail
824862
}
@@ -843,33 +881,45 @@ trait Contexts { self: Analyzer =>
843881
finish(EmptyTree, defSym)
844882
}
845883
else if (impSym.exists) {
846-
def sameDepth = imp1.depth == imp2.depth
847-
def needsCheck = if (sameDepth) imp1Explicit == imp2Explicit else imp1Explicit || imp2Explicit
848-
def isDone = imports.tail.isEmpty || (!sameDepth && imp1Explicit)
849-
def ambiguous = needsCheck && isAmbiguousImport(imp1, imp2, name) && {
850-
lookupError = ambiguousImports(imp1, imp2)
851-
true
852-
}
853-
// Ambiguity check between imports.
854-
// The same name imported again is potentially ambiguous if the name is:
855-
// - after explicit import, explicitly imported again at the same or lower depth
856-
// - after explicit import, wildcard imported at lower depth
857-
// - after wildcard import, wildcard imported at the same depth
858-
// Under all such conditions isAmbiguousImport is called, which will
859-
// examine the imports in case they are importing the same thing; if that
860-
// can't be established conclusively, an error is issued.
861-
while (lookupError == null && !isDone) {
862-
val other = importedAccessibleSymbol(imp2, name)
863-
// if the competing import is unambiguous and explicit, it is the new winner.
864-
val isNewWinner = qualifies(other) && !ambiguous && imp2Explicit
865-
// imports is imp1 :: imp2 :: rest.
866-
// If there is a new winner, it is imp2, and imports drops imp1.
867-
// If there is not, imp1 is still the winner, and it drops imp2.
868-
if (isNewWinner) {
869-
impSym = other
870-
imports = imports.tail
884+
// We continue walking down the imports as long as the tail is non-empty, which gives us:
885+
// imports == imp1 :: imp2 :: _
886+
// And at least one of the following is true:
887+
// - imp1 and imp2 are at the same depth
888+
// - imp1 is a wildcard import, so all explicit imports from outer scopes must be checked
889+
def keepLooking = (
890+
lookupError == null
891+
&& imports.tail.nonEmpty
892+
&& (sameDepth || !imp1Explicit)
893+
)
894+
// If we find a competitor imp2 which imports the same name, possible outcomes are:
895+
//
896+
// - same depth, imp1 wild, imp2 explicit: imp2 wins, drop imp1
897+
// - same depth, imp1 wild, imp2 wild: ambiguity check
898+
// - same depth, imp1 explicit, imp2 explicit: ambiguity check
899+
// - differing depth, imp1 wild, imp2 explicit: ambiguity check
900+
// - all others: imp1 wins, drop imp2
901+
//
902+
// The ambiguity check is: if we can verify that both imports refer to the same
903+
// symbol (e.g. import foo.X followed by import foo._) then we discard imp2
904+
// and proceed. If we cannot, issue an ambiguity error.
905+
while (keepLooking) {
906+
// If not at the same depth, limit the lookup to explicit imports.
907+
// This is desirable from a performance standpoint (compare to
908+
// filtering after the fact) but also necessary to keep the unused
909+
// import check from being misled by symbol lookups which are not
910+
// actually used.
911+
val other = lookupImport(imp2, requireExplicit = !sameDepth)
912+
def imp1wins = { imports = imp1 :: imports.tail.tail }
913+
def imp2wins = { impSym = other ; imports = imports.tail }
914+
915+
if (!other.exists) // imp1 wins; drop imp2 and continue.
916+
imp1wins
917+
else if (sameDepth && !imp1Explicit && imp2Explicit) // imp2 wins; drop imp1 and continue.
918+
imp2wins
919+
else resolveAmbiguousImport(name, imp1, imp2) match {
920+
case Some(imp) => if (imp eq imp1) imp1wins else imp2wins
921+
case _ => lookupError = ambiguousImports(imp1, imp2)
871922
}
872-
else imports = imp1 :: imports.tail.tail
873923
}
874924
// optimization: don't write out package prefixes
875925
finish(resetPos(imp1.qual.duplicate), impSym)
@@ -901,6 +951,9 @@ trait Contexts { self: Analyzer =>
901951
} //class Context
902952

903953
class ImportInfo(val tree: Import, val depth: Int) {
954+
def pos = tree.pos
955+
def posOf(sel: ImportSelector) = tree.pos withPoint sel.namePos
956+
904957
/** The prefix expression */
905958
def qual: Tree = tree.symbol.info match {
906959
case ImportType(expr) => expr
@@ -914,22 +967,43 @@ trait Contexts { self: Analyzer =>
914967

915968
/** The symbol with name `name` imported from import clause `tree`.
916969
*/
917-
def importedSymbol(name: Name): Symbol = {
970+
def importedSymbol(name: Name): Symbol = importedSymbol(name, requireExplicit = false)
971+
972+
private def recordUsage(sel: ImportSelector, result: Symbol) {
973+
def posstr = pos.source.file.name + ":" + posOf(sel).safeLine
974+
def resstr = if (tree.symbol.hasCompleteInfo) s"(qual=$qual, $result)" else s"(expr=${tree.expr}, ${result.fullLocationString})"
975+
debuglog(s"In $this at $posstr, selector '${selectorString(sel)}' resolved to $resstr")
976+
allUsedSelectors(this) += sel
977+
}
978+
979+
/** If requireExplicit is true, wildcard imports are not considered. */
980+
def importedSymbol(name: Name, requireExplicit: Boolean): Symbol = {
918981
var result: Symbol = NoSymbol
919982
var renamed = false
920983
var selectors = tree.selectors
921-
while (selectors != Nil && result == NoSymbol) {
922-
if (selectors.head.rename == name.toTermName)
984+
def current = selectors.head
985+
while (selectors.nonEmpty && result == NoSymbol) {
986+
if (current.rename == name.toTermName)
923987
result = qual.tpe.nonLocalMember( // new to address #2733: consider only non-local members for imports
924-
if (name.isTypeName) selectors.head.name.toTypeName else selectors.head.name)
925-
else if (selectors.head.name == name.toTermName)
988+
if (name.isTypeName) current.name.toTypeName else current.name)
989+
else if (current.name == name.toTermName)
926990
renamed = true
927-
else if (selectors.head.name == nme.WILDCARD && !renamed)
991+
else if (current.name == nme.WILDCARD && !renamed && !requireExplicit)
928992
result = qual.tpe.nonLocalMember(name)
929-
selectors = selectors.tail
993+
994+
if (result == NoSymbol)
995+
selectors = selectors.tail
930996
}
997+
if (settings.lint.value && selectors.nonEmpty && result != NoSymbol && pos != NoPosition)
998+
recordUsage(current, result)
999+
9311000
result
9321001
}
1002+
private def selectorString(s: ImportSelector): String = {
1003+
if (s.name == nme.WILDCARD && s.rename == null) "_"
1004+
else if (s.name == s.rename) "" + s.name
1005+
else s.name + " => " + s.rename
1006+
}
9331007

9341008
def allImportedSymbols: Iterable[Symbol] =
9351009
qual.tpe.members flatMap (transformImport(tree.selectors, _))
@@ -943,7 +1017,12 @@ trait Contexts { self: Analyzer =>
9431017
case _ :: rest => transformImport(rest, sym)
9441018
}
9451019

946-
override def toString() = tree.toString()
1020+
override def hashCode = tree.##
1021+
override def equals(other: Any) = other match {
1022+
case that: ImportInfo => (tree == that.tree)
1023+
case _ => false
1024+
}
1025+
override def toString = tree.toString
9471026
}
9481027

9491028
case class ImportType(expr: Tree) extends Type {

0 commit comments

Comments
 (0)