Skip to content

Commit b7fee7f

Browse files
Josh Goldbergortarbuckton
authored
Allowed non-this, non-super code before super call in derived classes with property initializers (#29374)
* Allowed non-this, non-super code before super call in derived classes Fixes #8277. It feels wrong to put a new `forEachChild` loop in the checker, though in the vast majority of user files this will be a very quick one. Is there a better way to check for a reference to `super` or `this`? * Used new isNewThisScope utility and dummy prop in baselines * Accounted for parameters and get/set accessor bodies * Accounted for class extensions * (node|statement)RefersToSuperOrThis wasn't checking root level scope boundaries ```ts function () { return this; } ``` It was immediately going to `ts.forEachChild` so the statement itself wasn't being counted as a new `this` scope. * Better 'references' name and comments; accounted for more method edge case * Limited super calls to root-level statements in constructor bodies As per discussion in the issue, it would be ideal to consider any block that always ends up calling to super() the equivalent of a root-level super() statement. This would be valid: ```ts foo = 1; constructor() { condition() ? super(1) : super(0); this.foo; } ``` ...as it would compile to the equivalent of: ```ts function () { condition() ? super(1) : super(0); this.foo = 1; this.foo; } That change would a bit more intense and I'm very timid, so leaving it out of this PR. In the meantime the requirement is that the super() statement must itself be root-level. * Fixed error number following 'master' merge * Added decorator test cases * Again allowed arrow functions * Accepted new baselines * Added allowance for (super()) * Reworked emitter transforms for ES this binding semantics In trying to adjust to rbuckton's PR feedback, this orders derived class constructor bodies into five sections: 1. Pre-`super()` code 2. The `super()` call itself 3. Class properties with initializers 4. Parameter properties 5. The rest of the constructor I've looked through the updated baselines and it looks like they're generally better than before. Within the existing test cases that result in semantic errors for `this` access before `super`, several previously resulted in a global `_this` being created; now, they correctly defer referring to `_this` until it's assigned to `_super.call(this) || this`. * Used skipOuterExpressions when diving for super()s; fix prop ordering * Allow direct var _this = super() when no pre-super() statements; lint fixes * Always with the TSLint * One last touchup: skipOuterExpressions in es2015 transformer * Fixed new lint complaint in utilities.ts * Again added a falls-through; it'd be swell if I could run linting locally * This time I think I got it * Well at least the error is a different one * Undid irrelevant whitespace changes * Mostly addressed private/field issues * Accepted derivedClassSuperProperties baseline * Lint fix, lovely * Remove now-unnecesary comment * First round of feedback * Moved prologue statements to start of statements * Added consideration for super statements in loops and the like * Ordering and a _this_1 test * Missed the one change I needed... * First round of feedback corrections * Feedback round two: statements * Feedback: used more direct statements * Fixed classFields emit to not duplicate temp variables * Refactored es2015 helper to be less overloaded * Accounted for parentheses * Simpler feedback: -1, and emptyArray * Next feedback: superStatementIndex * Feedback: simplified to no longer create slice arrays * Adjusted for default and rest parameters * Added test case for commas * Corrected comment ranges * Handled comments after super, with tests * Fixed Bad/Late super baselines * Remove unused param and unnecessary baseline comments Co-authored-by: Orta Therox <[email protected]> Co-authored-by: Ron Buckton <[email protected]>
1 parent 70097c4 commit b7fee7f

File tree

74 files changed

+5561
-317
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+5561
-317
lines changed

src/compiler/checker.ts

+42-17
Original file line numberDiff line numberDiff line change
@@ -34731,34 +34731,42 @@ namespace ts {
3473134731
error(superCall, Diagnostics.A_constructor_cannot_contain_a_super_call_when_its_class_extends_null);
3473234732
}
3473334733

34734-
// The first statement in the body of a constructor (excluding prologue directives) must be a super call
34735-
// if both of the following are true:
34734+
// A super call must be root-level in a constructor if both of the following are true:
3473634735
// - The containing class is a derived class.
3473734736
// - The constructor declares parameter properties
3473834737
// or the containing class declares instance member variables with initializers.
34739-
const superCallShouldBeFirst =
34738+
34739+
const superCallShouldBeRootLevel =
3474034740
(getEmitScriptTarget(compilerOptions) !== ScriptTarget.ESNext || !useDefineForClassFields) &&
3474134741
(some((node.parent as ClassDeclaration).members, isInstancePropertyWithInitializerOrPrivateIdentifierProperty) ||
3474234742
some(node.parameters, p => hasSyntacticModifier(p, ModifierFlags.ParameterPropertyModifier)));
3474334743

34744-
// Skip past any prologue directives to find the first statement
34745-
// to ensure that it was a super call.
34746-
if (superCallShouldBeFirst) {
34747-
const statements = node.body!.statements;
34748-
let superCallStatement: ExpressionStatement | undefined;
34744+
if (superCallShouldBeRootLevel) {
34745+
// Until we have better flow analysis, it is an error to place the super call within any kind of block or conditional
34746+
// See GH #8277
34747+
if (!superCallIsRootLevelInConstructor(superCall, node.body!)) {
34748+
error(superCall, Diagnostics.A_super_call_must_be_a_root_level_statement_within_a_constructor_of_a_derived_class_that_contains_initialized_properties_parameter_properties_or_private_identifiers);
34749+
}
34750+
// Skip past any prologue directives to check statements for referring to 'super' or 'this' before a super call
34751+
else {
34752+
let superCallStatement: ExpressionStatement | undefined;
3474934753

34750-
for (const statement of statements) {
34751-
if (statement.kind === SyntaxKind.ExpressionStatement && isSuperCall((statement as ExpressionStatement).expression)) {
34752-
superCallStatement = statement as ExpressionStatement;
34753-
break;
34754+
for (const statement of node.body!.statements) {
34755+
if (isExpressionStatement(statement) && isSuperCall(skipOuterExpressions(statement.expression))) {
34756+
superCallStatement = statement;
34757+
break;
34758+
}
34759+
if (!isPrologueDirective(statement) && nodeImmediatelyReferencesSuperOrThis(statement)) {
34760+
break;
34761+
}
3475434762
}
34755-
if (!isPrologueDirective(statement)) {
34756-
break;
34763+
34764+
// Until we have better flow analysis, it is an error to place the super call within any kind of block or conditional
34765+
// See GH #8277
34766+
if (superCallStatement === undefined) {
34767+
error(node, Diagnostics.A_super_call_must_be_the_first_statement_in_the_constructor_to_refer_to_super_or_this_when_a_derived_class_contains_initialized_properties_parameter_properties_or_private_identifiers);
3475734768
}
3475834769
}
34759-
if (!superCallStatement) {
34760-
error(node, Diagnostics.A_super_call_must_be_the_first_statement_in_the_constructor_when_a_class_contains_initialized_properties_parameter_properties_or_private_identifiers);
34761-
}
3476234770
}
3476334771
}
3476434772
else if (!classExtendsNull) {
@@ -34767,6 +34775,23 @@ namespace ts {
3476734775
}
3476834776
}
3476934777

34778+
function superCallIsRootLevelInConstructor(superCall: Node, body: Block) {
34779+
const superCallParent = walkUpParenthesizedExpressions(superCall.parent);
34780+
return isExpressionStatement(superCallParent) && superCallParent.parent === body;
34781+
}
34782+
34783+
function nodeImmediatelyReferencesSuperOrThis(node: Node): boolean {
34784+
if (node.kind === SyntaxKind.SuperKeyword || node.kind === SyntaxKind.ThisKeyword) {
34785+
return true;
34786+
}
34787+
34788+
if (isThisContainerOrFunctionBlock(node)) {
34789+
return false;
34790+
}
34791+
34792+
return !!forEachChild(node, nodeImmediatelyReferencesSuperOrThis);
34793+
}
34794+
3477034795
function checkAccessorDeclaration(node: AccessorDeclaration) {
3477134796
if (produceDiagnostics) {
3477234797
// Grammar checking accessors

src/compiler/diagnosticMessages.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -1747,7 +1747,7 @@
17471747
"category": "Error",
17481748
"code": 2375
17491749
},
1750-
"A 'super' call must be the first statement in the constructor when a class contains initialized properties, parameter properties, or private identifiers.": {
1750+
"A 'super' call must be the first statement in the constructor to refer to 'super' or 'this' when a derived class contains initialized properties, parameter properties, or private identifiers.": {
17511751
"category": "Error",
17521752
"code": 2376
17531753
},
@@ -1839,6 +1839,10 @@
18391839
"category": "Error",
18401840
"code": 2400
18411841
},
1842+
"A 'super' call must be a root-level statement within a constructor of a derived class that contains initialized properties, parameter properties, or private identifiers.": {
1843+
"category": "Error",
1844+
"code": 2401
1845+
},
18421846
"Expression resolves to '_super' that compiler uses to capture base class reference.": {
18431847
"category": "Error",
18441848
"code": 2402

src/compiler/factory/nodeFactory.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -5885,7 +5885,7 @@ namespace ts {
58855885
* @param visitor Optional callback used to visit any custom prologue directives.
58865886
*/
58875887
function copyPrologue(source: readonly Statement[], target: Push<Statement>, ensureUseStrict?: boolean, visitor?: (node: Node) => VisitResult<Node>): number {
5888-
const offset = copyStandardPrologue(source, target, ensureUseStrict);
5888+
const offset = copyStandardPrologue(source, target, 0, ensureUseStrict);
58895889
return copyCustomPrologue(source, target, offset, visitor);
58905890
}
58915891

@@ -5901,12 +5901,13 @@ namespace ts {
59015901
* Copies only the standard (string-expression) prologue-directives into the target statement-array.
59025902
* @param source origin statements array
59035903
* @param target result statements array
5904+
* @param statementOffset The offset at which to begin the copy.
59045905
* @param ensureUseStrict boolean determining whether the function need to add prologue-directives
5906+
* @returns Count of how many directive statements were copied.
59055907
*/
5906-
function copyStandardPrologue(source: readonly Statement[], target: Push<Statement>, ensureUseStrict?: boolean): number {
5908+
function copyStandardPrologue(source: readonly Statement[], target: Push<Statement>, statementOffset = 0, ensureUseStrict?: boolean): number {
59075909
Debug.assert(target.length === 0, "Prologue directives should be at the first statement in the target statements array");
59085910
let foundUseStrict = false;
5909-
let statementOffset = 0;
59105911
const numStatements = source.length;
59115912
while (statementOffset < numStatements) {
59125913
const statement = source[statementOffset];

src/compiler/transformers/classFields.ts

+65-14
Original file line numberDiff line numberDiff line change
@@ -1249,10 +1249,28 @@ namespace ts {
12491249

12501250
resumeLexicalEnvironment();
12511251

1252-
let indexOfFirstStatement = 0;
1252+
const needsSyntheticConstructor = !constructor && isDerivedClass;
1253+
let indexOfFirstStatementAfterSuper = 0;
1254+
let prologueStatementCount = 0;
1255+
let superStatementIndex = -1;
12531256
let statements: Statement[] = [];
12541257

1255-
if (!constructor && isDerivedClass) {
1258+
if (constructor?.body?.statements) {
1259+
prologueStatementCount = factory.copyPrologue(constructor.body.statements, statements, /*ensureUseStrict*/ false, visitor);
1260+
superStatementIndex = findSuperStatementIndex(constructor.body.statements, prologueStatementCount);
1261+
1262+
// If there was a super call, visit existing statements up to and including it
1263+
if (superStatementIndex >= 0) {
1264+
indexOfFirstStatementAfterSuper = superStatementIndex + 1;
1265+
statements = [
1266+
...statements.slice(0, prologueStatementCount),
1267+
...visitNodes(constructor.body.statements, visitor, isStatement, prologueStatementCount, indexOfFirstStatementAfterSuper - prologueStatementCount),
1268+
...statements.slice(prologueStatementCount),
1269+
];
1270+
}
1271+
}
1272+
1273+
if (needsSyntheticConstructor) {
12561274
// Add a synthetic `super` call:
12571275
//
12581276
// super(...arguments);
@@ -1268,9 +1286,6 @@ namespace ts {
12681286
);
12691287
}
12701288

1271-
if (constructor) {
1272-
indexOfFirstStatement = addPrologueDirectivesAndInitialSuperCall(factory, constructor, statements, visitor);
1273-
}
12741289
// Add the property initializers. Transforms this:
12751290
//
12761291
// public x = 1;
@@ -1281,26 +1296,52 @@ namespace ts {
12811296
// this.x = 1;
12821297
// }
12831298
//
1299+
// If we do useDefineForClassFields, they'll be converted elsewhere.
1300+
// We instead *remove* them from the transformed output at this stage.
1301+
let parameterPropertyDeclarationCount = 0;
12841302
if (constructor?.body) {
1285-
let afterParameterProperties = findIndex(constructor.body.statements, s => !isParameterPropertyDeclaration(getOriginalNode(s), constructor), indexOfFirstStatement);
1286-
if (afterParameterProperties === -1) {
1287-
afterParameterProperties = constructor.body.statements.length;
1303+
if (useDefineForClassFields) {
1304+
statements = statements.filter(statement => !isParameterPropertyDeclaration(getOriginalNode(statement), constructor));
12881305
}
1289-
if (afterParameterProperties > indexOfFirstStatement) {
1290-
if (!useDefineForClassFields) {
1291-
addRange(statements, visitNodes(constructor.body.statements, visitor, isStatement, indexOfFirstStatement, afterParameterProperties - indexOfFirstStatement));
1306+
else {
1307+
for (const statement of constructor.body.statements) {
1308+
if (isParameterPropertyDeclaration(getOriginalNode(statement), constructor)) {
1309+
parameterPropertyDeclarationCount++;
1310+
}
1311+
}
1312+
if (parameterPropertyDeclarationCount > 0) {
1313+
const parameterProperties = visitNodes(constructor.body.statements, visitor, isStatement, indexOfFirstStatementAfterSuper, parameterPropertyDeclarationCount);
1314+
1315+
// If there was a super() call found, add parameter properties immediately after it
1316+
if (superStatementIndex >= 0) {
1317+
addRange(statements, parameterProperties);
1318+
}
1319+
// If a synthetic super() call was added, add them just after it
1320+
else if (needsSyntheticConstructor) {
1321+
statements = [
1322+
statements[0],
1323+
...parameterProperties,
1324+
...statements.slice(1),
1325+
];
1326+
}
1327+
// Since there wasn't a super() call, add them to the top of the constructor
1328+
else {
1329+
statements = [...parameterProperties, ...statements];
1330+
}
1331+
1332+
indexOfFirstStatementAfterSuper += parameterPropertyDeclarationCount;
12921333
}
1293-
indexOfFirstStatement = afterParameterProperties;
12941334
}
12951335
}
1336+
12961337
const receiver = factory.createThis();
12971338
// private methods can be called in property initializers, they should execute first.
12981339
addMethodStatements(statements, privateMethodsAndAccessors, receiver);
12991340
addPropertyOrClassStaticBlockStatements(statements, properties, receiver);
13001341

1301-
// Add existing statements, skipping the initial super call.
1342+
// Add existing statements after the initial prologues and super call
13021343
if (constructor) {
1303-
addRange(statements, visitNodes(constructor.body!.statements, visitor, isStatement, indexOfFirstStatement));
1344+
addRange(statements, visitNodes(constructor.body!.statements, visitBodyStatement, isStatement, indexOfFirstStatementAfterSuper + prologueStatementCount));
13041345
}
13051346

13061347
statements = factory.mergeLexicalEnvironment(statements, endLexicalEnvironment());
@@ -1315,6 +1356,14 @@ namespace ts {
13151356
),
13161357
/*location*/ constructor ? constructor.body : undefined
13171358
);
1359+
1360+
function visitBodyStatement(statement: Node) {
1361+
if (useDefineForClassFields && isParameterPropertyDeclaration(getOriginalNode(statement), constructor!)) {
1362+
return undefined;
1363+
}
1364+
1365+
return visitor(statement);
1366+
}
13181367
}
13191368

13201369
/**
@@ -1335,11 +1384,13 @@ namespace ts {
13351384
setSourceMapRange(statement, moveRangePastModifiers(property));
13361385
setCommentRange(statement, property);
13371386
setOriginalNode(statement, property);
1387+
13381388
// `setOriginalNode` *copies* the `emitNode` from `property`, so now both
13391389
// `statement` and `expression` have a copy of the synthesized comments.
13401390
// Drop the comments from expression to avoid printing them twice.
13411391
setSyntheticLeadingComments(expression, undefined);
13421392
setSyntheticTrailingComments(expression, undefined);
1393+
13431394
statements.push(statement);
13441395
}
13451396
}

0 commit comments

Comments
 (0)