Skip to content

Commit 3cc86de

Browse files
committed
Fix variance loophole for private vars
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.
1 parent 48bb59c commit 3cc86de

File tree

5 files changed

+71
-20
lines changed

5 files changed

+71
-20
lines changed

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

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,17 @@ object SymDenotations {
864864
final def isNullableClassAfterErasure(using Context): Boolean =
865865
isClass && !isValueClass && !is(ModuleClass) && symbol != defn.NothingClass
866866

867+
/** Is `pre` the same as C.this, where C is exactly the owner of this symbol,
868+
* or, if this symbol is protected, a subclass of the owner?
869+
*/
870+
def isCorrectThisType(pre: Type)(using Context): Boolean = pre match
871+
case pre: ThisType =>
872+
(pre.cls eq owner) || this.is(Protected) && pre.cls.derivesFrom(owner)
873+
case pre: TermRef =>
874+
pre.symbol.moduleClass == owner
875+
case _ =>
876+
false
877+
867878
/** Is this definition accessible as a member of tree with type `pre`?
868879
* @param pre The type of the tree from which the selection is made
869880
* @param superAccess Access is via super
@@ -888,18 +899,6 @@ object SymDenotations {
888899
(linked ne NoSymbol) && accessWithin(linked)
889900
}
890901

891-
/** Is `pre` the same as C.thisThis, where C is exactly the owner of this symbol,
892-
* or, if this symbol is protected, a subclass of the owner?
893-
*/
894-
def isCorrectThisType(pre: Type): Boolean = pre match {
895-
case pre: ThisType =>
896-
(pre.cls eq owner) || this.is(Protected) && pre.cls.derivesFrom(owner)
897-
case pre: TermRef =>
898-
pre.symbol.moduleClass == owner
899-
case _ =>
900-
false
901-
}
902-
903902
/** Is protected access to target symbol permitted? */
904903
def isProtectedAccessOK: Boolean =
905904
inline def fail(str: String): false =

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
109109
try op finally noCheckNews = saved
110110
}
111111

112+
/** The set of all private class variables that are assigned
113+
* when selected with a qualifier other than the `this` of the owning class.
114+
* Such variables can contain only invariant type parameters in
115+
* their types.
116+
*/
117+
private var privateVarsSetNonLocally: Set[Symbol] = Set()
118+
112119
def isCheckable(t: New): Boolean = !inJavaAnnot && !noCheckNews.contains(t)
113120

114121
/** Mark parameter accessors that are aliases of like-named parameters
@@ -149,6 +156,8 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
149156
private def processMemberDef(tree: Tree)(using Context): tree.type = {
150157
val sym = tree.symbol
151158
Checking.checkValidOperator(sym)
159+
if sym.isClass then
160+
VarianceChecker.check(tree, privateVarsSetNonLocally)
152161
sym.transformAnnotations(transformAnnot)
153162
sym.defTree = tree
154163
tree
@@ -257,6 +266,14 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
257266
}
258267
}
259268

269+
/** Update privateVarsSetNonLocally is symbol is a private variable
270+
* that is selected from something other than `this` when assigned
271+
*/
272+
private def markVarAccess(tree: Tree, qual: Tree)(using Context): Unit =
273+
val sym = tree.symbol
274+
if sym.is(Private, butNot = Local) && !sym.isCorrectThisType(qual.tpe) then
275+
privateVarsSetNonLocally += sym
276+
260277
def checkNoConstructorProxy(tree: Tree)(using Context): Unit =
261278
if tree.symbol.is(ConstructorProxy) then
262279
report.error(em"constructor proxy ${tree.symbol} cannot be used as a value", tree.srcPos)
@@ -395,7 +412,6 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
395412
registerIfHasMacroAnnotations(tree)
396413
val sym = tree.symbol
397414
if (sym.isClass)
398-
VarianceChecker.check(tree)
399415
annotateExperimental(sym)
400416
checkMacroAnnotation(sym)
401417
if sym.isOneOf(GivenOrImplicit) then
@@ -472,6 +488,9 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
472488
case tpe => tpe
473489
}
474490
)
491+
case Assign(lhs @ Select(qual, _), _) =>
492+
markVarAccess(lhs, qual)
493+
super.transform(tree)
475494
case Typed(Ident(nme.WILDCARD), _) =>
476495
withMode(Mode.Pattern)(super.transform(tree))
477496
// The added mode signals that bounds in a pattern need not

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import printing.Formatting.hl
1919
*/
2020
object VarianceChecker {
2121
case class VarianceError(tvar: Symbol, required: Variance)
22-
def check(tree: tpd.Tree)(using Context): Unit =
23-
VarianceChecker().Traverser.traverse(tree)
22+
def check(tree: tpd.Tree, privateVarsSetNonLocally: collection.Set[Symbol])(using Context): Unit =
23+
VarianceChecker(privateVarsSetNonLocally).Traverser.traverse(tree)
2424

2525
/** Check that variances of type lambda correspond to their occurrences in its body.
2626
* Note: this is achieved by a mechanism separate from checking class type parameters.
@@ -62,7 +62,7 @@ object VarianceChecker {
6262
end checkLambda
6363
}
6464

65-
class VarianceChecker(using Context) {
65+
class VarianceChecker(privateVarsSetNonLocally: collection.Set[Symbol])(using Context) {
6666
import VarianceChecker._
6767
import tpd._
6868

@@ -148,12 +148,19 @@ 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) && !privateVarsSetNonLocally.contains(base)
158+
if base.is(Mutable, butNot = Method) && !isLocal then
159+
variance = 0
154160
try checkInfo(base.info)
155-
finally this.base = saved
156-
}
161+
finally
162+
this.base = savedBase
163+
this.variance = savedVariance
157164
}
158165

159166
private object Traverser extends TreeTraverser {

tests/neg/i18588.check

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
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

tests/neg/i18588.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
trait Animal
17+
object Dog extends Animal
18+
object Cat extends Animal
19+
20+
val dogBox: Box[Dog.type] = new Box(Dog)
21+
val _ = dogBox.put(Cat)
22+
val dog: Dog.type = dogBox.get

0 commit comments

Comments
 (0)