Skip to content

Commit 407962a

Browse files
committed
Check restrictions on how this values can be used in constructors.
Essentially, `this` cannot be used before the super constructor call, except to assign fields (but not reading). These restrictions match those enforced by the JVM for bytecode. While we never intended `this` values to be restricted in this way, in practice we never produced IR that would violate those restrictions. In addition, we also enforce that we cannot perform calls to constructors, except exactly once within another constructor, and only with `this` as receiver. This also matches JVM restrictions. In the future, we may leverage those restrictions to validate strict field initialization. If a field *needs* to be initialized before it is ever read (for example, because it has a non-nullable reference type), then we can do so: we only have to check that it is definitely initialized before the super constructor call. The restrictions implemented in this commit ensure that nobody can read the field before the super constructor call starts.
1 parent ede4dc1 commit 407962a

File tree

7 files changed

+314
-50
lines changed

7 files changed

+314
-50
lines changed

linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala

Lines changed: 140 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ private final class ClassDefChecker(classDef: ClassDef,
231231
}
232232

233233
private def checkMethodDef(methodDef: MethodDef): Unit = withPerMethodState {
234-
val MethodDef(flags, MethodIdent(name), _, params, _, body) = methodDef
234+
val MethodDef(flags, MethodIdent(name), _, params, _, optBody) = methodDef
235235
implicit val ctx = ErrorContext(methodDef)
236236

237237
val namespace = flags.namespace
@@ -246,7 +246,7 @@ private final class ClassDefChecker(classDef: ClassDef,
246246
if (!methods(namespace.ordinal).add(name))
247247
reportError(i"duplicate ${namespace.prefixString}method '$name'")
248248

249-
if (body.isEmpty && namespace != MemberNamespace.Public)
249+
if (optBody.isEmpty && namespace != MemberNamespace.Public)
250250
reportError("Abstract methods may only be in the public namespace")
251251

252252
// ClassInitializer
@@ -294,11 +294,20 @@ private final class ClassDefChecker(classDef: ClassDef,
294294
}
295295

296296
// Body
297-
val thisType = if (static) NoType else instanceThisType
298-
val bodyEnv = Env.fromParams(params)
299-
.withThisType(thisType)
300-
.withInConstructor(isConstructor)
301-
body.foreach(checkTree(_, bodyEnv))
297+
for (body <- optBody) {
298+
val thisType = if (static) NoType else instanceThisType
299+
val bodyEnv = Env.fromParams(params)
300+
.withThisType(thisType)
301+
.withInConstructor(isConstructor)
302+
303+
if (isConstructor && !postOptimizer) {
304+
// Strict checking of `this` values and super/delegate constructor calls
305+
checkConstructorBodyStrict(body, bodyEnv)
306+
} else {
307+
// Regular checking of `this` values
308+
checkTree(body, bodyEnv)
309+
}
310+
}
302311
}
303312

304313
private def checkJSConstructorDef(ctorDef: JSConstructorDef): Unit = withPerMethodState {
@@ -319,14 +328,10 @@ private final class ClassDefChecker(classDef: ClassDef,
319328
.withHasNewTarget(true)
320329
.withInConstructor(true)
321330

322-
val envJustBeforeSuper = body.beforeSuper.foldLeft(startEnv) { (prevEnv, stat) =>
323-
checkTree(stat, prevEnv)
324-
}
331+
val envJustBeforeSuper = checkBlockStats(body.beforeSuper, startEnv)
325332
checkTreeOrSpreads(body.superCall.args, envJustBeforeSuper)
326333
val envJustAfterSuper = envJustBeforeSuper.withThisType(instanceThisType)
327-
body.afterSuper.foldLeft(envJustAfterSuper) { (prevEnv, stat) =>
328-
checkTree(stat, prevEnv)
329-
}
334+
checkBlockStats(body.afterSuper, envJustAfterSuper)
330335
}
331336

332337
private def checkJSMethodDef(methodDef: JSMethodDef): Unit = withPerMethodState {
@@ -505,6 +510,81 @@ private final class ClassDefChecker(classDef: ClassDef,
505510
}
506511
}
507512

513+
private def checkConstructorBodyStrict(body: Tree, bodyEnv: Env): Unit = {
514+
/* In a strictly checked constructor body, usages of the `this` value are
515+
* restricted according to the following rules.
516+
*
517+
* If the enclosing class is `jl.Object`, the full `body` is unrestricted.
518+
* Otherwise:
519+
*
520+
* - The body must be a possible Block with statements `stats`.
521+
* - There exists a unique `stat` in the `stats` list that is an
522+
* `ApplyStatically(_, This(), cls, someConstructor, args)`, called the
523+
* delegate constructor call.
524+
* - There is no such `ApplyStatically` anywhere else in the body.
525+
* - `cls` must be the enclosing class or its direct superclass.
526+
* - In the statements before the delegate constructor call, and within
527+
* `args`:
528+
* - `This()` cannot be used except in `Assign(Select(This(), _), _)`,
529+
* i.e., to assign to a field (but not read from one).
530+
* - `StoreModule()` cannot be used.
531+
* - After the delegate constructor call, `This` can be used without
532+
* restriction.
533+
*/
534+
535+
implicit val ctx = ErrorContext(body)
536+
537+
val bodyStats = body match {
538+
case Block(stats) => stats
539+
case Skip() => Nil
540+
case _ => body :: Nil
541+
}
542+
543+
if (isJLObject) {
544+
checkBlockStats(bodyStats, bodyEnv)
545+
} else {
546+
val (beforeDelegateCtor, rest) = bodyStats.span {
547+
case ApplyStatically(_, This(), _, MethodIdent(ctor), _) =>
548+
!ctor.isConstructor
549+
case _ =>
550+
true
551+
}
552+
553+
if (rest.isEmpty) {
554+
reportError(i"Constructor must contain a delegate constructor call")
555+
checkBlockStats(beforeDelegateCtor, bodyEnv) // to report further errors
556+
} else {
557+
val (delegateCtorCall: ApplyStatically) :: afterDelegateCtor = rest
558+
val ApplyStatically(_, receiver, cls, MethodIdent(ctor), args) = delegateCtorCall
559+
560+
val initEnv = bodyEnv.withIsThisRestricted(true)
561+
val envJustBeforeSuper = checkBlockStats(beforeDelegateCtor, initEnv)
562+
563+
checkApplyArgs(ctor, args, envJustBeforeSuper)
564+
565+
val unrestrictedEnv = envJustBeforeSuper.withIsThisRestricted(false)
566+
567+
checkTree(receiver, unrestrictedEnv) // check that the This itself is valid
568+
569+
if (!(cls == classDef.className || classDef.superClass.exists(_.name == cls))) {
570+
implicit val ctx = ErrorContext(delegateCtorCall)
571+
reportError(
572+
i"Invalid target class $cls for delegate constructor call; " +
573+
i"expected ${classDef.className}" +
574+
classDef.superClass.fold("")(s => i" or ${s.name}"))
575+
}
576+
577+
checkBlockStats(afterDelegateCtor, unrestrictedEnv)
578+
}
579+
}
580+
}
581+
582+
private def checkBlockStats(stats: List[Tree], env: Env): Env = {
583+
stats.foldLeft(env) { (prevEnv, stat) =>
584+
checkTree(stat, prevEnv)
585+
}
586+
}
587+
508588
private def checkTreeOrSpreads(trees: List[TreeOrJSSpread], env: Env): Unit = {
509589
trees.foreach {
510590
case JSSpread(items) => checkTree(items, env)
@@ -515,15 +595,19 @@ private final class ClassDefChecker(classDef: ClassDef,
515595
private def checkTrees(trees: List[Tree], env: Env): Unit =
516596
trees.foreach(checkTree(_, env))
517597

598+
private def checkApplyArgs(methodName: MethodName, args: List[Tree], env: Env)(
599+
implicit ctx: ErrorContext): Unit = {
600+
val paramRefsCount = methodName.paramTypeRefs.size
601+
if (args.size != paramRefsCount)
602+
reportError(i"Arity mismatch: $paramRefsCount expected but ${args.size} found")
603+
checkTrees(args, env)
604+
}
605+
518606
private def checkTree(tree: Tree, env: Env): Env = {
519607
implicit val ctx = ErrorContext(tree)
520608

521-
def checkApplyGeneric(methodName: MethodName, args: List[Tree]): Unit = {
522-
val paramRefsCount = methodName.paramTypeRefs.size
523-
if (args.size != paramRefsCount)
524-
reportError(i"Arity mismatch: $paramRefsCount expected but ${args.size} found")
525-
checkTrees(args, env)
526-
}
609+
def checkApplyGeneric(methodName: MethodName, args: List[Tree]): Unit =
610+
checkApplyArgs(methodName, args, env)
527611

528612
val newEnv = tree match {
529613
case VarDef(ident, _, vtpe, mutable, _) =>
@@ -540,16 +624,19 @@ private final class ClassDefChecker(classDef: ClassDef,
540624
case Skip() =>
541625

542626
case Block(stats) =>
543-
stats.foldLeft(env) { (prevEnv, stat) =>
544-
checkTree(stat, prevEnv)
545-
}
627+
checkBlockStats(stats, env)
546628

547629
case Labeled(label, _, body) =>
548630
checkDeclareLabel(label)
549631
checkTree(body, env.withLabel(label.name))
550632

551633
case Assign(lhs, rhs) =>
552-
checkTree(lhs, env)
634+
lhs match {
635+
case Select(This(), _) if env.isThisRestricted =>
636+
checkTree(lhs, env.withIsThisRestricted(false))
637+
case _ =>
638+
checkTree(lhs, env)
639+
}
553640
checkTree(rhs, env)
554641

555642
lhs match {
@@ -618,10 +705,12 @@ private final class ClassDefChecker(classDef: ClassDef,
618705
case StoreModule() =>
619706
if (!classDef.kind.hasModuleAccessor)
620707
reportError(i"Illegal StoreModule inside class of kind ${classDef.kind}")
621-
if (!env.inConstructor)
708+
else if (!env.inConstructor)
622709
reportError(i"Illegal StoreModule outside of constructor")
623-
if (env.thisType == NoType) // can happen before JSSuperConstructorCall in JSModuleClass
710+
else if (env.thisType == NoType) // can happen before JSSuperConstructorCall in JSModuleClass
624711
reportError(i"Cannot find `this` in scope for StoreModule()")
712+
else if (env.isThisRestricted)
713+
reportError(i"Restricted use of `this` for StoreModule() before super constructor call")
625714

626715
case Select(qualifier, _) =>
627716
checkTree(qualifier, env)
@@ -637,6 +726,8 @@ private final class ClassDefChecker(classDef: ClassDef,
637726
checkApplyGeneric(method, args)
638727

639728
case ApplyStatically(_, receiver, _, MethodIdent(method), args) =>
729+
if (!postOptimizer && method.isConstructor)
730+
reportError(i"Illegal constructor call")
640731
checkTree(receiver, env)
641732
checkApplyGeneric(method, args)
642733

@@ -816,6 +907,8 @@ private final class ClassDefChecker(classDef: ClassDef,
816907
reportError(i"Cannot find `this` in scope")
817908
else if (tree.tpe != env.thisType)
818909
reportError(i"`this` of type ${env.thisType} typed as ${tree.tpe}")
910+
else if (env.isThisRestricted)
911+
reportError(i"Restricted use of `this` before the super constructor call")
819912

820913
case Closure(arrow, captureParams, params, restParam, body, captureValues) =>
821914
/* Check compliance of captureValues wrt. captureParams in the current
@@ -960,7 +1053,9 @@ object ClassDefChecker {
9601053
/** Return types by label. */
9611054
val returnLabels: Set[LabelName],
9621055
/** Whether we are in a constructor of the class. */
963-
val inConstructor: Boolean
1056+
val inConstructor: Boolean,
1057+
/** Whether usages of `this` are restricted in this scope. */
1058+
val isThisRestricted: Boolean
9641059
) {
9651060
import Env._
9661061

@@ -979,20 +1074,33 @@ object ClassDefChecker {
9791074
def withInConstructor(inConstructor: Boolean): Env =
9801075
copy(inConstructor = inConstructor)
9811076

1077+
def withIsThisRestricted(isThisRestricted: Boolean): Env =
1078+
copy(isThisRestricted = isThisRestricted)
1079+
9821080
private def copy(
9831081
hasNewTarget: Boolean = hasNewTarget,
9841082
thisType: Type = thisType,
9851083
locals: Map[LocalName, LocalDef] = locals,
9861084
returnLabels: Set[LabelName] = returnLabels,
987-
inConstructor: Boolean = inConstructor
1085+
inConstructor: Boolean = inConstructor,
1086+
isThisRestricted: Boolean = isThisRestricted
9881087
): Env = {
989-
new Env(hasNewTarget, thisType, locals, returnLabels, inConstructor)
1088+
new Env(hasNewTarget, thisType, locals, returnLabels, inConstructor,
1089+
isThisRestricted)
9901090
}
9911091
}
9921092

9931093
private object Env {
994-
val empty: Env =
995-
new Env(hasNewTarget = false, thisType = NoType, Map.empty, Set.empty, inConstructor = false)
1094+
val empty: Env = {
1095+
new Env(
1096+
hasNewTarget = false,
1097+
thisType = NoType,
1098+
locals = Map.empty,
1099+
returnLabels = Set.empty,
1100+
inConstructor = false,
1101+
isThisRestricted = false
1102+
)
1103+
}
9961104

9971105
def fromParams(params: List[ParamDef]): Env = {
9981106
val paramLocalDefs =
@@ -1004,7 +1112,8 @@ object ClassDefChecker {
10041112
thisType = NoType,
10051113
paramLocalDefs.toMap,
10061114
Set.empty,
1007-
inConstructor = false
1115+
inConstructor = false,
1116+
isThisRestricted = false
10081117
)
10091118
}
10101119
}

linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ class AnalyzerTest {
192192
val classDefs = Seq(
193193
classDef("A", kind = kindSub,
194194
superClass = Some("B"),
195-
methods = requiredMethods("A", kindSub),
195+
methods = requiredMethods("A", kindSub, "B"),
196196
jsConstructor = requiredJSConstructor(kindSub)),
197197
classDef("B", kind = kindBase,
198198
superClass = validParentForKind(kindBase),
@@ -321,7 +321,7 @@ class AnalyzerTest {
321321
)))(EOH, UNV)
322322
)),
323323
classDef("B", superClass = Some("A"),
324-
methods = List(trivialCtor("B")))
324+
methods = List(trivialCtor("B", "A")))
325325
)
326326

327327
val analysis = computeAnalysis(classDefs,
@@ -350,7 +350,7 @@ class AnalyzerTest {
350350
methods = List(trivialCtor("A"))),
351351
classDef("B", superClass = Some("A"),
352352
methods = List(
353-
trivialCtor("B"),
353+
trivialCtor("B", "A"),
354354
MethodDef(EMF, fooMethodName, NON, Nil, IntType, Some(int(5)))(EOH, UNV)
355355
))
356356
)
@@ -771,12 +771,12 @@ class AnalyzerTest {
771771
)),
772772
classDef("B", superClass = Some("A"), interfaces = List("I2"),
773773
methods = List(
774-
trivialCtor("B"),
774+
trivialCtor("B", "A"),
775775
MethodDef(EMF, fooMethodName, NON, Nil, IntType, Some(int(5)))(EOH, UNV)
776776
)),
777777
classDef("C", superClass = Some("B"),
778778
methods = List(
779-
trivialCtor("C"),
779+
trivialCtor("C", "B"),
780780
MethodDef(EMF, barMethodName, NON, Nil, IntType, Some(int(5)))(EOH, UNV)
781781
))
782782
)

linker/shared/src/test/scala/org/scalajs/linker/BaseLinkerTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class BaseLinkerTest {
5555
"Sub",
5656
kind = ClassKind.Class,
5757
superClass = Some("Base"),
58-
methods = List(trivialCtor("Sub"))
58+
methods = List(trivialCtor("Sub", "Base"))
5959
),
6060
mainTestClassDef(
6161
consoleLog(Apply(EAF, New("Sub", NoArgConstructorName, Nil), fooName, Nil)(IntType))

linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ class IRCheckerTest {
132132
superClass = Some("C"),
133133
interfaces = List("B"),
134134
methods = List(
135-
trivialCtor("D"),
135+
trivialCtor("D", "C"),
136136
MethodDef(
137137
EMF.withNamespace(MemberNamespace.PublicStatic),
138138
testMethodName,

linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,10 +500,12 @@ class OptimizerTest {
500500
// def this() = {
501501
// this.x = null
502502
// this.y = 5
503+
// this.jl.Object::<init>()
503504
// }
504505
MethodDef(EMF.withNamespace(Constructor), NoArgConstructorName, NON, Nil, NoType, Some(Block(
505506
Assign(Select(This()(ClassType("Foo")), FieldName("Foo", "x"))(witnessType), Null()),
506-
Assign(Select(This()(ClassType("Foo")), FieldName("Foo", "y"))(IntType), int(5))
507+
Assign(Select(This()(ClassType("Foo")), FieldName("Foo", "y"))(IntType), int(5)),
508+
trivialSuperCtorCall("Foo")
507509
)))(EOH, UNV),
508510

509511
// def method(): Int = this.y

0 commit comments

Comments
 (0)