Compile derived constructors whose super() call leads a comma statement (#1354)

This commit is contained in:
Chris Tate
2026-10-18 20:25:58 -05:00
committed by GitHub
10 changed files with 328 additions and 1 deletions
@@ -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);
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/super-comma-jose-errors.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/super-comma-sequence-js.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/super-comma-sequence.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/super-comma-nested.ts"
],
"diags": []
}
+84
View File
@@ -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);
}
+49
View File
@@ -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);
+101
View File
@@ -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<T>(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);
+24
View File
@@ -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<T>(value: T): T {
return value;
}
class Argument extends Base {
constructor() {
keep((super("argument"), 1));
}
}
console.log(new Selected(true).origin, new Argument().origin);
@@ -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 | }