@@ -2,6 +2,7 @@ package dotty.tools.dotc.transform
2
2
3
3
import scala .annotation .tailrec
4
4
5
+ import dotty .tools .uncheckedNN
5
6
import dotty .tools .dotc .ast .tpd
6
7
import dotty .tools .dotc .core .Symbols .*
7
8
import dotty .tools .dotc .ast .tpd .{Inlined , TreeTraverser }
@@ -109,7 +110,7 @@ class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _ke
109
110
ctx
110
111
111
112
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 ))
113
114
114
115
override def prepareForBlock (tree : tpd.Block )(using Context ): Context =
115
116
pushInBlockTemplatePackageDef(tree)
@@ -127,7 +128,7 @@ class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _ke
127
128
if ! tree.symbol.is(Module ) then
128
129
ud.registerDef(tree)
129
130
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 )
131
132
ud.addIgnoredUsage(tree.symbol)
132
133
}
133
134
@@ -356,7 +357,7 @@ object CheckUnused:
356
357
var unusedAggregate : Option [UnusedResult ] = None
357
358
358
359
/* IMPORTS */
359
- private val impInScope = MutStack (MutList [tpd. Import ]())
360
+ private val impInScope = MutStack (MutList [ImportSelectorData ]())
360
361
/**
361
362
* We store the symbol along with their accessibility without import.
362
363
* Accessibility to their definition in outer context/scope
@@ -366,7 +367,7 @@ object CheckUnused:
366
367
private val usedInScope = MutStack (MutSet [(Symbol ,Boolean , Option [Name ], Boolean )]())
367
368
private val usedInPosition = MutMap .empty[Name , MutSet [Symbol ]]
368
369
/* unused import collected during traversal */
369
- private val unusedImport = new java.util. IdentityHashMap [ ImportSelector , Unit ]
370
+ private val unusedImport = MutList .empty[ ImportSelectorData ]
370
371
371
372
/* LOCAL DEF OR VAL / Private Def or Val / Pattern variables */
372
373
private val localDefInScope = MutList .empty[tpd.MemberDef ]
@@ -406,16 +407,17 @@ object CheckUnused:
406
407
* The optional name will be used to target the right import
407
408
* as the same element can be imported with different renaming
408
409
*/
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 =
410
411
if sym.exists && ! isConstructorOfSynth(sym) && ! doNotRegister(sym) then
411
412
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
413
414
else
414
415
val accessibleAsIdent = sym.isAccessibleAsIdent
415
416
def addIfExists (sym : Symbol ): Unit =
416
417
if sym.exists then
417
418
usedDef += sym
418
- usedInScope.top += ((sym, accessibleAsIdent, name, isDerived))
419
+ if ! notForImport then
420
+ usedInScope.top += ((sym, accessibleAsIdent, name, isDerived))
419
421
addIfExists(sym)
420
422
addIfExists(sym.companionModule)
421
423
addIfExists(sym.companionClass)
@@ -436,12 +438,27 @@ object CheckUnused:
436
438
437
439
/** Register an import */
438
440
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)
445
462
end registerImport
446
463
447
464
/** Register (or not) some `val` or `def` according to the context, scope and flags */
@@ -479,40 +496,27 @@ object CheckUnused:
479
496
* - If there are imports in this scope check for unused ones
480
497
*/
481
498
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)
501
507
}
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
516
520
end popScope
517
521
518
522
/**
@@ -531,9 +535,8 @@ object CheckUnused:
531
535
532
536
val sortedImp =
533
537
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 ))
537
540
else
538
541
Nil
539
542
// Partition to extract unset local variables from usedLocalDefs
@@ -694,61 +697,40 @@ object CheckUnused:
694
697
}
695
698
696
699
/** 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 =
698
701
assert(sym.exists)
699
702
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
720
704
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
725
713
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))
735
716
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
743
723
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 )
745
731
end if
746
732
end isInImport
747
733
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
752
734
/** Annotated with @unused */
753
735
private def isUnusedAnnot (using Context ): Boolean =
754
736
sym.annotations.exists(a => a.symbol == ctx.definitions.UnusedAnnot )
@@ -846,11 +828,43 @@ object CheckUnused:
846
828
case _:tpd.Block => Local
847
829
case _ => Other
848
830
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
+
849
857
case class UnusedSymbol (pos : SrcPos , name : Name , warnType : WarnTypes )
850
858
/** A container for the results of the used elements analysis */
851
859
case class UnusedResult (warnings : Set [UnusedSymbol ])
852
860
object UnusedResult :
853
861
val Empty = UnusedResult (Set .empty)
854
862
end UnusedData
855
863
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
+
856
870
end CheckUnused
0 commit comments