Revert "Read fields as undefined before their assignment (#1292)"

This reverts commit ff90245b27.
This commit is contained in:
Chris Tate
2026-10-18 12:20:36 -05:00
parent 3c638d5537
commit da1af7437f
10 changed files with 58 additions and 583 deletions
@@ -124,15 +124,6 @@ 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;
@@ -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, mentionsReceiver } from "./construction-escapes.js";
import { constructionEscape } 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,11 +468,6 @@ export interface ClassInfo {
* unassigned read throws the catchable TypeError instead of yielding
* an undefined the declared type cannot hold (SEMANTICS.md). */
deferredInitFields?: Set<string>;
/** 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<string>;
/** 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. */
@@ -3507,54 +3502,55 @@ export function collectClassShapeInner(
const derived =
decl.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword) ??
false;
// 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<string> | 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,
// Generic instances and mixins share layouts across declarations, so
// they only report whether construction exposes the instance.
const escape: { escapes: boolean; assigned: Set<string> | 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,
)
)
)
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
@@ -3610,30 +3606,18 @@ export function collectClassShapeInner(
}
}
// 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<string>(base?.earlyReadFields ?? []);
if (escape.assigned && escape.escapes) {
if (escape.assigned && !inst) {
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 === undefined || !earlyReadable(lowerer, declared)) continue;
// A declared `T | undefined` field already starts undefined.
if (declared.kind === "union" && lowerer.armTag(declared.unionId, UNDEFINED_T) >= 0)
if (declared?.kind !== "object" || lowerer.classes.get(declared.className)?.def.runtime)
continue;
const armed = lowerer.runtimeOptionalType(declared);
if (armed.kind !== "union") continue;
const armed = lowerer.withUndefinedArm(declared);
if (armed === null || armed.kind !== "union") continue;
fields.set(f.name, armed);
f.type = armed;
// 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);
deferredInitFields.add(f.name);
}
}
@@ -3738,43 +3722,6 @@ 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<ts.Statement>();
{
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 =
@@ -3845,15 +3792,8 @@ 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.
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);
}
}
if (base && lowerer.dynConvertible(type) && !sealedPrefix?.created.has(name))
type = DYN;
fields.set(name, type);
fieldOrder.push({ name, type, initializer: undefined });
continue;
@@ -4114,7 +4054,6 @@ 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
@@ -6289,33 +6228,6 @@ function overrideReturnAbi(
return declared;
}
/** Field kinds whose early reads yield the undefined value (earlyReadFields). */
const PRIMITIVE_EARLY_READS: ReadonlySet<IrType["kind"]> = 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(
@@ -7753,11 +7665,7 @@ function paramPropInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLoca
obj: { kind: "varRef", localId: thisLocal.id, type: thisType, loc },
className: info.def.name,
field: pp.name,
// 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,
),
value: { kind: "varRef", localId: local!.id, type: local!.type, loc },
loc,
});
}
@@ -3161,51 +3161,6 @@ 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") &&
@@ -3416,25 +3371,6 @@ 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).
@@ -15465,20 +15401,6 @@ 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 {
@@ -16019,22 +15941,6 @@ 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) {
@@ -3976,18 +3976,6 @@ 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;
@@ -1,93 +0,0 @@
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<IrModule> {
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");
});
@@ -1,6 +0,0 @@
{
"order": [
"<repo>/tests/corpus/5943-fields-read-before-assignment.ts"
],
"diags": []
}
@@ -1,6 +0,0 @@
{
"order": [
"<repo>/tests/corpus/5944-js-fields-read-before-assignment.js"
],
"diags": []
}
@@ -1,181 +0,0 @@
// @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<string, number>;
tags: Set<string>;
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<T> 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<number>(3).show();
new Box<string>("s").show();
type Ctor<T = {}> = new (...args: any[]) => T;
class Viewer {
constructor() {
this.show();
}
show(): void {}
}
function Labeled<B extends Ctor<Viewer>>(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());
@@ -1,28 +0,0 @@
// 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();
-4
View File
@@ -2160,8 +2160,6 @@
"5940-callback-helper-specialization.ts": 3109,
"5941-callback-specialization-recursion.ts": 3112,
"5942-callback-specialization-throws.ts": 3221,
"5943-fields-read-before-assignment.ts": 3902,
"5944-js-fields-read-before-assignment.js": 2868,
"5945-local-class-observed-methods.ts": 2284,
"5946-nested-record-field-optional-writes.ts": 2987,
"5950-tuple-predicate-methods.ts": 4611,
@@ -5173,8 +5171,6 @@
"5940-callback-helper-specialization.ts": 4548,
"5941-callback-specialization-recursion.ts": 4448,
"5942-callback-specialization-throws.ts": 3443,
"5943-fields-read-before-assignment.ts": 4319,
"5944-js-fields-read-before-assignment.js": 3945,
"5945-local-class-observed-methods.ts": 4458,
"5946-nested-record-field-optional-writes.ts": 3537,
"5950-tuple-predicate-methods.ts": 4018,