diff --git a/packages/compiler/src/frontend/lowering/construction-escapes.ts b/packages/compiler/src/frontend/lowering/construction-escapes.ts index 484920a1c..a1626f12d 100644 --- a/packages/compiler/src/frontend/lowering/construction-escapes.ts +++ b/packages/compiler/src/frontend/lowering/construction-escapes.ts @@ -124,6 +124,15 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca continue; } if (ts.isExpressionStatement(stmt) && !exposes(stmt.expression, depth)) continue; + // A check that only reads assigned fields and throws or does nothing + // with the receiver (`if (this.size < 0) throw new RangeError()`) + // does not expose the instance; assignments in its branches are not + // definite and still count as exposures. + if ( + (ts.isIfStatement(stmt) || ts.isThrowStatement(stmt) || ts.isBlock(stmt)) && + !exposes(stmt, depth) + ) + continue; return false; } return true; diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 06739166c..7a98096b7 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -143,7 +143,7 @@ import { lowerObjectFactoryNew } from "./object-factory-new.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { lowerInstanceConstructorNew } from "./class-instance-constructor.js"; import { classPrototypeData } from "./class-prototypes.js"; -import { constructionEscape } from "./construction-escapes.js"; +import { constructionEscape, mentionsReceiver } from "./construction-escapes.js"; import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js"; import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js"; import { reflectedClassStaticMethodValue } from "./class-method-values.js"; @@ -468,6 +468,11 @@ export interface ClassInfo { * unassigned read throws the catchable TypeError instead of yielding * an undefined the declared type cannot hold (SEMANTICS.md). */ deferredInitFields?: Set; + /** Fields (inherited included) that code construction exposes the + * instance to can read before their assignment: the slot is the + * undefined-armed union, allocation writes undefined, and reads yield the + * union value, so an early read sees undefined as in Node. */ + earlyReadFields?: Set; /** True when construction may hand the instance to other code (a method * call, an argument, a callback, or a base constructor that does) — a * subclass's own fields can then be read before their assignment. */ @@ -3473,55 +3478,54 @@ export function collectClassShapeInner( const derived = decl.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword) ?? false; - // Generic instances and mixins share layouts across declarations, so - // they only report whether construction exposes the instance. - const escape: { escapes: boolean; assigned: Set | null } = - familyMode || mixin - ? { escapes: derived, assigned: null } - : constructionEscape({ - ctor, - fields: fieldOrder.flatMap((f) => - paramProps.some((pp) => pp.name === f.name) - ? [] - : [{ name: f.name, initializer: f.initializer }], - ), - paramProps: paramProps.map((pp) => pp.name), - derived, - baseEscapes: - base === null || - callableBase !== undefined || - factoryBaseExpression !== undefined || - base.constructionEscapes === true, - isField: (name) => - fields.has(name) && - !methods.has(`set:${name}`) && - !lowerer.findMethodOn(base, `set:${name}`), - privateMethodBody: (call) => { - const callee = call.expression; - if (!ts.isPropertyAccessExpression(callee)) return null; - const name = callee.name.text; - const declaration = decl.members.find( - (member): member is ts.MethodDeclaration => - ts.isMethodDeclaration(member) && - member.body !== undefined && - (ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) && - member.name.text === name, - ); - if (!declaration?.body || declaration.asteriskToken) return null; - const modifiers = ts.getModifiers(declaration) ?? []; - if ( - modifiers.some( - (m) => - m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword, - ) + // A generic family is not emitted; its instances and each mixin + // application analyze their own construction with their actual base. + const escape: { escapes: boolean; assigned: Set | null } = familyMode + ? { escapes: derived, assigned: null } + : constructionEscape({ + ctor, + fields: fieldOrder.flatMap((f) => + paramProps.some((pp) => pp.name === f.name) + ? [] + : [{ name: f.name, initializer: f.initializer }], + ), + paramProps: paramProps.map((pp) => pp.name), + derived, + baseEscapes: + base === null || + callableBase !== undefined || + factoryBaseExpression !== undefined || + base.constructionEscapes === true, + isField: (name) => + fields.has(name) && + !methods.has(`set:${name}`) && + !lowerer.findMethodOn(base, `set:${name}`), + privateMethodBody: (call) => { + const callee = call.expression; + if (!ts.isPropertyAccessExpression(callee)) return null; + const name = callee.name.text; + const declaration = decl.members.find( + (member): member is ts.MethodDeclaration => + ts.isMethodDeclaration(member) && + member.body !== undefined && + (ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) && + member.name.text === name, + ); + if (!declaration?.body || declaration.asteriskToken) return null; + const modifiers = ts.getModifiers(declaration) ?? []; + if ( + modifiers.some( + (m) => + m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword, ) - return null; - const isPrivate = - ts.isPrivateIdentifier(declaration.name) || - modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword); - return isPrivate ? declaration.body : null; - }, - }); + ) + return null; + const isPrivate = + ts.isPrivateIdentifier(declaration.name) || + modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword); + return isPrivate ? declaration.body : null; + }, + }); // The deferred definite-assignment check: a field on the unguarded // list passes only with an unconditional `this.x = ...` at the // constructor's TOP LEVEL — the same standard the JS-class path @@ -3577,18 +3581,30 @@ export function collectClassShapeInner( } } - if (escape.assigned && !inst) { + // Fields assigned after construction can expose the instance (a method + // call, a base constructor that calls an override) can be read while + // Node still holds undefined. Their slots admit undefined, allocation + // writes it, and reads yield it, as in Node. + const earlyReadFields = new Set(base?.earlyReadFields ?? []); + if (escape.assigned && escape.escapes) { for (const f of fieldOrder) { if (f.redeclared || escape.assigned.has(f.name) || deferredInitFields.has(f.name)) continue; - if (paramProps.some((pp) => pp.name === f.name)) continue; const declared = fields.get(f.name); - if (declared?.kind !== "object" || lowerer.classes.get(declared.className)?.def.runtime) + if (declared === undefined || !earlyReadable(lowerer, declared)) continue; + // A declared `T | undefined` field already starts undefined. + if (declared.kind === "union" && lowerer.armTag(declared.unionId, UNDEFINED_T) >= 0) continue; - const armed = lowerer.withUndefinedArm(declared); - if (armed === null || armed.kind !== "union") continue; + const armed = lowerer.runtimeOptionalType(declared); + if (armed.kind !== "union") continue; fields.set(f.name, armed); f.type = armed; - deferredInitFields.add(f.name); + // Primitive and union slots yield the undefined value itself. + // Reference slots keep the deferred-init reads: consumers that + // observe undefined (printing, equality, typeof, nullish defaults) + // see it, and member access on it throws a TypeError, as in Node. + if (declared.kind === "union" || PRIMITIVE_EARLY_READS.has(declared.kind)) + earlyReadFields.add(f.name); + else deferredInitFields.add(f.name); } } @@ -3693,6 +3709,43 @@ export function collectClassShapeInner( (name) => !hasAccessor(name) && !methods.has(name) && !lowerer.findMethodOn(base, name), ) : null; + // Constructor statements that run after the instance may have been + // exposed (a method call, `this` passed along, a base constructor + // that exposes it): a field they create can be read before its + // assignment, while Node holds undefined. + const exposedBefore = new Set(); + { + let exposed = false; + for (const stmt of ctor?.body?.statements ?? []) { + const expression = ts.isExpressionStatement(stmt) ? stmt.expression : null; + if ( + expression && + ts.isCallExpression(expression) && + expression.expression.kind === ts.SyntaxKind.SuperKeyword + ) { + if (base?.constructionEscapes === true || expression.arguments.some(mentionsReceiver)) + exposed = true; + continue; + } + const assigned = + expression && + ts.isBinaryExpression(expression) && + expression.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(expression.left) && + expression.left.expression.kind === ts.SyntaxKind.ThisKeyword + ? expression + : null; + if (exposed || (assigned && mentionsReceiver(assigned.right))) exposedBefore.add(stmt); + const plainAssignment = + assigned !== null && + ts.isPropertyAccessExpression(assigned.left) && + !mentionsReceiver(assigned.right) && + !hasAccessor(assigned.left.name.text) && + !methods.has(assigned.left.name.text) && + !lowerer.findMethodOn(base, assigned.left.name.text); + if (!plainAssignment && mentionsReceiver(stmt)) exposed = true; + } + } if (ctor?.body) { for (const stmt of ctor.body.statements) { const lhs = @@ -3763,8 +3816,15 @@ export function collectClassShapeInner( type = inheritedJsFieldType(lowerer, decl, name, type); // super() can invoke a subclass setter or method before its // constructor assignments run. These slots start undefined. - if (base && lowerer.dynConvertible(type) && !sealedPrefix?.created.has(name)) - type = DYN; + const sealed = sealedPrefix?.created.has(name) === true; + if (base && lowerer.dynConvertible(type) && !sealed) type = DYN; + if (exposedBefore.has(stmt) && !sealed && earlyReadable(lowerer, type)) { + const armed = lowerer.runtimeOptionalType(type); + if (armed.kind === "union") { + type = armed; + earlyReadFields.add(name); + } + } fields.set(name, type); fieldOrder.push({ name, type, initializer: undefined }); continue; @@ -4025,6 +4085,7 @@ export function collectClassShapeInner( ? { classDecorators: { nodes: classDecoratorNodes } } : {}), ...(deferredInitFields.size > 0 ? { deferredInitFields } : {}), + ...(earlyReadFields.size > 0 ? { earlyReadFields } : {}), ...(escape.escapes ? { constructionEscapes: true as const } : {}), }; // GENERIC members get their declaring-class backlink now that the @@ -6199,6 +6260,33 @@ function overrideReturnAbi( return declared; } +/** Field kinds whose early reads yield the undefined value (earlyReadFields). */ +const PRIMITIVE_EARLY_READS: ReadonlySet = new Set([ + "f64", + "bool", + "string", + "bigint", +]); + +/** Field types whose slot can admit undefined for reads before the field's + * assignment. Date values are scalars, so their unassigned slot is memory + * safe; checked-dynamic slots already hold undefined. */ +function earlyReadable(lowerer: Lowerer, type: IrType): boolean { + if (isUnitType(type)) return false; + switch (type.kind) { + case "dyn": + case "void": + case "date": + case "generator": + case "caught": + return false; + case "object": + return lowerer.classes.get(type.className)?.def.runtime !== true; + default: + return true; + } +} + /** The nearest declaration of `name` at or above `info` — the method a * receiver of that static class runs when nothing below overrides it. */ export function findMethodOn( @@ -7628,7 +7716,11 @@ function paramPropInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLoca obj: { kind: "varRef", localId: thisLocal.id, type: thisType, loc }, className: info.def.name, field: pp.name, - value: { kind: "varRef", localId: local!.id, type: local!.type, loc }, + // An early-readable field's slot admits undefined (earlyReadFields). + value: lowerer.coerceToExpected( + { kind: "varRef", localId: local!.id, type: local!.type, loc }, + info.fields.get(pp.name) ?? local!.type, + ), loc, }); } diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d2d990e16..8777cb960 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -3133,6 +3133,51 @@ export function lowerTernary( * the same union (unnarrowed use), to a SUB-union (partial narrowing — * unrepresentable without a re-tag), or to nothing (`never` in an * exhaustive default) leaves the expression union-typed. */ +/** Uses that read an early-read field's value as Node does when it is + * undefined: printing, templates, operators, tests, member access, calls of + * program functions (whose parameters the optional-value analysis widens), + * and stores into bindings, fields and returns. Other uses, such as library + * arguments, extract the declared type with a check. */ +function observesEarlyRead(node: ts.Node): boolean { + let child = node; + let parent = node.parent; + while (parent && ts.isParenthesizedExpression(parent)) { + child = parent; + parent = parent.parent; + } + if (!parent) return false; + if ( + ts.isTemplateSpan(parent) || + ts.isBinaryExpression(parent) || + ts.isPrefixUnaryExpression(parent) || + ts.isPostfixUnaryExpression(parent) || + ts.isTypeOfExpression(parent) || + ts.isConditionalExpression(parent) || + ts.isIfStatement(parent) || + ts.isWhileStatement(parent) || + ts.isReturnStatement(parent) || + ts.isVariableDeclaration(parent) || + ts.isPropertyAssignment(parent) || + ts.isArrayLiteralExpression(parent) || + (ts.isPropertyAccessExpression(parent) && parent.expression === child) || + (ts.isElementAccessExpression(parent) && parent.expression === child) + ) + return true; + if (ts.isCallExpression(parent) && parent.arguments.includes(child as ts.Expression)) { + const callee = parent.expression; + if ( + ts.isPropertyAccessExpression(callee) && + ts.isIdentifier(callee.expression) && + callee.expression.text === "console" + ) + return true; + return ( + !ts.isPropertyAccessExpression(callee) || callee.expression.kind === ts.SyntaxKind.ThisKeyword + ); + } + return false; +} + export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrExpr { if ( (expr.type.kind === "union" || expr.type.kind === "dyn") && @@ -3343,6 +3388,25 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx if (expr.type.kind !== "union") return expr; if (ts.isPropertyAccessExpression(node) && isOptionalProcessStreamProperty(lowerer, node)) return expr; + // A field read before its assignment holds undefined although its + // declared type omits it; the declared type is no narrowing proof. A flow + // narrowing past the declared type (`if (this.x !== null)`) extracts the + // narrowed arm with a check, since the slot may still hold undefined. + if ( + expr.kind === "fieldGet" && + lowerer.classes.get(expr.className)?.earlyReadFields?.has(expr.field) === true + ) { + const symbol = ts.isPropertyAccessExpression(node) + ? lowerer.checker.getSymbolAtLocation(node.name) + : undefined; + const declared = symbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(symbol)) : null; + const narrowedArm = lowerer.mapTypeOf(lowerer.typeOf(node)); + if (narrowedArm === null || narrowedArm.kind === "union") return expr; + if (declared !== null && typeEquals(declared, narrowedArm) && observesEarlyRead(node)) + return expr; + const helper = lowerer.narrowedArmHelper(expr.type.unionId, narrowedArm, expr.loc); + return helper ? lowerer.checkedNarrowCall(helper, expr, narrowedArm, expr.loc) : expr; + } // Represented fields and index values can include undefined even when // checker inference or lib.d.ts declares strings (RegExp captures). @@ -15103,6 +15167,20 @@ export function fieldGetExpr( const helper = lowerer.deferredReadHelper(target.fieldType.unionId, inner, loc); if (helper) return { kind: "call", callee: helper, args: [read], type: inner, loc }; } + // An early-read field (ClassInfo.earlyReadFields) yields its undefined + // where the use reads it as Node does; elsewhere the declared type is + // extracted with a check. + if ( + target.fieldType.kind === "union" && + lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true && + !observesEarlyRead(blame) + ) { + const declared = lowerer.mapTypeOf(lowerer.typeOf(blame)); + if (declared && declared.kind !== "union") { + const helper = lowerer.narrowedArmHelper(target.fieldType.unionId, declared, loc); + if (helper) return lowerer.checkedNarrowCall(helper, read, declared, loc); + } + } return read; } return { @@ -15643,6 +15721,22 @@ function lowerFieldCompoundValue( if (op === "+" && read.type.kind === "string") { appended = lowerer.ensureString(rhs, rhsNode ?? access); value = { kind: "strConcat", left: read, right: appended, type: STRING, loc }; + } else if ( + op === "+" && + read.type.kind === "union" && + lowerer.stripUndefinedArm(read.type).kind === "string" && + target.container === "class" && + lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true + ) { + // A string field read before its assignment holds undefined, which + // concatenates as "undefined". + value = { + kind: "strConcat", + left: lowerer.ensureString(read, access), + right: lowerer.ensureString(rhs, rhsNode ?? access), + type: STRING, + loc, + }; } else if (numericRead.type.kind === "f64" && numericRhs.type.kind === "f64") { value = { kind: "bin", op, left: numericRead, right: numericRhs, type: F64, loc }; } else if (bigintValue) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 087b4e628..eeba5c8fb 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3933,6 +3933,18 @@ export class Lowerer { if (!s) return null; return s.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(s) : s; }; + // Fields that can be read before their assignment hold undefined then; + // their reads are runtime-optional values like missing array reads. + for (const info of this.classes.values()) { + if (!info.earlyReadFields || !info.decl?.name) continue; + const classSymbol = symbolOf(info.decl.name); + if (!classSymbol) continue; + const instance = this.checker.getDeclaredTypeOfSymbol(classSymbol); + for (const name of info.earlyReadFields) { + const property = this.checker.getPropertyOfType(instance, name); + if (property) optionalSymbols.add(property); + } + } const signatureOf = (node: ts.Identifier): FnSig | null => { const symbol = symbolOf(node); if (!symbol) return null; diff --git a/packages/compiler/test/class-early-field-reads.test.ts b/packages/compiler/test/class-early-field-reads.test.ts new file mode 100644 index 000000000..c602a2f42 --- /dev/null +++ b/packages/compiler/test/class-early-field-reads.test.ts @@ -0,0 +1,93 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule, validateModule } from "../src/index.js"; +import type { IrModule } from "../src/ir/ir.js"; + +async function lower(source: string, file = "main.ts"): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-early-field-reads-")); + try { + const entry = join(dir, file), + output = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n")); + const module = deserializeModule(await readFile(output, "utf8")); + expect(validateModule(module)).toEqual([]); + return module; + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +/** Field name → IR kind for a class. */ +function fields(module: IrModule, name: string): string { + const cls = module.classes.find((candidate) => candidate.name === name); + expect(cls, name).toBeDefined(); + return cls!.fields + .filter((field) => !field.name.startsWith("%")) + .map((field) => `${field.name}:${field.type.kind}`) + .join(" "); +} + +test("fields assigned after construction can call an override admit undefined", async () => { + const module = await lower(` +abstract class Base { + constructor() { + this.describe(); + } + abstract describe(): void; +} +class Panel extends Base { + label: string; + width: number; + count = 0; + constructor() { + super(); + this.label = "panel"; + this.width = 2; + } + describe(): void { + console.log(this.label, this.width, this.count); + } +} +class Plain { + label: string; + width: number; + constructor() { + this.label = "plain"; + this.width = 1; + this.describe(); + } + describe(): void { + console.log(this.label, this.width); + } +} +new Panel(); +new Plain(); +`); + expect(fields(module, "Panel")).toBe("label:union width:union count:union"); + expect(fields(module, "Plain")).toBe("label:string width:f64"); +}); + +test("JavaScript fields created after the constructor calls a method admit undefined", async () => { + const module = await lower( + ` +class Widget { + constructor() { + this.ready = true; + this.render(); + this.title = "widget"; + } + render() { + console.log(this.ready, this.title); + } +} +new Widget(); +`, + "main.js", + ); + expect(fields(module, "Widget")).toBe("ready:bool title:union"); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5943-fields-read-before-assignment.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5943-fields-read-before-assignment.ts.json new file mode 100644 index 000000000..d2dcdcc24 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5943-fields-read-before-assignment.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5943-fields-read-before-assignment.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5944-js-fields-read-before-assignment.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5944-js-fields-read-before-assignment.js.json new file mode 100644 index 000000000..e9800b71d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5944-js-fields-read-before-assignment.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5944-js-fields-read-before-assignment.js" + ], + "diags": [] +} diff --git a/tests/corpus/5943-fields-read-before-assignment.ts b/tests/corpus/5943-fields-read-before-assignment.ts new file mode 100644 index 000000000..922b3c34d --- /dev/null +++ b/tests/corpus/5943-fields-read-before-assignment.ts @@ -0,0 +1,181 @@ +// @transform-types +// Fields that a base constructor (or the constructor itself) can read +// through an override or a method call before they are assigned hold +// undefined then, as in Node, for every field kind. After construction +// the fields read and update normally. +class Point { + x: number; + constructor(x: number) { + this.x = x; + } +} +function show(label: string, value: number): string { + return `${label}=${value}`; +} +abstract class Widget { + log: string[] = []; + constructor() { + this.describe("base"); + } + abstract describe(when: string): void; +} +class Panel extends Widget { + label: string; + width: number; + visible: boolean; + origin: Point; + items: number[]; + options: { depth: number }; + key: string | number; + index: Map; + tags: Set; + count = 5; + constructor(label: string, readonly id: string) { + super(); + this.label = label; + this.width = 10; + this.visible = true; + this.origin = new Point(1); + this.items = [1, 2]; + this.options = { depth: 2 }; + this.key = 7; + this.index = new Map([["a", 1]]); + this.tags = new Set(["t"]); + this.describe("ctor"); + } + describe(when: string): void { + console.log(when, this.label, this.width, this.visible, this.origin, this.items, this.options, this.key, this.count, this.id); + console.log(when, `${this.label}:${this.width}`, this.width + 1, this.count * 2, this.label === undefined, typeof this.items, this.origin?.x, this.options?.depth ?? -1, this.index?.size, this.tags?.has("t")); + console.log(when, show(this.label, this.width), String(this.key), [this.label].length); + try { + console.log(this.label.length, this.items.length); + } catch (error) { + console.log(when, (error as Error).message); + } + } + grow(): number { + this.count++; + this.width += 5; + this.label += "!"; + this.items.push(this.width); + return this.count + this.width + this.items.length; + } +} +const panel = new Panel("main", "p1"); +console.log(panel.grow(), panel.label, panel.items, panel.count, panel.width); + +class Plain { + total: number; + name: string; + constructor() { + this.total = 1; + this.name = "plain"; + this.bump(); + } + bump(): void { + this.total++; + } +} +const plain = new Plain(); +console.log(plain.total, plain.name); + +class Toucher { + constructor() { + this.touch(); + } + touch(): void {} +} +class Counter extends Toucher { + count = 0; + label: string; + items: string[]; + constructor() { + super(); + this.label = "c"; + this.items = []; + } + override touch(): void { + this.count++; + this.label += "x"; + console.log(this.count, this.label, Number.isNaN(this.count)); + try { + this.items.push("a"); + } catch (error) { + console.log(error instanceof TypeError); + } + } +} +const c = new Counter(); +c.touch(); +console.log(c.count, c.label, c.items); + +class Shower { + constructor() { + this.show(); + } + show(): void {} +} +class Box extends Shower { + label: string; + value: T; + constructor(value: T) { + super(); + this.label = "box"; + this.value = value; + } + override show(): void { + console.log("show", this.label, this.value); + } +} +new Box(3).show(); +new Box("s").show(); + +type Ctor = new (...args: any[]) => T; +class Viewer { + constructor() { + this.show(); + } + show(): void {} +} +function Labeled>(Parent: B) { + return class extends Parent { + label = "mixed"; + override show(): void { + console.log("show", this.label); + } + }; +} +const L = Labeled(Viewer); +new L().show(); + +interface Crash { loc: number; message: string } +class Setupper { + constructor() { + this.setup(); + } + setup(): void {} +} +class Host extends Setupper { + crash: Crash | null; + name: string | null; + constructor(c: Crash | null) { + super(); + this.crash = c; + this.name = "host"; + } + override setup(): void { + console.log("setup", this.crash, this.name); + if (this.crash !== null) { + try { + console.log(this.crash.loc); + } catch (error) { + console.log(error instanceof TypeError); + } + } + } + report(): string { + if (this.crash) return `${this.crash.message} at ${this.crash.loc}`; + return this.name === null ? "none" : this.name.toUpperCase(); + } +} +console.log(new Host({ loc: 3, message: "boom" }).report(), new Host(null).report()); diff --git a/tests/corpus/5944-js-fields-read-before-assignment.js b/tests/corpus/5944-js-fields-read-before-assignment.js new file mode 100644 index 000000000..e313a12b0 --- /dev/null +++ b/tests/corpus/5944-js-fields-read-before-assignment.js @@ -0,0 +1,28 @@ +// JavaScript class fields that the constructor creates after it calls a +// method, or after a base constructor that does, hold undefined until +// they are assigned, as in Node. +class Widget { + constructor(name) { + this.render(); + this.name = name; + this.size = 3; + this.title = "widget"; + this.parts = ["a"]; + this.meta = { id: 1 }; + } + render() { + console.log("render", this.name, this.title, this.size, this.parts, this.meta, this.size + 1, `${this.name}!`); + } +} +class Big extends Widget { + constructor() { + super("big"); + this.extra = 2; + } + render() { + super.render(); + console.log("big", this.extra); + } +} +new Widget("w").render(); +new Big().render(); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 2c80c39a2..a3f845cf8 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1869,6 +1869,8 @@ "5842-qs-parse-null-prototype.ts": 1774, "5843-qs-parse-null-prototype-js.mjs": 1559, "5844-date-unknown-values.ts": 3969, + "5846-events-esm-default-max.ts": 2633, + "5847-events-cjs-default-max.cjs": 2823, "5857-regexp-unicode-sets.ts": 3350, "5858-disposable-stack.ts": 3050, "5859-disposable-stack-js.js": 3100, @@ -1899,6 +1901,8 @@ "5923-matchall-groups-js.js": 3970, "5939-js-let-object-reassigned-wider.js": 1945, "5942-local-class-observed-methods.ts": 5063, + "5943-fields-read-before-assignment.ts": 6856, + "5944-js-fields-read-before-assignment.js": 4247, "5950-tuple-predicate-methods.ts": 5990, "5951-result-never-widening.ts": 4710, "5955-any-promise-methods.js": 3940, @@ -1913,8 +1917,6 @@ "5990-worker-uncloneable.mjs": 3283, "5991-worker-async-dispose.mjs": 5699, "5992-worker-thread-name-lifetime.mjs": 2687, - "5846-events-esm-default-max.ts": 2633, - "5847-events-cjs-default-max.cjs": 2823, "600-closures-basic.ts": 2349, "601-closures-loops.ts": 1788, "602-closures-identity-recursion.ts": 2372, @@ -4569,6 +4571,8 @@ "5842-qs-parse-null-prototype.ts": 7077, "5843-qs-parse-null-prototype-js.mjs": 5395, "5844-date-unknown-values.ts": 4543, + "5846-events-esm-default-max.ts": 3102, + "5847-events-cjs-default-max.cjs": 3039, "5857-regexp-unicode-sets.ts": 3900, "5858-disposable-stack.ts": 3750, "5859-disposable-stack-js.js": 3600, @@ -4599,6 +4603,8 @@ "5923-matchall-groups-js.js": 6650, "5939-js-let-object-reassigned-wider.js": 4436, "5942-local-class-observed-methods.ts": 6212, + "5943-fields-read-before-assignment.ts": 72293, + "5944-js-fields-read-before-assignment.js": 40310, "5950-tuple-predicate-methods.ts": 7790, "5951-result-never-widening.ts": 6120, "5955-any-promise-methods.js": 5120, @@ -4613,8 +4619,6 @@ "5990-worker-uncloneable.mjs": 22432, "5991-worker-async-dispose.mjs": 25768, "5992-worker-thread-name-lifetime.mjs": 28932, - "5846-events-esm-default-max.ts": 3102, - "5847-events-cjs-default-max.cjs": 3039, "600-closures-basic.ts": 2534, "601-closures-loops.ts": 2297, "602-closures-identity-recursion.ts": 2427,