Skip to content

Commit 308e3e9

Browse files
committed
Refactor resolveUsage, cache Precedence for correctness
1 parent add05d0 commit 308e3e9

File tree

2 files changed

+138
-59
lines changed

2 files changed

+138
-59
lines changed

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

Lines changed: 89 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -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
386392
end 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

tests/warn/i15503i.scala

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,3 +319,52 @@ package foo.test.i17117:
319319
val test = t1.test
320320
}
321321
}
322+
323+
// manual testing of cached look-ups
324+
package deeply:
325+
object Deep:
326+
def f(): Unit =
327+
def g(): Unit =
328+
def h(): Unit =
329+
println(Deep)
330+
println(Deep)
331+
println(Deep)
332+
h()
333+
g()
334+
override def toString = "man, that is deep!"
335+
/* result cache saves before context traversal and import/member look-up
336+
CHK object Deep
337+
recur * 10 = context depth is 10 between reference and definition
338+
CHK method println
339+
recur = was 19 at predef where max root is 21
340+
CHK object Deep
341+
recur = cached result
342+
CHK method println
343+
recur
344+
CHK object Deep
345+
recur
346+
*/
347+
348+
package constructors:
349+
class C private (i: Int): // warn param
350+
def this() = this(27)
351+
private def this(s: String) = this(s.toInt) // warn ctor
352+
def c = new C(42)
353+
private def f(i: Int) = i // warn overloaded member
354+
private def f(s: String) = s
355+
def g = f("hello") // use one of overloaded member
356+
357+
class D private (i: Int):
358+
private def this() = this(42) // no warn used by companion
359+
def d = i
360+
object D:
361+
def apply(): D = new D()
362+
363+
package reversed: // reverse-engineered
364+
class C:
365+
def c: scala.Int = 42 // Int marked used; lint does not look up <empty>.scala
366+
class D:
367+
def d: Int = 27 // Int is found in root import scala.*
368+
class E:
369+
import scala.* // no warn because root import (which is cached! by previous lookup) is lower precedence
370+
def e: Int = 27

0 commit comments

Comments
 (0)