Skip to content

Commit c381374

Browse files
committed
Make assign check a context check
At Assign, context receives the tree so that variable lookup detects assignment by target symbol and by position.
1 parent 01ad683 commit c381374

File tree

4 files changed

+54
-38
lines changed

4 files changed

+54
-38
lines changed

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

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -53,19 +53,19 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
5353
override def transformIdent(tree: Ident)(using Context): tree.type =
5454
if tree.symbol.exists then
5555
// if in an inline expansion, resolve at summonInline (synthetic pos) or in an enclosing call site
56-
val resolving =
56+
val resolvingImports =
5757
tree.srcPos.isUserCode(using if tree.hasAttachment(InlinedParameter) then ctx.outer else ctx)
5858
|| tree.srcPos.isZeroExtentSynthetic // take as summonInline
5959
if !ignoreTree(tree) then
6060
def loopOverPrefixes(prefix: Type, depth: Int): Unit =
6161
if depth < 10 && prefix.exists && !prefix.classSymbol.isEffectiveRoot then
62-
resolveUsage(prefix.classSymbol, nme.NO_NAME, NoPrefix, imports = resolving)
62+
resolveUsage(prefix.classSymbol, nme.NO_NAME, NoPrefix, tree.srcPos, resolvingImports)
6363
loopOverPrefixes(prefix.normalizedPrefix, depth + 1)
6464
if tree.srcPos.isZeroExtentSynthetic then
6565
loopOverPrefixes(tree.typeOpt.normalizedPrefix, depth = 0)
66-
resolveUsage(tree.symbol, tree.name, tree.typeOpt.importPrefix.skipPackageObject, imports = resolving)
66+
resolveUsage(tree.symbol, tree.name, tree.typeOpt.importPrefix.skipPackageObject, tree.srcPos, resolvingImports)
6767
else if tree.hasType then
68-
resolveUsage(tree.tpe.classSymbol, tree.name, tree.tpe.importPrefix.skipPackageObject)
68+
resolveUsage(tree.tpe.classSymbol, tree.name, tree.tpe.importPrefix.skipPackageObject, tree.srcPos)
6969
tree
7070

7171
// import x.y; y may be rewritten x.y, also import x.z as y
@@ -86,13 +86,13 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
8686
else if tycon.typeSymbol == defn.TypeableType then args(0) // T in Typeable[T]
8787
else return tree
8888
val target = res.dealias.typeSymbol
89-
resolveUsage(target, target.name, res.importPrefix.skipPackageObject) // case _: T =>
89+
resolveUsage(target, target.name, res.importPrefix.skipPackageObject, tree.srcPos) // case _: T =>
9090
case _ =>
9191
else if isImportable || name.exists(_ != sym.name) then
9292
if !ignoreTree(tree) then
93-
resolveUsage(sym, name, tree.qualifier.tpe)
93+
resolveUsage(sym, name, tree.qualifier.tpe, tree.srcPos)
9494
else if !ignoreTree(tree) then
95-
refUsage(sym)
95+
refUsage(sym, tree.srcPos)
9696
tree
9797

9898
override def transformLiteral(tree: Literal)(using Context): tree.type =
@@ -117,23 +117,21 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
117117
tree match
118118
case Apply(Select(left, nme.Equals | nme.NotEquals), right :: Nil) =>
119119
val caneq = defn.CanEqualClass.typeRef.appliedTo(left.tpe.widen :: right.tpe.widen :: Nil)
120-
resolveScoped(caneq)
120+
resolveScoped(caneq, tree.srcPos)
121121
case tree =>
122-
refUsage(tree.tpe.typeSymbol)
122+
refUsage(tree.tpe.typeSymbol, tree.srcPos)
123123
tree
124124

125125
override def transformTypeApply(tree: TypeApply)(using Context): tree.type =
126126
if tree.symbol.exists && tree.symbol.isConstructor then
127-
refUsage(tree.symbol.owner) // redundant with use of resultType in transformSelect of fun
127+
refUsage(tree.symbol.owner, tree.srcPos) // redundant with use of resultType in transformSelect of fun
128128
tree
129129

130130
override def prepareForAssign(tree: Assign)(using Context): Context =
131131
if tree.lhs.symbol.exists then
132-
refInfos.setAssignmentTarget(tree.lhs.symbol)
133-
ctx
134-
override def transformAssign(tree: Assign)(using Context): tree.type =
135-
refInfos.resetAssignmentTarget()
136-
tree
132+
refInfos.addAssignmentTarget(tree.lhs.symbol)
133+
ctx.fresh.setTree(tree)
134+
else ctx
137135

138136
override def prepareForMatch(tree: Match)(using Context): Context =
139137
// allow case.pat against tree.selector (simple var pat only for now)
@@ -144,13 +142,14 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
144142
override def transformMatch(tree: Match)(using Context): tree.type =
145143
if tree.isInstanceOf[InlineMatch] && tree.selector.isEmpty then
146144
val sf = defn.Compiletime_summonFrom
147-
resolveUsage(sf, sf.name, NoPrefix)
145+
resolveUsage(sf, sf.name, NoPrefix, tree.srcPos)
148146
tree
149147

150148
override def transformTypeTree(tree: TypeTree)(using Context): tree.type =
151149
tree.tpe match
152150
case AnnotatedType(_, annot) => transformAllDeep(annot.tree)
153-
case tpt if !tree.isInferred && tpt.typeSymbol.exists => resolveUsage(tpt.typeSymbol, tpt.typeSymbol.name, NoPrefix)
151+
case tpt if !tree.isInferred && tpt.typeSymbol.exists =>
152+
resolveUsage(tpt.typeSymbol, tpt.typeSymbol.name, NoPrefix, tree.srcPos)
154153
case _ =>
155154
tree
156155

@@ -191,12 +190,14 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
191190
traverseAnnotations(tree.symbol)
192191
if tree.name.startsWith("derived$") && tree.hasType then
193192
def loop(t: Tree): Unit = t match
194-
case Ident(name) => resolveUsage(t.tpe.typeSymbol, name, t.tpe.underlyingPrefix.skipPackageObject)
195-
case Select(t, _) => loop(t)
196-
case _ =>
193+
case Ident(name) =>
194+
resolveUsage(t.tpe.typeSymbol, name, t.tpe.underlyingPrefix.skipPackageObject, tree.srcPos)
195+
case Select(t, _) =>
196+
loop(t)
197+
case _ =>
197198
tree.getAttachment(OriginalTypeClass).foreach(loop)
198199
if tree.symbol.isAllOf(DeferredGivenFlags) then
199-
resolveUsage(defn.Compiletime_deferred, nme.NO_NAME, NoPrefix)
200+
resolveUsage(defn.Compiletime_deferred, nme.NO_NAME, NoPrefix, tree.srcPos)
200201
tree
201202

202203
override def prepareForDefDef(tree: DefDef)(using Context): Context =
@@ -216,7 +217,7 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
216217
if tree.symbol.is(Inline) then
217218
refInfos.inliners -= 1
218219
if tree.symbol.isAllOf(DeferredGivenFlags) then
219-
resolveUsage(defn.Compiletime_deferred, nme.NO_NAME, NoPrefix)
220+
resolveUsage(defn.Compiletime_deferred, nme.NO_NAME, NoPrefix, tree.srcPos)
220221
tree
221222

222223
override def transformTypeDef(tree: TypeDef)(using Context): tree.type =
@@ -282,7 +283,7 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
282283
def resolve(tpe: Type): Unit =
283284
val sym = tpe.typeSymbol
284285
if sym.exists then
285-
resolveUsage(sym, sym.name, NoPrefix)
286+
resolveUsage(sym, sym.name, NoPrefix, tree.srcPos)
286287
resolve(lo)
287288
resolve(hi)
288289
case _ =>
@@ -314,8 +315,8 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
314315
*
315316
* The LHS of a current Assign is never recorded as a reference (that is, a usage).
316317
*/
317-
def refUsage(sym: Symbol)(using Context): Unit =
318-
if !refInfos.hasRef(sym) && !refInfos.isAssignmentTarget(sym) then
318+
def refUsage(sym: Symbol, pos: SrcPos)(using Context): Unit =
319+
if !refInfos.hasRef(sym) then
319320
val isCase = sym.is(Case) && sym.isClass
320321
if !ctx.outersIterator.exists: outer =>
321322
val owner = outer.owner
@@ -324,6 +325,9 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
324325
&& owner.exists
325326
&& owner.is(Synthetic)
326327
&& owner.owner.eq(sym.companionModule.moduleClass)
328+
|| outer.tree.match
329+
case Assign(lhs, _) => lhs.symbol.eq(sym) && outer.tree.srcPos.sourcePos.contains(pos.sourcePos)
330+
case _ => false
327331
then
328332
refInfos.addRef(sym)
329333

@@ -338,7 +342,7 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
338342
* The `imports` flag is whether an identifier can mark an import as used: the flag is false
339343
* for inlined code, except for `summonInline` (and related constructs) which are resolved at inlining.
340344
*/
341-
def resolveUsage(sym0: Symbol, name: Name, prefix: Type, imports: Boolean = true)(using Context): Unit =
345+
def resolveUsage(sym0: Symbol, name: Name, prefix: Type, pos: SrcPos, imports: Boolean = true)(using Context): Unit =
342346
import PrecedenceLevels.*
343347
val sym = sym0.userSymbol
344348

@@ -441,15 +445,15 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
441445
end while
442446
// record usage and possibly an import
443447
if !enclosed then
444-
refUsage(sym)
448+
refUsage(sym, pos)
445449
if imports && candidate != NoContext && candidate.isImportContext && importer != null then
446450
refInfos.sels.put(importer, ())
447451
end resolveUsage
448452

449453
/** Simulate implicit search for contextual implicits in lexical scope and mark any definitions or imports as used.
450454
* Avoid cached ctx.implicits because it needs the precise import context that introduces the given.
451455
*/
452-
def resolveScoped(tp: Type)(using Context): Unit =
456+
def resolveScoped(tp: Type, pos: SrcPos)(using Context): Unit =
453457
var done = false
454458
val ctxs = ctx.outersIterator
455459
while !done && ctxs.hasNext do
@@ -461,15 +465,15 @@ class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPha
461465
else Nil
462466
implicitRefs.find(ref => ref.underlyingRef.widen <:< tp) match
463467
case Some(found: TermRef) =>
464-
refUsage(found.denot.symbol)
468+
refUsage(found.denot.symbol, pos)
465469
if cur.isImportContext then
466470
cur.importInfo.nn.selectors.find(sel => sel.isGiven || sel.rename == found.name) match
467471
case Some(sel) =>
468472
refInfos.sels.put(sel, ())
469473
case _ =>
470474
return
471475
case Some(found: RenamedImplicitRef) if cur.isImportContext =>
472-
refUsage(found.underlyingRef.denot.symbol)
476+
refUsage(found.underlyingRef.denot.symbol, pos)
473477
cur.importInfo.nn.selectors.find(sel => sel.rename == found.implicitName) match
474478
case Some(sel) =>
475479
refInfos.sels.put(sel, ())
@@ -555,14 +559,8 @@ object CheckUnused:
555559

556560
var inliners = 0 // depth of inline def (not inlined yet)
557561

558-
private var assignmentTarget: Symbol = NoSymbol
559-
def isAssignmentTarget(sym: Symbol): Boolean = sym eq assignmentTarget
560-
def resetAssignmentTarget(): Unit =
561-
assignmentTarget = NoSymbol
562-
def setAssignmentTarget(sym: Symbol): Unit =
563-
assignmentTarget = sym
562+
def addAssignmentTarget(sym: Symbol): Unit =
564563
asss.addOne(sym)
565-
// @pre !isAssignmentTarget(sym), see refUsage
566564
def addRef(sym: Symbol): Unit =
567565
refs.addOne(sym)
568566
def hasRef(sym: Symbol): Boolean =

tests/warn/i23704.check

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
16 | private var myvar: Int = 0 // warn for the same case with simpler syntax
77
| ^^^^^
88
| private variable was mutated but not read
9+
-- [E198] Unused Symbol Warning: tests/warn/i23704.scala:22:14 ---------------------------------------------------------
10+
22 | private var myvar: Int = 0 // warn (because read is in RHS of assignment; see incr)
11+
| ^^^^^
12+
| private variable was mutated but not read
913
-- [E198] Unused Symbol Warning: tests/warn/i23704.scala:26:8 ----------------------------------------------------------
1014
26 | var localvar = 0 // warn local variable was mutated but not read
1115
| ^^^^^^^^

tests/warn/i23704.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class C:
1919
27
2020

2121
class D:
22-
private var myvar: Int = 0 // nowarn (although read is RHS of assignment)
22+
private var myvar: Int = 0 // warn (because read is in RHS of assignment; see incr)
2323
def incr(): Unit = myvar = myvar + 1
2424

2525
def local(): Unit =

tests/warn/i24280.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,18 @@ class Foo {
1515
private var i = 0 // warn
1616
class Select:
1717
def test = Select.i += 1
18+
19+
def nested(): Any =
20+
var i = 0 // warn, read of i is in RHS of assign to i
21+
i =
22+
var j = 0 // nowarn, j is assigned to and read
23+
j = i + 1
24+
j
25+
26+
def escaped(): Any =
27+
var i = 0 // warn, read of i is in RHS of assign to i, but should nowarn because assigned to j
28+
var j = 0 // nowarn, j is assigned to and read
29+
i =
30+
j = i + 1
31+
j
1832
}

0 commit comments

Comments
 (0)