Skip to content

Commit 975df4a

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, including 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). Regardless, it is already quite a bit faster than before, thanks to smaller constant factors.
1 parent 0c1f090 commit 975df4a

File tree

2 files changed

+112
-92
lines changed

2 files changed

+112
-92
lines changed

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

+110-90
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,8 +110,8 @@ class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _ke
109110
ctx
110111

111112
override def prepareForSelect(tree: tpd.Select)(using Context): Context =
112-
val name = tree.removeAttachment(OriginalName).orElse(Some(tree.name))
113-
unusedDataApply(_.registerUsed(tree.symbol, name))
113+
val name = tree.removeAttachment(OriginalName)
114+
unusedDataApply(_.registerUsed(tree.symbol, name, includeForImport = tree.qualifier.span.isSynthetic))
114115

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

@@ -359,7 +360,7 @@ object CheckUnused:
359360
var unusedAggregate: Option[UnusedResult] = None
360361

361362
/* IMPORTS */
362-
private val impInScope = MutStack(MutList[tpd.Import]())
363+
private val impInScope = MutStack(MutList[ImportSelectorData]())
363364
/**
364365
* We store the symbol along with their accessibility without import.
365366
* Accessibility to their definition in outer context/scope
@@ -369,7 +370,7 @@ object CheckUnused:
369370
private val usedInScope = MutStack(MutSet[(Symbol,Boolean, Option[Name], Boolean)]())
370371
private val usedInPosition = MutMap.empty[Name, MutSet[Symbol]]
371372
/* unused import collected during traversal */
372-
private val unusedImport = new java.util.IdentityHashMap[ImportSelector, Unit]
373+
private val unusedImport = MutList.empty[ImportSelectorData]
373374

374375
/* LOCAL DEF OR VAL / Private Def or Val / Pattern variables */
375376
private val localDefInScope = MutList.empty[tpd.MemberDef]
@@ -409,16 +410,17 @@ object CheckUnused:
409410
* The optional name will be used to target the right import
410411
* as the same element can be imported with different renaming
411412
*/
412-
def registerUsed(sym: Symbol, name: Option[Name], isDerived: Boolean = false)(using Context): Unit =
413+
def registerUsed(sym: Symbol, name: Option[Name], includeForImport: Boolean = true, isDerived: Boolean = false)(using Context): Unit =
413414
if sym.exists && !isConstructorOfSynth(sym) && !doNotRegister(sym) then
414415
if sym.isConstructor then
415-
registerUsed(sym.owner, None) // constructor are "implicitly" imported with the class
416+
registerUsed(sym.owner, None, includeForImport) // constructor are "implicitly" imported with the class
416417
else
417418
val accessibleAsIdent = sym.isAccessibleAsIdent
418419
def addIfExists(sym: Symbol): Unit =
419420
if sym.exists then
420421
usedDef += sym
421-
usedInScope.top += ((sym, accessibleAsIdent, name, isDerived))
422+
if includeForImport then
423+
usedInScope.top += ((sym, accessibleAsIdent, name, isDerived))
422424
addIfExists(sym)
423425
addIfExists(sym.companionModule)
424426
addIfExists(sym.companionClass)
@@ -439,12 +441,27 @@ object CheckUnused:
439441

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

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

521525
/**
@@ -534,9 +538,8 @@ object CheckUnused:
534538

535539
val sortedImp =
536540
if ctx.settings.WunusedHas.imports || ctx.settings.WunusedHas.strictNoImplicitWarn then
537-
import scala.jdk.CollectionConverters.*
538-
unusedImport.keySet().nn.iterator().nn.asScala
539-
.map(d => UnusedSymbol(d.srcPos, d.name, WarnTypes.Imports)).toList
541+
unusedImport.toList
542+
.map(d => UnusedSymbol(d.selector.srcPos, d.selector.name, WarnTypes.Imports))
540543
else
541544
Nil
542545
// Partition to extract unset local variables from usedLocalDefs
@@ -697,52 +700,40 @@ object CheckUnused:
697700
}
698701

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

703-
val tpd.Import(qual, sels) = imp
704-
val qualTpe = qual.tpe
705-
val dealiasedSym = sym.dealias
706-
707-
val selectionsToDealias: List[SingleDenotation] =
708-
val typeSelections = sels.flatMap(n => qualTpe.member(n.name.toTypeName).alternatives)
709-
val termSelections = sels.flatMap(n => qualTpe.member(n.name.toTermName).alternatives)
710-
typeSelections ::: termSelections
711-
712-
val qualHasSymbol: Boolean =
713-
val simpleSelections = qualTpe.member(sym.name).alternatives
714-
simpleSelections.exists(d => d.symbol == sym || d.symbol.dealias == dealiasedSym)
715-
|| selectionsToDealias.exists(d => d.symbol.dealias == dealiasedSym)
706+
val selector = selData.selector
716707

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

741-
private def dealias(using Context): Symbol =
742-
if sym.isType && sym.asType.denot.isAliasType then
743-
sym.asType.typeRef.dealias.typeSymbol
744-
else sym
745-
746737
/** Annotated with @unused */
747738
private def isUnusedAnnot(using Context): Boolean =
748739
sym.annotations.exists(a => a.symbol == ctx.definitions.UnusedAnnot)
@@ -840,11 +831,40 @@ object CheckUnused:
840831
case _:tpd.Block => Local
841832
case _ => Other
842833

834+
final class ImportSelectorData(val qualTpe: Type, val selector: ImportSelector):
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+
843857
case class UnusedSymbol(pos: SrcPos, name: Name, warnType: WarnTypes)
844858
/** A container for the results of the used elements analysis */
845859
case class UnusedResult(warnings: Set[UnusedSymbol])
846860
object UnusedResult:
847861
val Empty = UnusedResult(Set.empty)
848862
end UnusedData
849863

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+
850870
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)