@@ -63,13 +63,13 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
6363 resolveUsage(tree.tpe.classSymbol, tree.name, tree.tpe.importPrefix.skipPackageObject)
6464 tree
6565
66- // import x.y; x may be rewritten x.y, also import x.z as y
66+ // import x.y; y may be rewritten x.y, also import x.z as y
6767 override def transformSelect (tree : Select )(using Context ): tree.type =
6868 val name = tree.removeAttachment(OriginalName ).getOrElse(nme.NO_NAME )
6969 if tree.span.isSynthetic && tree.symbol == defn.TypeTest_unapply then
7070 tree.qualifier.tpe.underlying.finalResultType match
71- case AppliedType (_, args) => // if tycon.typeSymbol == defn.TypeTestClass
72- val res = args(1 )
71+ case AppliedType (_, args) => // tycon.typeSymbol == defn.TypeTestClass
72+ val res = args(1 ) // T in TypeTest[-S, T]
7373 val target = res.dealias.typeSymbol
7474 resolveUsage(target, target.name, res.importPrefix.skipPackageObject) // case _: T =>
7575 case _ =>
@@ -156,7 +156,7 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
156156 case Select (t, _) => loop(t)
157157 case _ =>
158158 tree.getAttachment(OriginalTypeClass ).foreach(loop)
159- if tree.symbol.isAllOf(Deferred | Given | HasDefault ) then
159+ if tree.symbol.isAllOf(DeferredGivenFlags ) then
160160 resolveUsage(defn.Compiletime_deferred , nme.NO_NAME , NoPrefix )
161161 tree
162162
@@ -173,7 +173,7 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
173173 traverseAnnotations(tree.symbol)
174174 if tree.symbol.is(Inline ) then
175175 refInfos.inliners -= 1
176- if tree.symbol.isAllOf(Deferred | Given | HasDefault ) then
176+ if tree.symbol.isAllOf(DeferredGivenFlags ) then
177177 resolveUsage(defn.Compiletime_deferred , nme.NO_NAME , NoPrefix )
178178 tree
179179
@@ -260,30 +260,37 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
260260 refInfos.refs.addOne(sym)
261261
262262 /** Look up a reference in enclosing contexts to determine whether it was introduced by a definition or import.
263- * The binding of highest precedence must be correct.
263+ * The binding of highest precedence must then be correct.
264+ *
265+ * Unqualified locals and fully qualified globals are neither imported nor in scope;
266+ * e.g., in `scala.Int`, `scala` is in scope for typer, but here we reverse-engineer the attribution.
267+ * For Select, lint does not look up `<empty>.scala` (so top-level syms look like magic) but records `scala.Int`.
268+ * For Ident, look-up finds the root import as usual. A competing import is OK because higher precedence.
264269 */
265270 def resolveUsage (sym : Symbol , name : Name , prefix : Type )(using Context ): Unit =
271+ import PrecedenceLevels .*
272+
266273 def matchingSelector (info : ImportInfo ): ImportSelector | Null =
267274 val qtpe = info.site
268- def loop (sels : List [ImportSelector ]): ImportSelector | Null =
269- sels match
270- case sel :: sels if sel.isWildcard =>
271- // the qualifier must have the target symbol as a member
272- val matches = qtpe.member(sym.name).hasAltWith(_.symbol == sym)
273- && {
274- if sel.isGiven then // Further check that the symbol is a given or implicit and conforms to the bound
275- sym.isOneOf(Given | Implicit )
275+ def hasAltMember (nm : Name ) = qtpe.member(nm).hasAltWith(_.symbol == sym)
276+ def loop (sels : List [ImportSelector ]): ImportSelector | Null = sels match
277+ case sel :: sels =>
278+ val matches =
279+ if sel.isWildcard then
280+ // the qualifier must have the target symbol as a member
281+ hasAltMember(sym.name) && {
282+ if sel.isGiven then // Further check that the symbol is a given or implicit and conforms to the bound
283+ sym.isOneOf(GivenOrImplicit )
276284 && (sel.bound.isEmpty || sym.info.finalResultType <:< sel.boundTpe)
277285 && (prefix.eq(NoPrefix ) || qtpe =:= prefix)
278- else
279- ! sym.is(Given ) // Normal wildcard, check that the symbol is not a given (but can be implicit)
280- }
281- if matches then sel else loop(sels)
282- case sel :: sels =>
283- def hasAltMember (nm : Name ) = qtpe.member(nm).hasAltWith(_.symbol == sym)
284- // if there is an explicit name, it must match
285- val matches = ! name.exists(_.toTermName != sel.rename) &&
286- (prefix.eq(NoPrefix ) || qtpe =:= prefix) && (hasAltMember(sel.name) || hasAltMember(sel.name.toTypeName))
286+ else
287+ ! sym.is(Given ) // Normal wildcard, check that the symbol is not a given (but can be implicit)
288+ }
289+ else
290+ // if there is an explicit name, it must match
291+ ! name.exists(_.toTermName != sel.rename)
292+ && (prefix.eq(NoPrefix ) || qtpe =:= prefix)
293+ && (hasAltMember(sel.name) || hasAltMember(sel.name.toTypeName))
287294 if matches then sel else loop(sels)
288295 case nil => null
289296 loop(info.selectors)
@@ -293,28 +300,17 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
293300 && ctxsym.thisType.baseClasses.contains(sym.owner)
294301 && ctxsym.thisType.member(sym.name).hasAltWith(d => d.containsSym(sym) && ! name.exists(_ != d.name))
295302
296- def addCached (where : Context ): Unit =
303+ // Attempt to cache a result at the given context. Not all contexts bear a cache, including NoContext.
304+ // If there is already any result for the name and prefix, do nothing.
305+ def addCached (where : Context , result : Precedence ): Unit =
297306 if where.moreProperties ne null then
298307 where.property(resolvedKey) match
299- case Some (res) =>
300- val np = (name, prefix)
301- res.seen.updateWith(sym):
302- case svs @ Some (vs) => if vs.exists((n, p) => n == name && p =:= prefix) then svs else Some (np :: vs)
303- case _ => Some (np :: Nil )
304- case _ =>
305-
306- if ! sym.exists then return
307-
308- // Names are resolved by definitions and imports, which have four precedence levels:
309- object PrecedenceLevels :
310- opaque type Precedence = Int
311- inline def NoPrecedence : Precedence = 5
312- inline def OtherUnit : Precedence = 4 // root import or def from another compilation unit via enclosing package
313- inline def Wildcard : Precedence = 3 // wildcard import
314- inline def NamedImport : Precedence = 2 // specific import
315- inline def Definition : Precedence = 1 // def from this compilation unit
316- extension (p : Precedence ) inline def weakerThan (q : Precedence ) = p > q
317- import PrecedenceLevels .*
308+ case Some (resolved) =>
309+ resolved.record(sym, name, prefix, result)
310+ case none =>
311+
312+ // Avoid spurious NoSymbol and also primary ctors which are never warned about.
313+ if ! sym.exists || sym.isPrimaryConstructor then return
318314
319315 // Find the innermost, highest precedence. Contexts have no nesting levels but assume correctness.
320316 // If the sym is an enclosing definition (the owner of a context), it does not count toward usages.
@@ -329,21 +325,28 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
329325 while ! done && ctxs.hasNext do
330326 val cur = ctxs.next()
331327 if cur.owner eq sym then
328+ addCached(cachePoint, Definition )
332329 return // found enclosing definition
333330 else if isLocal then
334331 if cur.owner eq sym.owner then
335332 done = true // for local def, just checking that it is not enclosing
336333 else
337- cached =
334+ val cachedPrecedence =
338335 cur.property(resolvedKey) match
339- case Some (res ) =>
336+ case Some (resolved ) =>
340337 // conservative, cache must be nested below the result context
341- if precedence == NoPrecedence then
342- cachePoint = cur
343- res.saw(sym, name, prefix)
344- case _ => false
338+ if precedence.isNone then
339+ cachePoint = cur // no result yet, and future result could be cached here
340+ resolved.hasRecord(sym, name, prefix)
341+ case none => NoPrecedence
342+ cached = ! cachedPrecedence.isNone
345343 if cached then
346- candidate = cur
344+ // if prefer cached precedence, then discard previous result
345+ if precedence.weakerThan(cachedPrecedence) then
346+ candidate = NoContext
347+ importer = null
348+ cachePoint = cur // actual cache context
349+ precedence = cachedPrecedence // actual cached precedence
347350 done = true
348351 else if cur.isImportContext then
349352 val sel = matchingSelector(cur.importInfo.nn)
@@ -368,7 +371,7 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
368371 if sym.srcPos.sourcePos.source == ctx.source then
369372 precedence = Definition
370373 candidate = cur
371- importer = null
374+ importer = null // ignore import in same scope; we can't check nesting level
372375 done = true
373376 else if precedence.weakerThan(OtherUnit ) then
374377 precedence = OtherUnit
@@ -378,10 +381,13 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
378381 refInfos.refs.addOne(sym)
379382 if candidate != NoContext && candidate.isImportContext && importer != null then
380383 refInfos.sels.put(importer, ())
381- if ! cached then
382- addCached(cachePoint)
384+ // possibly record that we have performed this look-up
385+ // if no result was found, take it as Definition (local or rooted head of fully qualified path)
386+ val adjusted = if precedence.isNone then Definition else precedence
387+ if ! cached && (cachePoint ne NoContext ) then
388+ addCached(cachePoint, adjusted)
383389 if cachePoint ne ctx then
384- addCached(ctx)
390+ addCached(ctx, adjusted) // at this ctx, since cachePoint may be far up the outer chain
385391 end resolveUsage
386392end CheckUnused
387393
@@ -450,11 +456,35 @@ object CheckUnused:
450456 var inliners = 0 // depth of inline def (not inlined yet)
451457 end RefInfos
452458
453- // Symbols already resolved in the given Context (with name and prefix of lookup)
459+ // Symbols already resolved in the given Context (with name and prefix of lookup).
454460 class Resolved :
455- val seen = mutable.Map .empty[Symbol , List [(Name , Type )]].withDefaultValue(Nil )
456- def saw (sym : Symbol , name : Name , pre : Type )(using Context ): Boolean =
457- seen(sym).exists((n, p) => n == name && p =:= pre)
461+ import PrecedenceLevels .*
462+ private val seen = mutable.Map .empty[Symbol , List [(Name , Type , Precedence )]].withDefaultValue(Nil )
463+ // if a result has been recorded, return it; otherwise, NoPrecedence.
464+ def hasRecord (symbol : Symbol , name : Name , prefix : Type )(using Context ): Precedence =
465+ seen(symbol).find((n, p, _) => n == name && p =:= prefix) match
466+ case Some ((_, _, r)) => r
467+ case none => NoPrecedence
468+ // "record" the look-up result, if there is not already a result for the name and prefix.
469+ def record (symbol : Symbol , name : Name , prefix : Type , result : Precedence )(using Context ): Unit =
470+ require(NoPrecedence .weakerThan(result))
471+ seen.updateWith(symbol):
472+ case svs @ Some (vs) =>
473+ if vs.exists((n, p, _) => n == name && p =:= prefix) then svs
474+ else Some ((name, prefix, result) :: vs)
475+ case none => Some ((name, prefix, result) :: Nil )
476+
477+ // Names are resolved by definitions and imports, which have four precedence levels:
478+ object PrecedenceLevels :
479+ opaque type Precedence = Int
480+ inline def NoPrecedence : Precedence = 5
481+ inline def OtherUnit : Precedence = 4 // root import or def from another compilation unit via enclosing package
482+ inline def Wildcard : Precedence = 3 // wildcard import
483+ inline def NamedImport : Precedence = 2 // specific import
484+ inline def Definition : Precedence = 1 // def from this compilation unit
485+ extension (p : Precedence )
486+ inline def weakerThan (q : Precedence ): Boolean = p > q
487+ inline def isNone : Boolean = p == NoPrecedence
458488
459489 def reportUnused ()(using Context ): Unit =
460490 for (msg, pos, origin) <- warnings do
@@ -479,7 +509,7 @@ object CheckUnused:
479509
480510 def checkPrivate (sym : Symbol , pos : SrcPos ) =
481511 if ctx.settings.WunusedHas .privates
482- && ! sym.isConstructor
512+ && ! sym.isPrimaryConstructor
483513 && sym.is(Private , butNot = SelfName | Synthetic | CaseAccessor )
484514 && ! sym.isSerializationSupport
485515 && ! (sym.is(Mutable ) && sym.isSetter && sym.owner.is(Trait )) // tracks sym.underlyingSymbol sibling getter
0 commit comments