@@ -2,6 +2,7 @@ package dotty.tools.dotc.transform
22
33import scala .annotation .tailrec
44
5+ import dotty .tools .uncheckedNN
56import dotty .tools .dotc .ast .tpd
67import dotty .tools .dotc .core .Symbols .*
78import 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+
850870end CheckUnused
0 commit comments