From 18033dc42dbb72f30b8e6b5b9ea061e277152329 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 17:45:49 -0700 Subject: [PATCH] Compile derived constructors whose super() call leads a comma statement Minifiers join a derived constructor's statements into one comma expression (`super(message, options), this.name = ..., ...;`), which jose 6.2.12 ships in its error classes. The statement lowering already runs each operand of a statement-level comma as its own statement, so super() is accepted when only parentheses and comma operators separate it from its expression statement. Every other expression position keeps the SC1090 refusal. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-classes.ts | 20 +++- .../corpus/super-comma-jose-errors.js.json | 6 ++ .../corpus/super-comma-sequence-js.js.json | 6 ++ .../tests/corpus/super-comma-sequence.ts.json | 6 ++ .../diagnostics/super-comma-nested.ts.json | 6 ++ tests/corpus/super-comma-jose-errors.js | 84 +++++++++++++++ tests/corpus/super-comma-sequence-js.js | 49 +++++++++ tests/corpus/super-comma-sequence.ts | 101 ++++++++++++++++++ tests/diagnostics/super-comma-nested.ts | 24 +++++ .../__snapshots__/super-comma-nested.ts.txt | 27 +++++ 10 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-jose-errors.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-sequence-js.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/super-comma-sequence.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/super-comma-nested.ts.json create mode 100644 tests/corpus/super-comma-jose-errors.js create mode 100644 tests/corpus/super-comma-sequence-js.js create mode 100644 tests/corpus/super-comma-sequence.ts create mode 100644 tests/diagnostics/super-comma-nested.ts create mode 100644 tests/harness/__snapshots__/super-comma-nested.ts.txt diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index cddc536c6..3838f067f 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -7705,6 +7705,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. */ @@ -7722,7 +7740,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