Skip to content

Commit 8d723a9

Browse files
authored
Disallow overriding val parameters (#16096)
We disallow overriding of val parameters, which fixes the soundness problem discovered in #16092. There is one exception: If a val parameter is overridden by another val parameter that can be shown to always have the same value (in the sense established by Paramforwarding.inheritedAccessor). This exception is needed to make a not-so-uncommon pattern of case class inheritance go through. Example: abstract class A(val x: Int) case class B(override val x: Int) extends A(x) case class C(override val x: Int) extends A(x) case object D extends A(0) Here, the `override val`s are necessary since case class parameters are always vals, so they do override the val in class A. It should be noted that the override val generates a second field, so this not a very efficient representation. A better design would be to use an abstract field in `A`: abstract class A { val x: Int } case class B(val x: Int) extends A case class C(val x: Int) extends A case object D extends A { val a = 0 } But that causes slightly more work for cases as in D. Which seems to be why the first pattern is sometimes used. It might be desirable to disallow the first pattern, but that would cause quite a bit of migration hassle since it requires synchronized changes at several places of a class hierarchy. Fixes #16092
2 parents 9d574ce + fb75d96 commit 8d723a9

File tree

19 files changed

+138
-83
lines changed

19 files changed

+138
-83
lines changed

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

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ import NameKinds.ParamAccessorName
3030
* The aim of this transformation is to avoid redundant parameter accessor fields.
3131
*/
3232
class ParamForwarding extends MiniPhase with IdentityDenotTransformer:
33-
import ast.tpd._
33+
import ast.tpd.*
34+
import ParamForwarding.inheritedAccessor
3435

3536
private def thisPhase: ParamForwarding = this
3637

@@ -39,20 +40,6 @@ class ParamForwarding extends MiniPhase with IdentityDenotTransformer:
3940
override def description: String = ParamForwarding.description
4041

4142
def transformIfParamAlias(mdef: ValOrDefDef)(using Context): Tree =
42-
43-
def inheritedAccessor(sym: Symbol)(using Context): Symbol =
44-
val candidate = sym.owner.asClass.superClass
45-
.info.decl(sym.name).suchThat(_.is(ParamAccessor, butNot = Mutable))
46-
.symbol
47-
if !candidate.is(Private) // candidate might be private and accessible if it is in an outer class
48-
&& candidate.isAccessibleFrom(currentClass.thisType, superAccess = true)
49-
then
50-
candidate
51-
else if candidate.is(SuperParamAlias) then
52-
inheritedAccessor(candidate)
53-
else
54-
NoSymbol
55-
5643
val sym = mdef.symbol.asTerm
5744
if sym.is(SuperParamAlias) then
5845
assert(sym.is(ParamAccessor, butNot = Mutable))
@@ -84,3 +71,17 @@ class ParamForwarding extends MiniPhase with IdentityDenotTransformer:
8471
object ParamForwarding:
8572
val name: String = "paramForwarding"
8673
val description: String = "add forwarders for aliases of superclass parameters"
74+
75+
def inheritedAccessor(sym: Symbol)(using Context): Symbol =
76+
val candidate = sym.owner.asClass.superClass
77+
.info.decl(sym.name).suchThat(_.is(ParamAccessor, butNot = Mutable))
78+
.symbol
79+
if !candidate.is(Private) // candidate might be private and accessible if it is in an outer class
80+
&& candidate.isAccessibleFrom(currentClass.thisType, superAccess = true)
81+
then
82+
candidate
83+
else if candidate.is(SuperParamAlias) then
84+
inheritedAccessor(candidate)
85+
else
86+
NoSymbol
87+
end ParamForwarding

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import config.Printers.{checks, noPrinter}
1515
import Decorators._
1616
import OverridingPairs.isOverridingPair
1717
import typer.ErrorReporting._
18-
import config.Feature.{warnOnMigration, migrateTo3}
19-
import config.SourceVersion.`3.0`
18+
import config.Feature.{warnOnMigration, migrateTo3, sourceVersion}
19+
import config.SourceVersion.{`3.0`, `future`}
2020
import config.Printers.refcheck
2121
import reporting._
2222
import Constants.Constant
@@ -264,6 +264,8 @@ object RefChecks {
264264
* 1.10. If O is inline (and deferred, otherwise O would be final), M must be inline
265265
* 1.11. If O is a Scala-2 macro, M must be a Scala-2 macro.
266266
* 1.12. If O is non-experimental, M must be non-experimental.
267+
* 1.13 Under -source future, if O is a val parameter, M must be a val parameter
268+
* that passes its value on to O.
267269
* 2. Check that only abstract classes have deferred members
268270
* 3. Check that concrete classes do not have deferred definitions
269271
* that are not implemented in a subclass.
@@ -514,12 +516,26 @@ object RefChecks {
514516
overrideError(i"needs to be declared with @targetName(${"\""}${other.targetName}${"\""}) so that external names match")
515517
else
516518
overrideError("cannot have a @targetName annotation since external names would be different")
519+
else if other.is(ParamAccessor) && !isInheritedAccessor(member, other) then // (1.13)
520+
if sourceVersion.isAtLeast(`future`) then
521+
overrideError(i"cannot override val parameter ${other.showLocated}")
522+
else
523+
report.deprecationWarning(
524+
i"overriding val parameter ${other.showLocated} is deprecated, will be illegal in a future version",
525+
member.srcPos)
517526
else if !other.isExperimental && member.hasAnnotation(defn.ExperimentalAnnot) then // (1.12)
518527
overrideError("may not override non-experimental member")
519528
else if other.hasAnnotation(defn.DeprecatedOverridingAnnot) then
520529
overrideDeprecation("", member, other, "removed or renamed")
521530
end checkOverride
522531

532+
def isInheritedAccessor(mbr: Symbol, other: Symbol): Boolean =
533+
mbr.is(ParamAccessor)
534+
&& {
535+
val next = ParamForwarding.inheritedAccessor(mbr)
536+
next == other || isInheritedAccessor(next, other)
537+
}
538+
523539
OverridingPairsChecker(clazz, self).checkAll(checkOverride)
524540
printMixinOverrideErrors()
525541

tests/init/neg/override13.scala

Lines changed: 0 additions & 13 deletions
This file was deleted.

tests/init/neg/override16.scala

Lines changed: 0 additions & 23 deletions
This file was deleted.

tests/init/neg/override5.scala

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,3 @@ trait Base {
2525

2626
val message = "hello, " + name
2727
}
28-
29-
class Derived(val name: String) extends Base
30-
31-
class Derived2 extends Derived("hello") {
32-
override val name: String = "ok" // error
33-
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
trait Pet(val name: String, rest: Int):
2+
def f(suffix: String) = s"$name$suffix$rest"
3+
4+
class Birdie(override val name: String) extends Pet("huh", 1) // error
5+
6+

tests/neg-strict/i16092.scala

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
trait X {
2+
type T
3+
def process(t: T): Unit
4+
}
5+
6+
class Z(val x: X, val t: x.T) {
7+
def process(): Unit = x.process(t)
8+
}
9+
class Evil(x1: X, x2: X, t: x1.T) extends Z(x1, t) {
10+
override val x: X = x2 // error breaks connection between x and t
11+
}
12+
// alarm bells should be ringing by now
13+
14+
// taking it to its conclusion...
15+
object x1 extends X {
16+
override type T = Int
17+
override def process(t: T): Unit = println("Int: " + t)
18+
}
19+
object x2 extends X {
20+
override type T = String
21+
override def process(t: T): Unit = println("String: " + t)
22+
}
23+
24+
@main def Test = new Evil(x1, x2, 42).process() // BOOM: basically did x2.process(42)

tests/neg/i16092-members-only.scala

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
trait X:
2+
type T
3+
def process(t: T): Unit
4+
5+
abstract class Z:
6+
def x1: X
7+
val x: X = x1
8+
def t: x.T
9+
def process(): Unit = x.process(t)
10+
11+
class Evil extends Z:
12+
def x2: X
13+
override val x: X = x2
14+
15+
// alarm bells should be ringing by now
16+
17+
// taking it to its conclusion...
18+
object X1 extends X:
19+
override type T = Int
20+
override def process(t: T): Unit = println("Int: " + t)
21+
22+
object X2 extends X:
23+
override type T = String
24+
override def process(t: T): Unit = println("String: " + t)
25+
26+
@main def Test =
27+
new Evil{
28+
val x1 = X1
29+
val x2 = X2
30+
val t = 42 // error
31+
}.process() // BOOM: basically did x2.process(42)

tests/neg/i9460.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
trait A(val s: String) { println(s) }
2-
trait B extends A { override val s = "B" } // requires override val s
1+
trait A(s: String) { println(s) }
2+
trait B extends A { val s = "B" }
33
class C extends B // error
44
@main def Test = C()

tests/pos-with-compiler/tasty/test-definitions.scala

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,9 @@ object definitions {
163163
}
164164
}
165165

166-
abstract class LambdaType[ParamInfo, This <: LambdaType[ParamInfo, This]](
166+
abstract class LambdaType[ParamInfo, This <: LambdaType[ParamInfo, This]]
167+
extends Type {
167168
val companion: LambdaTypeCompanion[ParamInfo, This]
168-
) extends Type {
169169
private[Type] var _pinfos: List[ParamInfo]
170170
private[Type] var _restpe: Type
171171

@@ -186,16 +186,21 @@ object definitions {
186186
}
187187

188188
case class MethodType(paramNames: List[String], private[Type] var _pinfos: List[Type], private[Type] var _restpe: Type)
189-
extends LambdaType[Type, MethodType](MethodType) {
189+
extends LambdaType[Type, MethodType] {
190+
override val companion = MethodType
190191
def isImplicit = (companion `eq` ImplicitMethodType) || (companion `eq` ErasedImplicitMethodType)
191192
def isErased = (companion `eq` ErasedMethodType) || (companion `eq` ErasedImplicitMethodType)
192193
}
193194

194195
case class PolyType(paramNames: List[String], private[Type] var _pinfos: List[TypeBounds], private[Type] var _restpe: Type)
195-
extends LambdaType[TypeBounds, PolyType](PolyType)
196+
extends LambdaType[TypeBounds, PolyType] {
197+
override val companion = PolyType
198+
}
196199

197200
case class TypeLambda(paramNames: List[String], private[Type] var _pinfos: List[TypeBounds], private[Type] var _restpe: Type)
198-
extends LambdaType[TypeBounds, TypeLambda](TypeLambda)
201+
extends LambdaType[TypeBounds, TypeLambda] {
202+
override val companion = TypeLambda
203+
}
199204

200205
object TypeLambda extends LambdaTypeCompanion[TypeBounds, TypeLambda]
201206
object PolyType extends LambdaTypeCompanion[TypeBounds, PolyType]

tests/pos/i16092.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
class A(val x: Int)
2+
class B(override val x: Int) extends A(x)
3+
4+
class C(x: Int) extends A(x)
5+
case class D(override val x: Int) extends C(x)
6+
7+
// The following is extracted from akka:
8+
trait LogEvent {
9+
def cause: Throwable
10+
}
11+
12+
/**
13+
* For ERROR Logging
14+
*/
15+
case class Error(override val cause: Throwable) extends LogEvent
16+
class Error2(override val cause: Throwable) extends Error(cause)
17+
class Error3(override val cause: Throwable) extends Error2(cause)
18+

tests/pos/i2051.scala

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,3 @@ class B[T](override val x:T) extends A[T](x)
44
class C[T](val x:T, val y: Int, val z: Boolean)
55
class D[T](override val x:T, y: Int, z: Boolean) extends C[T](x, y, z)
66

7-
trait X(val x: Int, y: Int, z: Int)
8-
trait Y(override val x: Int, y: Int, z: Int) extends X
9-
class Z(override val x: Int, y: Int, z: Int) extends Y(x, y, z) with X(x, y, z)

tests/run/i11344.scala

Lines changed: 0 additions & 8 deletions
This file was deleted.

tests/run/i16092.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
class A(a: Int)
3+
4+
class B extends A(1):
5+
val a = 2 // ok
6+
7+
@main def Test =
8+
assert(B().a == 2)

tests/run/paramForwarding.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ class B(override val theValue: Int) extends A(42) {
1616

1717
// Bz contains a field Bz.theValue$$local accessible using the getter
1818
// Bz.theValue() which overrides A.theValue()
19-
class Bz extends A(42) {
20-
override val theValue: Int = 10
19+
class Bz(override val theValue: Int = 10) extends A(42) {
2120
val theValueInBz = theValue
2221
}
2322

0 commit comments

Comments
 (0)