Skip to content

Commit 933d8fa

Browse files
committed
Simplify the logic for checking unused imports.
Instead of dealing with entire `tpd.Import`s at the end of the scope, we eagerly flatten them into individual `ImportSelector`s. We store them along with some data, include a mutable flag for whether a selector has been used. This allows to dramatically simplify `isInImport`, as well as more aggressively cache the resolution of selectors. We also get rid of the `IdentityHashMap`. The algorithm is still O(n*m) where n is the number of imports in a scope, and m the number of references found in that scope. It is not entirely clear to me whether the previous logic was already O(n*m) or worse (it may have included an additional p factor for the number of possible selections from a given qualifier).
1 parent 549ab86 commit 933d8fa

File tree

2 files changed

+111
-97
lines changed

2 files changed

+111
-97
lines changed

compiler/src/dotty/tools/dotc/transform/CheckUnused.scala

+109-95
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dotty.tools.dotc.transform
22

33
import scala.annotation.tailrec
44

5+
import dotty.tools.uncheckedNN
56
import dotty.tools.dotc.ast.tpd
67
import dotty.tools.dotc.core.Symbols.*
78
import dotty.tools.dotc.ast.tpd.{Inlined, TreeTraverser}
@@ -109,7 +110,7 @@ class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _ke
109110
ctx
110111

111112
override def prepareForSelect(tree: tpd.Select)(using Context): Context =
112-
unusedDataApply(_.registerUsed(tree.symbol, Some(tree.name)))
113+
unusedDataApply(_.registerUsed(tree.symbol, None, notForImport = tree.qualifier.span.isSourceDerived))
113114

114115
override def prepareForBlock(tree: tpd.Block)(using Context): Context =
115116
pushInBlockTemplatePackageDef(tree)
@@ -127,7 +128,7 @@ class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _ke
127128
if !tree.symbol.is(Module) then
128129
ud.registerDef(tree)
129130
if tree.name.startsWith("derived$") && tree.typeOpt != NoType then
130-
ud.registerUsed(tree.typeOpt.typeSymbol, None, true)
131+
ud.registerUsed(tree.typeOpt.typeSymbol, None, isDerived = true)
131132
ud.addIgnoredUsage(tree.symbol)
132133
}
133134

@@ -356,7 +357,7 @@ object CheckUnused:
356357
var unusedAggregate: Option[UnusedResult] = None
357358

358359
/* IMPORTS */
359-
private val impInScope = MutStack(MutList[tpd.Import]())
360+
private val impInScope = MutStack(MutList[ImportSelectorData]())
360361
/**
361362
* We store the symbol along with their accessibility without import.
362363
* Accessibility to their definition in outer context/scope
@@ -366,7 +367,7 @@ object CheckUnused:
366367
private val usedInScope = MutStack(MutSet[(Symbol,Boolean, Option[Name], Boolean)]())
367368
private val usedInPosition = MutMap.empty[Name, MutSet[Symbol]]
368369
/* unused import collected during traversal */
369-
private val unusedImport = new java.util.IdentityHashMap[ImportSelector, Unit]
370+
private val unusedImport = MutList.empty[ImportSelectorData]
370371

371372
/* LOCAL DEF OR VAL / Private Def or Val / Pattern variables */
372373
private val localDefInScope = MutList.empty[tpd.MemberDef]
@@ -406,16 +407,17 @@ object CheckUnused:
406407
* The optional name will be used to target the right import
407408
* as the same element can be imported with different renaming
408409
*/
409-
def registerUsed(sym: Symbol, name: Option[Name], isDerived: Boolean = false)(using Context): Unit =
410+
def registerUsed(sym: Symbol, name: Option[Name], notForImport: Boolean = false, isDerived: Boolean = false)(using Context): Unit =
410411
if sym.exists && !isConstructorOfSynth(sym) && !doNotRegister(sym) then
411412
if sym.isConstructor then
412-
registerUsed(sym.owner, None) // constructor are "implicitly" imported with the class
413+
registerUsed(sym.owner, None, notForImport) // constructor are "implicitly" imported with the class
413414
else
414415
val accessibleAsIdent = sym.isAccessibleAsIdent
415416
def addIfExists(sym: Symbol): Unit =
416417
if sym.exists then
417418
usedDef += sym
418-
usedInScope.top += ((sym, accessibleAsIdent, name, isDerived))
419+
if !notForImport then
420+
usedInScope.top += ((sym, accessibleAsIdent, name, isDerived))
419421
addIfExists(sym)
420422
addIfExists(sym.companionModule)
421423
addIfExists(sym.companionClass)
@@ -436,12 +438,27 @@ object CheckUnused:
436438

437439
/** Register an import */
438440
def registerImport(imp: tpd.Import)(using Context): Unit =
439-
if !tpd.languageImport(imp.expr).nonEmpty && !imp.isGeneratedByEnum && !isTransparentAndInline(imp) then
440-
impInScope.top += imp
441-
if currScopeType.top != ScopeType.ReplWrapper then // #18383 Do not report top-level import's in the repl as unused
442-
for s <- imp.selectors do
443-
if !shouldSelectorBeReported(imp, s) && !isImportExclusion(s) && !isImportIgnored(imp, s) then
444-
unusedImport.put(s, ())
441+
if
442+
!tpd.languageImport(imp.expr).nonEmpty
443+
&& !imp.isGeneratedByEnum
444+
&& !isTransparentAndInline(imp)
445+
&& currScopeType.top != ScopeType.ReplWrapper // #18383 Do not report top-level import's in the repl as unused
446+
then
447+
val qualTpe = imp.expr.tpe
448+
449+
// Put wildcard imports at the end, because they have lower priority within one Import
450+
val reorderdSelectors =
451+
val (wildcardSels, nonWildcardSels) = imp.selectors.partition(_.isWildcard)
452+
nonWildcardSels ::: wildcardSels
453+
454+
val newDataInScope =
455+
for sel <- reorderdSelectors yield
456+
val data = new ImportSelectorData(qualTpe, sel)
457+
if shouldSelectorBeReported(imp, sel) || isImportExclusion(sel) || isImportIgnored(imp, sel) then
458+
// Immediately mark the selector as used
459+
data.markUsed()
460+
data
461+
impInScope.top.prependAll(newDataInScope)
445462
end registerImport
446463

447464
/** Register (or not) some `val` or `def` according to the context, scope and flags */
@@ -479,40 +496,27 @@ object CheckUnused:
479496
* - If there are imports in this scope check for unused ones
480497
*/
481498
def popScope()(using Context): Unit =
482-
// used symbol in this scope
483-
val used = usedInScope.pop().toSet
484-
// used imports in this scope
485-
val imports = impInScope.pop()
486-
val kept = used.filterNot { (sym, isAccessible, optName, isDerived) =>
487-
// keep the symbol for outer scope, if it matches **no** import
488-
// This is the first matching wildcard selector
489-
var selWildCard: Option[ImportSelector] = None
490-
491-
val matchedExplicitImport = imports.exists { imp =>
492-
sym.isInImport(imp, isAccessible, optName, isDerived) match
493-
case None => false
494-
case optSel@Some(sel) if sel.isWildcard =>
495-
if selWildCard.isEmpty then selWildCard = optSel
496-
// We keep wildcard symbol for the end as they have the least precedence
497-
false
498-
case Some(sel) =>
499-
unusedImport.remove(sel)
500-
true
499+
currScopeType.pop()
500+
val usedInfos = usedInScope.pop()
501+
val selDatas = impInScope.pop()
502+
503+
for usedInfo <- usedInfos do
504+
val (sym, isAccessible, optName, isDerived) = usedInfo
505+
val usedData = selDatas.find { selData =>
506+
sym.isInImport(selData, isAccessible, optName, isDerived)
501507
}
502-
if !matchedExplicitImport && selWildCard.isDefined then
503-
unusedImport.remove(selWildCard.get)
504-
true // a matching import exists so the symbol won't be kept for outer scope
505-
else
506-
matchedExplicitImport
507-
}
508-
509-
// if there's an outer scope
510-
if usedInScope.nonEmpty then
511-
// we keep the symbols not referencing an import in this scope
512-
// as it can be the only reference to an outer import
513-
usedInScope.top ++= kept
514-
// retrieve previous scope type
515-
currScopeType.pop
508+
usedData match
509+
case Some(data) =>
510+
data.markUsed()
511+
case None =>
512+
// Propage the symbol one level up
513+
if usedInScope.nonEmpty then
514+
usedInScope.top += usedInfo
515+
end for // each in `used`
516+
517+
for selData <- selDatas do
518+
if !selData.isUsed then
519+
unusedImport += selData
516520
end popScope
517521

518522
/**
@@ -531,9 +535,8 @@ object CheckUnused:
531535

532536
val sortedImp =
533537
if ctx.settings.WunusedHas.imports || ctx.settings.WunusedHas.strictNoImplicitWarn then
534-
import scala.jdk.CollectionConverters.*
535-
unusedImport.keySet().nn.iterator().nn.asScala
536-
.map(d => UnusedSymbol(d.srcPos, d.name, WarnTypes.Imports)).toList
538+
unusedImport.toList
539+
.map(d => UnusedSymbol(d.selector.srcPos, d.selector.name, WarnTypes.Imports))
537540
else
538541
Nil
539542
// Partition to extract unset local variables from usedLocalDefs
@@ -694,61 +697,40 @@ object CheckUnused:
694697
}
695698

696699
/** Given an import and accessibility, return selector that matches import<->symbol */
697-
private def isInImport(imp: tpd.Import, isAccessible: Boolean, symName: Option[Name], isDerived: Boolean)(using Context): Option[ImportSelector] =
700+
private def isInImport(selData: ImportSelectorData, isAccessible: Boolean, symName: Option[Name], isDerived: Boolean)(using Context): Boolean =
698701
assert(sym.exists)
699702

700-
val tpd.Import(qual, sels) = imp
701-
702-
val renamedSelection: Option[ImportSelector] =
703-
val sameTermPath = qual.isTerm && sym.owner.isType && qual.tpe.typeSymbol == sym.owner.asType
704-
if sameTermPath then sels.find(sel => sel.name == sym.name) else None
705-
706-
if renamedSelection.isDefined then
707-
renamedSelection
708-
else
709-
val dealiasedSym: Symbol = dealias(sym)
710-
711-
val selectionsToDealias: List[SingleDenotation] =
712-
val typeSelections = sels.flatMap(n => qual.tpe.member(n.name.toTypeName).alternatives)
713-
val termSelections = sels.flatMap(n => qual.tpe.member(n.name.toTermName).alternatives)
714-
typeSelections ::: termSelections
715-
716-
val qualHasSymbol: Boolean =
717-
val simpleSelections = qual.tpe.member(sym.name).alternatives
718-
simpleSelections.exists(d => d.symbol == sym || dealias(d.symbol) == dealiasedSym)
719-
|| selectionsToDealias.exists(d => dealias(d.symbol) == dealiasedSym)
703+
val selector = selData.selector
720704

721-
def selector: Option[ImportSelector] =
722-
sels.find(sel => sym.name.toTermName == sel.name && symName.forall(n => n.toTermName == sel.rename))
723-
724-
def dealiasedSelector: Option[ImportSelector] =
705+
if isAccessible && !symName.exists(_.toTermName != sym.name.toTermName) then
706+
// Even if this import matches, it is pointless because the symbol would be accessible anyway
707+
false
708+
else if !selector.isWildcard then
709+
if !symName.forall(explicitName => selector.rename == explicitName.toTermName) then
710+
// if there is an explicit name, it must match
711+
false
712+
else
725713
if isDerived then
726-
sels.flatMap(sel => selectionsToDealias.map(m => (sel, m.symbol))).collectFirst {
727-
case (sel, sym) if dealias(sym) == dealiasedSym => sel
728-
}
729-
else
730-
None
731-
732-
def givenSelector: Option[ImportSelector] =
733-
if sym.is(Given) || sym.is(Implicit) then
734-
sels.filter(sel => sel.isGiven && !sel.bound.isEmpty).find(sel => sel.boundTpe =:= sym.info)
714+
// See i15503i.scala, grep for "package foo.test.i17156"
715+
selData.allSymbolsDealiasedForNamed.contains(dealias(sym))
735716
else
736-
None
737-
738-
def wildcard: Option[ImportSelector] =
739-
sels.find(sel => sel.isWildcard && ((sym.is(Given) == sel.isGiven && sel.bound.isEmpty) || sym.is(Implicit)))
740-
741-
if qualHasSymbol && (!isAccessible || symName.exists(_.toSimpleName != sym.name.toSimpleName)) then
742-
selector.orElse(dealiasedSelector).orElse(givenSelector).orElse(wildcard) // selector with name or wildcard (or given)
717+
selData.allSymbolsForNamed.contains(sym)
718+
else
719+
// Wildcard
720+
if !selData.qualTpe.member(sym.name).hasAltWith(_.symbol == sym) then
721+
// The qualifier does not have the target symbol as a member
722+
false
743723
else
744-
None
724+
if selector.isGiven then
725+
// Further check that the symbol is a given or implicit and conforms to the bound
726+
sym.isOneOf(Given | Implicit)
727+
&& (selector.bound.isEmpty || sym.info <:< selector.boundTpe)
728+
else
729+
// Normal wildcard, check that the symbol is not a given (but can be implicit)
730+
!sym.is(Given)
745731
end if
746732
end isInImport
747733

748-
private def dealias(symbol: Symbol)(using Context): Symbol =
749-
if(symbol.isType && symbol.asType.denot.isAliasType) then
750-
symbol.asType.typeRef.dealias.typeSymbol
751-
else symbol
752734
/** Annotated with @unused */
753735
private def isUnusedAnnot(using Context): Boolean =
754736
sym.annotations.exists(a => a.symbol == ctx.definitions.UnusedAnnot)
@@ -846,11 +828,43 @@ object CheckUnused:
846828
case _:tpd.Block => Local
847829
case _ => Other
848830

831+
final class ImportSelectorData(
832+
val qualTpe: Type,
833+
val selector: ImportSelector
834+
):
835+
private var myUsed: Boolean = false
836+
837+
def markUsed(): Unit = myUsed = true
838+
839+
def isUsed: Boolean = myUsed
840+
841+
private var myAllSymbols: Set[Symbol] | Null = null
842+
843+
def allSymbolsForNamed(using Context): Set[Symbol] =
844+
if myAllSymbols == null then
845+
val allDenots = qualTpe.member(selector.name).alternatives ::: qualTpe.member(selector.name.toTypeName).alternatives
846+
myAllSymbols = allDenots.map(_.symbol).toSet
847+
myAllSymbols.uncheckedNN
848+
849+
private var myAllSymbolsDealiased: Set[Symbol] | Null = null
850+
851+
def allSymbolsDealiasedForNamed(using Context): Set[Symbol] =
852+
if myAllSymbolsDealiased == null then
853+
myAllSymbolsDealiased = allSymbolsForNamed.map(sym => dealias(sym))
854+
myAllSymbolsDealiased.uncheckedNN
855+
end ImportSelectorData
856+
849857
case class UnusedSymbol(pos: SrcPos, name: Name, warnType: WarnTypes)
850858
/** A container for the results of the used elements analysis */
851859
case class UnusedResult(warnings: Set[UnusedSymbol])
852860
object UnusedResult:
853861
val Empty = UnusedResult(Set.empty)
854862
end UnusedData
855863

864+
private def dealias(symbol: Symbol)(using Context): Symbol =
865+
if symbol.isType && symbol.asType.denot.isAliasType then
866+
symbol.asType.typeRef.dealias.typeSymbol
867+
else
868+
symbol
869+
856870
end CheckUnused

tests/warn/i15503i.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ package foo.test.i16679b:
270270
given x: myPackage.CaseClassName[secondPackage.CoolClass] = null
271271

272272
object secondPackage:
273-
import myPackage.CaseClassName // OK
273+
import myPackage.CaseClassName // warn
274274
import Foo.x
275275
case class CoolClass(i: Int)
276276
println(summon[myPackage.CaseClassName[CoolClass]])
@@ -312,4 +312,4 @@ package foo.test.i17117:
312312

313313
val test = t1.test
314314
}
315-
}
315+
}

0 commit comments

Comments
 (0)