Skip to content

Commit 89fa247

Browse files
authored
Fix variance loophole for private vars (#18693)
In Scala 2 a setter was created at Typer for private, non-local vars. Variance checking then worked on the setter. But in Scala 3, the setter is only created later, which caused a loophole for variance checking. This PR does actually better than Scala 2 in the following sense: A private variable counts as an invariant occurrence only if it is assigned with a selector different from `this`. Or conversely, a variable containing a covariant type parameter in its type can be read from different objects, but all assignments must be via this. The motivation is that such variables effectively behave like vals for the purposes of variance checking.
2 parents 69e1338 + c2f0db4 commit 89fa247

File tree

7 files changed

+100
-23
lines changed

7 files changed

+100
-23
lines changed

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,7 @@ class Definitions {
993993
// Annotation classes
994994
@tu lazy val AllowConversionsAnnot: ClassSymbol = requiredClass("scala.annotation.allowConversions")
995995
@tu lazy val AnnotationDefaultAnnot: ClassSymbol = requiredClass("scala.annotation.internal.AnnotationDefault")
996+
@tu lazy val AssignedNonLocallyAnnot: ClassSymbol = requiredClass("scala.annotation.internal.AssignedNonLocally")
996997
@tu lazy val BeanPropertyAnnot: ClassSymbol = requiredClass("scala.beans.BeanProperty")
997998
@tu lazy val BooleanBeanPropertyAnnot: ClassSymbol = requiredClass("scala.beans.BooleanBeanProperty")
998999
@tu lazy val BodyAnnot: ClassSymbol = requiredClass("scala.annotation.internal.Body")

compiler/src/dotty/tools/dotc/core/SymDenotations.scala

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,17 @@ object SymDenotations {
868868
final def isNullableClassAfterErasure(using Context): Boolean =
869869
isClass && !isValueClass && !is(ModuleClass) && symbol != defn.NothingClass
870870

871+
/** Is `pre` the same as C.this, where C is exactly the owner of this symbol,
872+
* or, if this symbol is protected, a subclass of the owner?
873+
*/
874+
def isAccessPrivilegedThisType(pre: Type)(using Context): Boolean = pre match
875+
case pre: ThisType =>
876+
(pre.cls eq owner) || this.is(Protected) && pre.cls.derivesFrom(owner)
877+
case pre: TermRef =>
878+
pre.symbol.moduleClass == owner
879+
case _ =>
880+
false
881+
871882
/** Is this definition accessible as a member of tree with type `pre`?
872883
* @param pre The type of the tree from which the selection is made
873884
* @param superAccess Access is via super
@@ -892,18 +903,6 @@ object SymDenotations {
892903
(linked ne NoSymbol) && accessWithin(linked)
893904
}
894905

895-
/** Is `pre` the same as C.thisThis, where C is exactly the owner of this symbol,
896-
* or, if this symbol is protected, a subclass of the owner?
897-
*/
898-
def isCorrectThisType(pre: Type): Boolean = pre match {
899-
case pre: ThisType =>
900-
(pre.cls eq owner) || this.is(Protected) && pre.cls.derivesFrom(owner)
901-
case pre: TermRef =>
902-
pre.symbol.moduleClass == owner
903-
case _ =>
904-
false
905-
}
906-
907906
/** Is protected access to target symbol permitted? */
908907
def isProtectedAccessOK: Boolean =
909908
inline def fail(str: String): false =
@@ -937,7 +936,7 @@ object SymDenotations {
937936
|| boundary.isRoot
938937
|| (accessWithin(boundary) || accessWithinLinked(boundary)) &&
939938
( !this.is(Local)
940-
|| isCorrectThisType(pre)
939+
|| isAccessPrivilegedThisType(pre)
941940
|| canBeLocal(name, flags)
942941
&& {
943942
resetFlag(Local)

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,6 +1103,18 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
11031103
// allow assignments from the primary constructor to class fields
11041104
ctx.owner.name.is(TraitSetterName) || ctx.owner.isStaticConstructor
11051105

1106+
/** Mark private variables that are assigned with a prefix other than
1107+
* the `this` type of their owner with a `annotation.internal.AssignedNonLocally`
1108+
* annotation. The annotation influences the variance check for these
1109+
* variables, which is done at PostTyper. It will be removed after the
1110+
* variance check.
1111+
*/
1112+
def rememberNonLocalAssignToPrivate(sym: Symbol) = lhs1 match
1113+
case Select(qual, _)
1114+
if sym.is(Private, butNot = Local) && !sym.isAccessPrivilegedThisType(qual.tpe) =>
1115+
sym.addAnnotation(Annotation(defn.AssignedNonLocallyAnnot, lhs1.span))
1116+
case _ =>
1117+
11061118
lhsCore match
11071119
case Apply(fn, _) if fn.symbol.is(ExtensionMethod) =>
11081120
def toSetter(fn: Tree): untpd.Tree = fn match
@@ -1136,15 +1148,16 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
11361148
case _ => lhsCore.tpe match {
11371149
case ref: TermRef =>
11381150
val lhsVal = lhsCore.denot.suchThat(!_.is(Method))
1139-
if (canAssign(lhsVal.symbol)) {
1140-
// lhsBounds: (T .. Any) as seen from lhs prefix, where T is the type of lhsVal.symbol
1151+
val lhsSym = lhsVal.symbol
1152+
if canAssign(lhsSym) then
1153+
rememberNonLocalAssignToPrivate(lhsSym)
1154+
// lhsBounds: (T .. Any) as seen from lhs prefix, where T is the type of lhsSym
11411155
// This ensures we do the as-seen-from on T with variance -1. Test case neg/i2928.scala
11421156
val lhsBounds =
1143-
TypeBounds.lower(lhsVal.symbol.info).asSeenFrom(ref.prefix, lhsVal.symbol.owner)
1157+
TypeBounds.lower(lhsSym.info).asSeenFrom(ref.prefix, lhsSym.owner)
11441158
assignType(cpy.Assign(tree)(lhs1, typed(tree.rhs, lhsBounds.loBound)))
11451159
.computeAssignNullable()
1146-
}
1147-
else {
1160+
else
11481161
val pre = ref.prefix
11491162
val setterName = ref.name.setterName
11501163
val setter = pre.member(setterName)
@@ -1157,7 +1170,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
11571170
case _ =>
11581171
reassignmentToVal
11591172
}
1160-
}
11611173
case TryDynamicCallType =>
11621174
typedDynamicAssign(tree, pt)
11631175
case tpe =>

compiler/src/dotty/tools/dotc/typer/VarianceChecker.scala

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,20 @@ class VarianceChecker(using Context) {
148148
case _ =>
149149
apply(None, info)
150150

151-
def validateDefinition(base: Symbol): Option[VarianceError] = {
152-
val saved = this.base
151+
def validateDefinition(base: Symbol): Option[VarianceError] =
152+
val savedBase = this.base
153153
this.base = base
154+
val savedVariance = variance
155+
def isLocal =
156+
base.isAllOf(PrivateLocal)
157+
|| base.is(Private) && !base.hasAnnotation(defn.AssignedNonLocallyAnnot)
158+
if base.is(Mutable, butNot = Method) && !isLocal then
159+
base.removeAnnotation(defn.AssignedNonLocallyAnnot)
160+
variance = 0
154161
try checkInfo(base.info)
155-
finally this.base = saved
156-
}
162+
finally
163+
this.base = savedBase
164+
this.variance = savedVariance
157165
}
158166

159167
private object Traverser extends TreeTraverser {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package scala.annotation
2+
package internal
3+
4+
/** An annotation to indicate that a private `var` was assigned with a prefix
5+
* other than the `this` type of its owner.
6+
*/
7+
class AssignedNonLocally() extends Annotation

tests/neg/i18588.check

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Error: tests/neg/i18588.scala:7:14 ----------------------------------------------------------------------------------
2+
7 | private var cached: A = value // error
3+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4+
| covariant type A occurs in invariant position in type A of variable cached
5+
-- Error: tests/neg/i18588.scala:17:14 ---------------------------------------------------------------------------------
6+
17 | private var cached: A = value // error
7+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8+
| covariant type A occurs in invariant position in type A of variable cached

tests/neg/i18588.scala

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
class ROBox[+A](value: A) {
2+
private var cached: A = value
3+
def get: A = ROBox[A](value).cached
4+
}
5+
6+
class Box[+A](value: A) {
7+
private var cached: A = value // error
8+
def get: A = cached
9+
10+
def put[AA >: A](value: AA): Unit = {
11+
val box: Box[AA] = this
12+
box.cached = value
13+
}
14+
}
15+
16+
class BoxWithCompanion[+A](value: A) {
17+
private var cached: A = value // error
18+
def get: A = cached
19+
}
20+
21+
class BoxValid[+A](value: A, orig: A) {
22+
private var cached: A = value // ok
23+
def get: A = cached
24+
25+
def reset(): Unit =
26+
cached = orig // ok: mutated through this prefix
27+
}
28+
29+
trait Animal
30+
object Dog extends Animal
31+
object Cat extends Animal
32+
33+
val dogBox: Box[Dog.type] = new Box(Dog)
34+
val _ = dogBox.put(Cat)
35+
val dog: Dog.type = dogBox.get
36+
37+
38+
object BoxWithCompanion {
39+
def put[A](box: BoxWithCompanion[A], value: A): Unit = {
40+
box.cached = value
41+
}
42+
}

0 commit comments

Comments
 (0)