mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Revert "Read fields as undefined before their assignment (#1292)"
This reverts commit ff90245b27.
This commit is contained in:
@@ -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");
|
||||
});
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5943-fields-read-before-assignment.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
-6
@@ -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();
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user