diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 95282f381..80fde36bd 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -7712,6 +7712,24 @@ function paramPropInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLoca return out; } +/** Whether a super() call's value is discarded by its statement: the call + * is the expression statement itself, or an operand of a comma chain that + * is (`super(m, o), this.code = c;`, as minifiers join statements). The + * statement lowering runs each comma operand as its own statement in + * source order, so the chain lowers exactly like `super(m, o); this.code = + * c;`. Parentheses are transparent to both. */ +function superCallInStatementPosition(call: ts.CallExpression): boolean { + let node: ts.Node = call; + for (;;) { + const parent = node.parent; + if (ts.isExpressionStatement(parent)) return true; + const comma = + ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.CommaToken; + if (!comma && !ts.isParenthesizedExpression(parent)) return false; + node = parent; + } +} + /** Super statements initialize the base and then the derived fields on * whichever control-flow path executes. Repeated calls are explicitly * refused until native layouts can allocate a second receiver. */ @@ -7729,7 +7747,7 @@ export function lowerDerivedCtorBody( const previousInitialized = context.superInitialized; context.superInitialized = initialized; context.superCall = (superCall) => { - if (!ts.isExpressionStatement(superCall.parent)) + if (!superCallInStatementPosition(superCall)) lowerer.unsupported("SC1090", superCall, "super() in an expression position"); superSeen = true; const loc = locOf(superCall); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-jose-errors.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-jose-errors.js.json new file mode 100644 index 000000000..0ed94bfd7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-jose-errors.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/super-comma-jose-errors.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-sequence-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-sequence-js.js.json new file mode 100644 index 000000000..5698bfbed --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-sequence-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/super-comma-sequence-js.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-sequence.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-sequence.ts.json new file mode 100644 index 000000000..3954c5766 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-sequence.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/super-comma-sequence.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/super-comma-nested.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/super-comma-nested.ts.json new file mode 100644 index 000000000..558b0ad78 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/super-comma-nested.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/super-comma-nested.ts" + ], + "diags": [] +} diff --git a/tests/corpus/super-comma-jose-errors.js b/tests/corpus/super-comma-jose-errors.js new file mode 100644 index 000000000..a194fca93 --- /dev/null +++ b/tests/corpus/super-comma-jose-errors.js @@ -0,0 +1,84 @@ +// jose 6.2.12's error classes (dist/webapi/util/errors.js), verbatim in +// shape: minifiers join a derived constructor's statements into one comma +// expression that opens with super(...). Each comma operand runs as its +// own statement, so the base constructor and the class field initializers +// finish before the trailing assignments. +class JOSEError extends Error { + static code = "ERR_JOSE_GENERIC"; + code = "ERR_JOSE_GENERIC"; + constructor(message, options) { + super(message, options), this.name = this.constructor.name, Error.captureStackTrace?.(this, this.constructor); + } +} +class JWTClaimValidationFailed extends JOSEError { + static code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JWTExpired extends JOSEError { + static code = "ERR_JWT_EXPIRED"; + code = "ERR_JWT_EXPIRED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JOSEAlgNotAllowed extends JOSEError { + static code = "ERR_JOSE_ALG_NOT_ALLOWED"; + code = "ERR_JOSE_ALG_NOT_ALLOWED"; +} +class JWSSignatureVerificationFailed extends JOSEError { + static code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + constructor(message = "signature verification failed", options) { + super(message, options); + } +} + +// The stack is read where the receiver's class is known: a read through an +// untyped parameter does not see the stack (a documented scriptc gap). +function describe(label, error, stack) { + console.log(label, error.name, error.code, JSON.stringify(error.message)); + console.log(" instanceof", error instanceof JOSEError, error instanceof Error); + console.log(" stack head", String(stack).split("\n")[0]); +} + +const generic = new JOSEError("generic failure"); +describe("generic", generic, generic.stack); +console.log(" static", JOSEError.code, "cause" in generic); + +const withCause = new JOSEError("wrapped", { cause: "root" }); +describe("wrapped", withCause, withCause.stack); +console.log(" cause", withCause.cause); + +const claims = { sub: "user", exp: 1 }; +const failed = new JWTClaimValidationFailed('"iss" claim mismatch', claims, "iss", "check_failed"); +describe("claim", failed, failed.stack); +console.log(" fields", failed.claim, failed.reason, failed.payload === claims); +console.log(" cause", failed.cause.claim, failed.cause.reason, failed.cause.payload === claims); +console.log(" chain", failed instanceof JWTClaimValidationFailed, failed instanceof JWTExpired); + +const defaults = new JWTClaimValidationFailed("defaults", claims); +console.log("defaults", defaults.claim, defaults.reason); + +const expired = new JWTExpired('"exp" claim timestamp check failed', claims, "exp"); +describe("expired", expired, expired.stack); +console.log(" static", JWTExpired.code, expired.reason); + +const alg = new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); +describe("alg", alg, alg.stack); +const sig = new JWSSignatureVerificationFailed(); +describe("sig", sig, sig.stack); + +try { + throw new JWTExpired("thrown", claims, "exp", "check_failed"); +} catch (error) { + if (error instanceof JOSEError) console.log("caught", error.name, error.code, error.claim); +} diff --git a/tests/corpus/super-comma-sequence-js.js b/tests/corpus/super-comma-sequence-js.js new file mode 100644 index 000000000..073bcd97d --- /dev/null +++ b/tests/corpus/super-comma-sequence-js.js @@ -0,0 +1,49 @@ +// The JavaScript side of super-comma-sequence.ts: a statement-level comma +// expression containing super(...) runs its operands in source order, with +// the base constructor and the derived field initializers at the super() +// call, as the separate statements a minifier joined would. +function log(label, value) { + console.log(" " + label); + return value; +} + +class Base { + constructor(origin) { + console.log(" Base body", origin); + this.origin = origin; + } +} + +class Joined extends Base { + first = log("field first", 1); + second = this.first + 1; + constructor(origin) { + super(log("super argument", origin)), log("after super", 0), this.tag = `${this.origin}:${this.second}`; + } +} + +class Leading extends Base { + value = log("field value", "v"); + constructor(origin) { + log("before super", 0), super(origin), log("after super " + this.value, 0); + } +} + +class Again extends Joined { + extra = log("field extra", "e"); + constructor() { + super("again"), this.tag = this.tag + "!", this.extra = this.extra + this.tag; + } +} + +console.log("joined:"); +const joined = new Joined("j"); +console.log(joined.origin, joined.first, joined.second, joined.tag); + +console.log("leading:"); +const leading = new Leading("l"); +console.log(leading.origin, leading.value); + +console.log("again:"); +const again = new Again(); +console.log(again.origin, again.tag, again.extra, again instanceof Joined, again instanceof Base); diff --git a/tests/corpus/super-comma-sequence.ts b/tests/corpus/super-comma-sequence.ts new file mode 100644 index 000000000..46e23a509 --- /dev/null +++ b/tests/corpus/super-comma-sequence.ts @@ -0,0 +1,101 @@ +// @transform-types +// super(...) as an operand of a statement-level comma expression lowers +// like the separate statements a minifier joined: operands run in source +// order, the base constructor and then the derived field initializers run +// at the super() call, and the trailing operands see the initialized +// instance. +function log(label: string, value: T): T { + console.log(" " + label); + return value; +} + +class Base { + readonly origin: string; + constructor(origin: string) { + console.log(" Base body", origin); + this.origin = origin; + } +} + +class Joined extends Base { + first = log("field first", 1); + second: number = this.first + 1; + tag: string; + constructor(origin: string) { + super(log("super argument", origin)), log("after super", 0), this.tag = `${this.origin}:${this.second}`; + } +} + +class Leading extends Base { + value = log("field value", "v"); + constructor(origin: string) { + log("before super", 0), super(origin), log("after super " + this.value, 0); + } +} + +class Parenthesized extends Base { + count = 0; + constructor() { + (super("paren"), this.count++, this.count++); + } +} + +class Nested extends Base { + steps: string[] = []; + constructor() { + super("nested"), (this.steps.push("a"), this.steps.push("b")), this.steps.push("c"); + } +} + +class Branching extends Base { + side: string; + constructor(left: boolean) { + if (left) super("left"), this.side = "L"; + else super("right"), this.side = "R"; + } +} + +class Point { + constructor( + public x: number, + public y: number, + ) {} +} + +class Point3 extends Point { + label = "p3"; + constructor( + x: number, + y: number, + public z: number, + ) { + super(x, y), console.log(" Point3", this.x, this.y, this.z, this.label); + } +} + +class Point4 extends Point3 { + w: number; + constructor(x: number, y: number, z: number, w: number) { + super(x, y, z), this.w = w, this.label = "p4"; + } +} + +console.log("joined:"); +const joined = new Joined("j"); +console.log(joined.origin, joined.first, joined.second, joined.tag); + +console.log("leading:"); +const leading = new Leading("l"); +console.log(leading.origin, leading.value); + +const paren = new Parenthesized(); +console.log("parenthesized:", paren.origin, paren.count); + +const nested = new Nested(); +console.log("nested:", nested.origin, nested.steps.join(",")); + +console.log("branching:", new Branching(true).side, new Branching(false).side); + +console.log("chain:"); +const p4 = new Point4(1, 2, 3, 4); +console.log(p4.x, p4.y, p4.z, p4.w, p4.label, p4 instanceof Point3, p4 instanceof Point); diff --git a/tests/diagnostics/super-comma-nested.ts b/tests/diagnostics/super-comma-nested.ts new file mode 100644 index 000000000..799436ff9 --- /dev/null +++ b/tests/diagnostics/super-comma-nested.ts @@ -0,0 +1,24 @@ +// super(...) as an operand of a statement-level comma expression compiles +// (super-comma-sequence.ts). Any other expression position still refuses: +// the field initializers that run at super() would be misordered. +class Base { + constructor(readonly origin: string) {} +} + +class Selected extends Base { + constructor(left: boolean) { + left ? super("left") : super("right"); + } +} + +function keep(value: T): T { + return value; +} + +class Argument extends Base { + constructor() { + keep((super("argument"), 1)); + } +} + +console.log(new Selected(true).origin, new Argument().origin); diff --git a/tests/harness/__snapshots__/super-comma-nested.ts.txt b/tests/harness/__snapshots__/super-comma-nested.ts.txt new file mode 100644 index 000000000..e7b7f275a --- /dev/null +++ b/tests/harness/__snapshots__/super-comma-nested.ts.txt @@ -0,0 +1,27 @@ +super-comma-nested.ts:9:3 - error SC1090: derived constructors without a lowered super() call is not supported yet + + 8 | class Selected extends Base { + 9 | constructor(left: boolean) { + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 10 | left ? super("left") : super("right"); + +super-comma-nested.ts:10:12 - error SC1090: super() in an expression position is not supported yet + + 9 | constructor(left: boolean) { + 10 | left ? super("left") : super("right"); + | ^~~~~~~~~~~~~ + 11 | } + +super-comma-nested.ts:19:3 - error SC1090: derived constructors without a lowered super() call is not supported yet + + 18 | class Argument extends Base { + 19 | constructor() { + | ^~~~~~~~~~~~~~~ + 20 | keep((super("argument"), 1)); + +super-comma-nested.ts:20:11 - error SC1090: super() in an expression position is not supported yet + + 19 | constructor() { + 20 | keep((super("argument"), 1)); + | ^~~~~~~~~~~~~~~~~ + 21 | } \ No newline at end of file