mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Check cycle bindings only where early code can reach the reading function (#1882)
This commit is contained in:
@@ -371,15 +371,60 @@ test("repeated cycle edges reuse a file's binding index", () => {
|
||||
|
||||
test.for([
|
||||
[
|
||||
// A shared instance built by a user constructor: the reads of it in
|
||||
// the partner's function can run early; the class cannot.
|
||||
// A shared instance built by a user constructor that calls the
|
||||
// partner's function: its read of the instance can run early.
|
||||
{
|
||||
"main.ts": 'import { shared } from "./a.ts"; console.log(shared.n);',
|
||||
"a.ts":
|
||||
'import { f } from "./b.ts"; export class C { n = 1; constructor() { console.log(f()); } } export const shared = new C();',
|
||||
"b.ts": 'import { shared } from "./a.ts"; export function f(): number { return shared.n; }',
|
||||
},
|
||||
["shared"],
|
||||
],
|
||||
[
|
||||
// User code runs while the instance is built, but nothing it reaches
|
||||
// calls the partner's function before the declaration completed.
|
||||
{
|
||||
"main.ts": 'import { shared } from "./a.ts"; console.log(shared.n);',
|
||||
"a.ts":
|
||||
'import { f } from "./b.ts"; export class C { n = 1; } export const shared = new C(); console.log(f());',
|
||||
"b.ts": 'import { shared } from "./a.ts"; export function f(): number { return shared.n; }',
|
||||
},
|
||||
["shared"],
|
||||
[],
|
||||
],
|
||||
[
|
||||
// The partner's top level runs first and calls a hoisted function of
|
||||
// the importer that reads a const declared later; a second function
|
||||
// reads another const but nothing calls it early.
|
||||
{
|
||||
"main.ts": 'import { late } from "./a.ts"; console.log(late());',
|
||||
"a.ts":
|
||||
'import { probe } from "./b.ts"; export function read(): string { return name; } export function late(): string { return other + probe; } export const name = "n"; export const other = "o";',
|
||||
"b.ts":
|
||||
'import { read } from "./a.ts"; function attempt(): string { try { return read(); } catch { return "tdz"; } } export const probe = attempt();',
|
||||
},
|
||||
["name"],
|
||||
],
|
||||
[
|
||||
// The partner's class calls a method through `this` in a static
|
||||
// initializer; the method reads a const of the importer declared later.
|
||||
{
|
||||
"main.ts": 'import { name } from "./a.ts"; console.log(name);',
|
||||
"a.ts": 'import { Probe } from "./b.ts"; export const name = "n"; console.log(Probe.value);',
|
||||
"b.ts":
|
||||
'import { name } from "./a.ts"; export class Probe { static read(): string { return name; } static value = this.read(); }',
|
||||
},
|
||||
["name"],
|
||||
],
|
||||
[
|
||||
// The same through a static block of a class stored by a const.
|
||||
{
|
||||
"main.ts": 'import { name } from "./a.ts"; console.log(name);',
|
||||
"a.ts": 'import { Box } from "./b.ts"; export const name = "n"; console.log(Box.value);',
|
||||
"b.ts":
|
||||
'import { name } from "./a.ts"; export const Box = class { static value = ""; static read(): string { return name; } static { this.value = this.read(); } };',
|
||||
},
|
||||
["name"],
|
||||
],
|
||||
[
|
||||
// The partner's top level reads the importer's const before it ran.
|
||||
|
||||
@@ -3903,22 +3903,7 @@ function computeCycleEarlyBindings7(
|
||||
const evaluation = known
|
||||
? comp.slice().sort((a, b) => position.get(a)! - position.get(b)!)
|
||||
: comp.slice();
|
||||
interface Candidate {
|
||||
symbol: ts.Symbol;
|
||||
name: ts.Identifier;
|
||||
kind: CycleEarlyBinding["kind"];
|
||||
module: ts.SourceFile;
|
||||
/** User code can run between the cycle's start and the end of the
|
||||
* declaration (always true when the order is unknown). */
|
||||
exposed: boolean;
|
||||
rank: number;
|
||||
start: number;
|
||||
end: number;
|
||||
reference: ts.Identifier | null;
|
||||
folded: ts.Identifier | null;
|
||||
localNames: Map<string, string>;
|
||||
}
|
||||
const candidates = new Map<ts.Symbol, Candidate>();
|
||||
const candidates = new Map<ts.Symbol, CycleCandidate7>();
|
||||
const names = new Set<string>();
|
||||
let dirty = !known;
|
||||
evaluation.forEach((m, rank) => {
|
||||
@@ -3955,6 +3940,7 @@ function computeCycleEarlyBindings7(
|
||||
start: ts.tokenStartOfNode(stmt),
|
||||
end: stmt.end,
|
||||
reference: null,
|
||||
deferredReference: null,
|
||||
folded: null,
|
||||
localNames: new Map(),
|
||||
});
|
||||
@@ -4023,15 +4009,27 @@ function computeCycleEarlyBindings7(
|
||||
ref.end <= c.end
|
||||
)
|
||||
continue;
|
||||
if (inDeferredPosition7(ref)) {
|
||||
if (c.exposed && c.deferredReference === null) c.deferredReference = ref;
|
||||
continue;
|
||||
}
|
||||
let early: boolean;
|
||||
if (inDeferredPosition7(ref)) early = c.exposed;
|
||||
else if (sf === c.module) early = ts.tokenStartOfNode(ref) < c.start;
|
||||
if (sf === c.module) early = ts.tokenStartOfNode(ref) < c.start;
|
||||
else if (!memberSet.has(sf) || !rankOf.has(sf)) early = false;
|
||||
else early = !known || rankOf.get(sf)! < c.rank;
|
||||
if (early) c.reference = ref;
|
||||
}
|
||||
}
|
||||
// With a static evaluation order, a deferred reference only runs early
|
||||
// when the code that runs before the declaration completes can reach
|
||||
// the function holding it.
|
||||
const reached =
|
||||
known && [...candidates.values()].some((c) => c.reference === null && c.deferredReference)
|
||||
? cycleReachedDeferredReferences7(program, evaluation, candidates)
|
||||
: null;
|
||||
for (const c of candidates.values()) {
|
||||
if (c.reference === null && c.deferredReference !== null)
|
||||
c.reference = reached === null ? c.deferredReference : (reached.get(c.symbol) ?? null);
|
||||
if (c.reference === null) continue;
|
||||
const settled = new Set<ts.SourceFile>(order.filter((sf) => !rankOf.has(sf)));
|
||||
if (known) for (const m of evaluation.slice(c.rank + 1)) settled.add(m);
|
||||
@@ -4050,6 +4048,299 @@ function computeCycleEarlyBindings7(
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Collects into `units` the callables an initializer expression stores
|
||||
* (see moduleEarlyBindingsOf7), recording in `immediate` the parts of a
|
||||
* stored node that run where it is evaluated. */
|
||||
function storedCallables7(
|
||||
checker: ts.TypeChecker,
|
||||
e: ts.Expression,
|
||||
units: ts.Node[],
|
||||
immediate: Map<ts.Node, ts.Node[]>,
|
||||
): void {
|
||||
while (
|
||||
ts.isParenthesizedExpression(e) ||
|
||||
ts.isAsExpression(e) ||
|
||||
ts.isSatisfiesExpression(e) ||
|
||||
ts.isNonNullExpression(e)
|
||||
)
|
||||
e = e.expression;
|
||||
if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) {
|
||||
units.push(e);
|
||||
} else if (ts.isClassExpression(e)) {
|
||||
units.push(e);
|
||||
immediate.set(e, classSyncParts7(e));
|
||||
} else if (ts.isObjectLiteralExpression(e)) {
|
||||
for (const p of e.properties.nodes) {
|
||||
if (
|
||||
ts.isMethodDeclaration(p) ||
|
||||
ts.isGetAccessorDeclaration(p) ||
|
||||
ts.isSetAccessorDeclaration(p)
|
||||
) {
|
||||
// A computed key evaluates where the literal does; a plain
|
||||
// member-chain key (`[Equal.symbol]`) runs no user code.
|
||||
if (!ts.isComputedPropertyName(p.name)) units.push(p);
|
||||
else if (memberChainKey7(p.name.expression)) {
|
||||
units.push(p);
|
||||
immediate.set(p, [p.name]);
|
||||
}
|
||||
} else if (ts.isPropertyAssignment(p))
|
||||
storedCallables7(checker, p.initializer, units, immediate);
|
||||
}
|
||||
} else if (ts.isArrayLiteralExpression(e)) {
|
||||
for (const el of e.elements.nodes)
|
||||
if (!ts.isSpreadElement(el)) storedCallables7(checker, el, units, immediate);
|
||||
} else if (ts.isCallExpression(e)) {
|
||||
// `const f = wrap(callback)` where wrap only returns closures over
|
||||
// the callback: it runs when the stored result is called.
|
||||
e.arguments.nodes.forEach((arg, index) => {
|
||||
let fn: ts.Expression = arg;
|
||||
while (ts.isParenthesizedExpression(fn)) fn = fn.expression;
|
||||
if (
|
||||
(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) &&
|
||||
returnsOnlyClosuresOver7(checker, e.expression, index)
|
||||
)
|
||||
units.push(fn);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** A binding of an import cycle and what the reference scan found. */
|
||||
interface CycleCandidate7 {
|
||||
symbol: ts.Symbol;
|
||||
name: ts.Identifier;
|
||||
kind: CycleEarlyBinding["kind"];
|
||||
module: ts.SourceFile;
|
||||
/** User code can run between the cycle's start and the end of the
|
||||
* declaration (always true when the order is unknown). */
|
||||
exposed: boolean;
|
||||
rank: number;
|
||||
start: number;
|
||||
end: number;
|
||||
/** A reference read at top level before the declaration ran. */
|
||||
reference: ts.Identifier | null;
|
||||
/** A deferred reference (function body, parameter default, instance
|
||||
* field) when user code may run before the declaration completes. */
|
||||
deferredReference: ts.Identifier | null;
|
||||
folded: ts.Identifier | null;
|
||||
localNames: Map<string, string>;
|
||||
}
|
||||
|
||||
/** For one import cycle with a static evaluation order: the first deferred
|
||||
* reference to each candidate that code running before the candidate's
|
||||
* declaration completes can reach, or null when the walk cannot be precise
|
||||
* (top-level await, a default-exported function or class declaration).
|
||||
*
|
||||
* The cycle's top-level code runs in evaluation order. A stored callable
|
||||
* (a function or class declaration, or a function, class or object-literal
|
||||
* method stored by a variable initializer) becomes reachable once
|
||||
* reachable code names its binding, through an import alias or a
|
||||
* namespace member too; every other function-like in reachable code counts
|
||||
* as reachable, since it may be handed to a callee. A namespace import of
|
||||
* a cycle member used other than for a member read makes every stored
|
||||
* callable reachable. moduleEarlyBindingsOf7 is the single-module form. */
|
||||
function cycleReachedDeferredReferences7(
|
||||
program: ts.Program,
|
||||
evaluation: readonly ts.SourceFile[],
|
||||
candidates: ReadonlyMap<ts.Symbol, CycleCandidate7>,
|
||||
): Map<ts.Symbol, ts.Identifier> | null {
|
||||
const checker = program.getTypeChecker();
|
||||
const members = new Set(evaluation);
|
||||
for (const sf of evaluation) {
|
||||
if (hasTopLevelAwait7(sf)) return null;
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (
|
||||
(ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) &&
|
||||
(ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) &
|
||||
ts.ModifierFlags.Default) !==
|
||||
0
|
||||
)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const stored = new Set<ts.Node>();
|
||||
const immediate = new Map<ts.Node, ts.Node[]>();
|
||||
const storedNames: ts.Identifier[] = [];
|
||||
const unitsOfName = new Map<ts.Identifier, ts.Node[]>();
|
||||
const store = (name: ts.Identifier, units: ts.Node[]): void => {
|
||||
if (units.length === 0) return;
|
||||
for (const u of units) stored.add(u);
|
||||
storedNames.push(name);
|
||||
unitsOfName.set(name, units);
|
||||
};
|
||||
for (const sf of evaluation) {
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (
|
||||
(ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) &
|
||||
ts.ModifierFlags.Ambient) !==
|
||||
0
|
||||
)
|
||||
continue;
|
||||
if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) {
|
||||
store(stmt.name, [stmt]);
|
||||
} else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) {
|
||||
store(stmt.name, [stmt]);
|
||||
} else if (ts.isVariableStatement(stmt)) {
|
||||
for (const d of stmt.declarationList.declarations.nodes) {
|
||||
if (ts.isIdentifier(d.name) && d.initializer !== undefined) {
|
||||
const units: ts.Node[] = [];
|
||||
storedCallables7(checker, d.initializer, units, immediate);
|
||||
store(d.name, units);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const unitsBySymbol = new Map<ts.Symbol, ts.Node[]>();
|
||||
const allUnits: ts.Node[] = [];
|
||||
for (const name of storedNames) {
|
||||
const symbol = checker.getSymbolAtLocation(name);
|
||||
if (symbol === undefined) continue;
|
||||
const units = unitsOfName.get(name)!;
|
||||
unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), ...units]);
|
||||
allUnits.push(...units);
|
||||
}
|
||||
// A named class expression's inner name (`class Impl { static x =
|
||||
// Impl.m(); }`) names the class inside its own body, as the binding it
|
||||
// is stored in names it outside.
|
||||
const innerNames: ts.Identifier[] = [];
|
||||
for (const unit of allUnits) {
|
||||
if (!ts.isClassExpression(unit) || unit.name === undefined) continue;
|
||||
const symbol = checker.getSymbolAtLocation(unit.name);
|
||||
if (symbol === undefined) continue;
|
||||
innerNames.push(unit.name);
|
||||
unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), unit]);
|
||||
}
|
||||
// The identifier texts worth resolving: declared names, and the local
|
||||
// names of imports (named, default and namespace) in cycle members.
|
||||
const names = new Set<string>();
|
||||
for (const c of candidates.values()) names.add(c.name.text);
|
||||
for (const name of storedNames) names.add(name.text);
|
||||
for (const name of innerNames) names.add(name.text);
|
||||
for (const sf of evaluation) {
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (!ts.isImportDeclaration(stmt) || stmt.importClause === undefined) continue;
|
||||
const clause = stmt.importClause;
|
||||
if (clause.name !== undefined) names.add(clause.name.text);
|
||||
const bindings = clause.namedBindings;
|
||||
if (bindings === undefined) continue;
|
||||
if (ts.isNamedImports(bindings))
|
||||
for (const element of bindings.elements.nodes) names.add(element.name.text);
|
||||
else names.add(bindings.name.text);
|
||||
}
|
||||
}
|
||||
const idents: ts.Identifier[] = [];
|
||||
for (const sf of evaluation) {
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (ts.isImportDeclaration(node)) return "skip";
|
||||
if (ts.isIdentifier(node) && names.has(node.text)) idents.push(node);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
checker.prefetchSymbolNodesExact(idents);
|
||||
const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => {
|
||||
const parent = id.parent;
|
||||
let symbol = checker.getSymbolAtLocation(id);
|
||||
if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id) {
|
||||
symbol = checker.getShorthandAssignmentValueSymbol(parent) ?? symbol;
|
||||
}
|
||||
if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) {
|
||||
symbol = checker.getAliasedSymbol(symbol);
|
||||
}
|
||||
return symbol;
|
||||
};
|
||||
const isMemberNamespace = (symbol: ts.Symbol): boolean =>
|
||||
(symbol.flags & ts.SymbolFlags.ValueModule) !== 0 &&
|
||||
symbol.declarations.some((d) => ts.isSourceFile(d) && members.has(d));
|
||||
const declared = new Set<ts.Symbol>();
|
||||
const reachedUnits = new Set<ts.Node>();
|
||||
const out = new Map<ts.Symbol, ts.Identifier>();
|
||||
const work: ts.Node[] = [];
|
||||
const reach = (unit: ts.Node): void => {
|
||||
if (reachedUnits.has(unit)) return;
|
||||
reachedUnits.add(unit);
|
||||
work.push(unit);
|
||||
};
|
||||
const visit = (root: ts.Node): void => {
|
||||
ts.walkPreorder(root, (node) => {
|
||||
if (node !== root && stored.has(node)) {
|
||||
for (const part of immediate.get(node) ?? []) visit(part);
|
||||
return "skip";
|
||||
}
|
||||
if (ts.isImportDeclaration(node)) return "skip";
|
||||
if (node.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
// `this` in a static initializer or block names the class, so it
|
||||
// reaches the class's methods as naming the class does.
|
||||
const owner = staticThisClass7(node);
|
||||
if (owner !== undefined && stored.has(owner)) reach(owner);
|
||||
return undefined;
|
||||
}
|
||||
if (!ts.isIdentifier(node) || !names.has(node.text)) return undefined;
|
||||
if (inTypePosition7(node)) return undefined;
|
||||
const parent = node.parent;
|
||||
if (parent !== undefined && ts.isExportSpecifier(parent)) return undefined;
|
||||
if (parent !== undefined && declaredName7(parent) === node) return undefined;
|
||||
const symbol = symbolOf(node);
|
||||
if (symbol === undefined) return undefined;
|
||||
if (isMemberNamespace(symbol)) {
|
||||
// `ns.member` resolves through the member's own identifier; any
|
||||
// other use can reach every export.
|
||||
if (
|
||||
!(
|
||||
parent !== undefined &&
|
||||
ts.isPropertyAccessExpression(parent) &&
|
||||
parent.expression === node
|
||||
)
|
||||
)
|
||||
for (const unit of allUnits) reach(unit);
|
||||
return undefined;
|
||||
}
|
||||
const c = candidates.get(symbol);
|
||||
if (
|
||||
c !== undefined &&
|
||||
node !== c.name &&
|
||||
!declared.has(symbol) &&
|
||||
!out.has(symbol) &&
|
||||
inDeferredPosition7(node) &&
|
||||
!(
|
||||
c.kind === "class" &&
|
||||
ts.getSourceFileOfNode(node) === c.module &&
|
||||
ts.tokenStartOfNode(node) >= c.start &&
|
||||
node.end <= c.end
|
||||
)
|
||||
)
|
||||
out.set(symbol, node);
|
||||
for (const unit of unitsBySymbol.get(symbol) ?? []) reach(unit);
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
const drain = (): void => {
|
||||
while (work.length > 0) visit(work.pop()!);
|
||||
};
|
||||
const declare = (name: ts.BindingName): void => {
|
||||
for (const id of boundNames7(name)) {
|
||||
const symbol = checker.getSymbolAtLocation(id);
|
||||
if (symbol !== undefined) declared.add(symbol);
|
||||
}
|
||||
};
|
||||
for (const sf of evaluation) {
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (ts.isClassDeclaration(stmt)) {
|
||||
for (const part of classSyncParts7(stmt)) visit(part);
|
||||
drain();
|
||||
if (stmt.name !== undefined) declare(stmt.name);
|
||||
} else if (ts.isVariableStatement(stmt)) {
|
||||
for (const d of stmt.declarationList.declarations.nodes) {
|
||||
visit(d);
|
||||
drain();
|
||||
declare(d.name);
|
||||
}
|
||||
} else if (!ts.isFunctionDeclaration(stmt)) visit(stmt);
|
||||
drain();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Top-level let/const and class bindings, in modules outside the
|
||||
* exactly-initialized cycles, that code running before their declaration
|
||||
* completes can read.
|
||||
@@ -4114,51 +4405,8 @@ function moduleEarlyBindingsOf7(
|
||||
storedBy.set(name.text, list);
|
||||
};
|
||||
const checker = program.getTypeChecker();
|
||||
const storedIn = (e: ts.Expression, units: ts.Node[]): void => {
|
||||
while (
|
||||
ts.isParenthesizedExpression(e) ||
|
||||
ts.isAsExpression(e) ||
|
||||
ts.isSatisfiesExpression(e) ||
|
||||
ts.isNonNullExpression(e)
|
||||
)
|
||||
e = e.expression;
|
||||
if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) {
|
||||
units.push(e);
|
||||
} else if (ts.isClassExpression(e)) {
|
||||
units.push(e);
|
||||
immediate.set(e, classSyncParts7(e));
|
||||
} else if (ts.isObjectLiteralExpression(e)) {
|
||||
for (const p of e.properties.nodes) {
|
||||
if (
|
||||
ts.isMethodDeclaration(p) ||
|
||||
ts.isGetAccessorDeclaration(p) ||
|
||||
ts.isSetAccessorDeclaration(p)
|
||||
) {
|
||||
// A computed key evaluates where the literal does; a plain
|
||||
// member-chain key (`[Equal.symbol]`) runs no user code.
|
||||
if (!ts.isComputedPropertyName(p.name)) units.push(p);
|
||||
else if (memberChainKey7(p.name.expression)) {
|
||||
units.push(p);
|
||||
immediate.set(p, [p.name]);
|
||||
}
|
||||
} else if (ts.isPropertyAssignment(p)) storedIn(p.initializer, units);
|
||||
}
|
||||
} else if (ts.isArrayLiteralExpression(e)) {
|
||||
for (const el of e.elements.nodes) if (!ts.isSpreadElement(el)) storedIn(el, units);
|
||||
} else if (ts.isCallExpression(e)) {
|
||||
// `const f = wrap(callback)` where wrap only returns closures over
|
||||
// the callback: it runs when the stored result is called.
|
||||
e.arguments.nodes.forEach((arg, index) => {
|
||||
let fn: ts.Expression = arg;
|
||||
while (ts.isParenthesizedExpression(fn)) fn = fn.expression;
|
||||
if (
|
||||
(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) &&
|
||||
returnsOnlyClosuresOver7(checker, e.expression, index)
|
||||
)
|
||||
units.push(fn);
|
||||
});
|
||||
}
|
||||
};
|
||||
const storedIn = (e: ts.Expression, units: ts.Node[]): void =>
|
||||
storedCallables7(checker, e, units, immediate);
|
||||
sf.statements.nodes.forEach((stmt, index) => {
|
||||
if (ambient(stmt)) return;
|
||||
if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) {
|
||||
@@ -4411,6 +4659,40 @@ function returnsOnlyClosuresOver7(
|
||||
* expressions, decorators, computed member keys, static field
|
||||
* initializers and static blocks. Methods and instance fields wait for a
|
||||
* call or a construction. */
|
||||
/** The class whose static field initializer or static block evaluates `this`
|
||||
* (through arrow functions, which keep the enclosing `this`), or undefined
|
||||
* when another function or a class's instance side binds it. */
|
||||
function staticThisClass7(node: ts.Node): ts.ClassLikeDeclaration | undefined {
|
||||
let child = node;
|
||||
for (let n = node.parent; n !== undefined; child = n, n = n.parent) {
|
||||
if (ts.isArrowFunction(n)) continue;
|
||||
if (ts.isClassStaticBlockDeclaration(n)) return n.parent as ts.ClassLikeDeclaration;
|
||||
if (ts.isPropertyDeclaration(n)) {
|
||||
if (n.initializer !== child) return undefined;
|
||||
return (ts.getCombinedModifierFlags(n) & ts.ModifierFlags.Static) !== 0
|
||||
? (n.parent as ts.ClassLikeDeclaration)
|
||||
: undefined;
|
||||
}
|
||||
if (ts.isComputedPropertyName(n)) {
|
||||
// A computed key evaluates in the scope around the class.
|
||||
const member = n.parent;
|
||||
const cls = member?.parent;
|
||||
if (cls === undefined) return undefined;
|
||||
child = cls;
|
||||
n = cls;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
ts.isFunctionLike(n) ||
|
||||
ts.isClassDeclaration(n) ||
|
||||
ts.isClassExpression(n) ||
|
||||
ts.isSourceFile(n)
|
||||
)
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function classSyncParts7(c: ts.ClassLikeDeclaration): ts.Node[] {
|
||||
const decorators = (n: ts.Node): ts.Node[] =>
|
||||
((n as { modifiers?: ts.NodeArray<ts.Node> }).modifiers?.nodes ?? []).filter(
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7380-cycle-deferred-folded-key/util.ts",
|
||||
"<repo>/tests/corpus/7380-cycle-deferred-folded-key/core.ts",
|
||||
"<repo>/tests/corpus/7380-cycle-deferred-folded-key/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts",
|
||||
"<repo>/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts",
|
||||
"<repo>/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7382-cycle-static-this-tdz/beta.ts",
|
||||
"<repo>/tests/corpus/7382-cycle-static-this-tdz/alpha.ts",
|
||||
"<repo>/tests/corpus/7382-cycle-static-this-tdz/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7383-cycle-class-expression-name-tdz/beta.ts",
|
||||
"<repo>/tests/corpus/7383-cycle-class-expression-name-tdz/alpha.ts",
|
||||
"<repo>/tests/corpus/7383-cycle-class-expression-name-tdz/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { describe, kind, mark } from "./util.ts";
|
||||
|
||||
export const version = 3;
|
||||
export const marked = mark({});
|
||||
console.log("core body", kind(), describe());
|
||||
@@ -0,0 +1,10 @@
|
||||
// A cycle member runs user code before it declares a constant that only
|
||||
// functions read, one of them as a computed key and one under `typeof`
|
||||
// (both resolved from the constant's literal type). Nothing calls those
|
||||
// functions before the declaration ran, so they read the constant
|
||||
// normally once the other member's top level calls them.
|
||||
import { marked } from "./core.ts";
|
||||
import { describe, kind, mark } from "./util.ts";
|
||||
|
||||
console.log(JSON.stringify(marked), kind());
|
||||
console.log(JSON.stringify(mark({ seen: false })), describe());
|
||||
@@ -0,0 +1,23 @@
|
||||
import { version } from "./core.ts";
|
||||
|
||||
export function makeCounter(): () => number {
|
||||
let n = 0;
|
||||
return () => ++n;
|
||||
}
|
||||
const next = makeCounter();
|
||||
console.log("util body", next(), next());
|
||||
|
||||
export const TAG = "~tagged";
|
||||
|
||||
export function mark(box: Record<string, boolean>): Record<string, boolean> {
|
||||
box[TAG] = true;
|
||||
return box;
|
||||
}
|
||||
|
||||
export function kind(): string {
|
||||
return typeof TAG;
|
||||
}
|
||||
|
||||
export function describe(): string {
|
||||
return `core v${version}`;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { probe } from "./beta.ts";
|
||||
|
||||
export function report(): string {
|
||||
return `alpha ${NAME}`;
|
||||
}
|
||||
|
||||
export function late(): string {
|
||||
return `${OTHER} after ${probe}`;
|
||||
}
|
||||
|
||||
export const NAME = "ready";
|
||||
export const OTHER = "other";
|
||||
console.log("alpha body", report());
|
||||
@@ -0,0 +1,12 @@
|
||||
import { report } from "./alpha.ts";
|
||||
|
||||
function attempt(): string {
|
||||
try {
|
||||
return report();
|
||||
} catch (error) {
|
||||
return error instanceof ReferenceError ? `ReferenceError: ${error.message}` : "other error";
|
||||
}
|
||||
}
|
||||
|
||||
export const probe = attempt();
|
||||
console.log("beta body", probe);
|
||||
@@ -0,0 +1,7 @@
|
||||
// The first cycle member to run calls a function of the other member
|
||||
// before that member declared the constant the function reads: Node's
|
||||
// temporal-dead-zone ReferenceError. A second function of the same member
|
||||
// reads another constant but runs only after the cycle finished.
|
||||
import { late, report } from "./alpha.ts";
|
||||
|
||||
console.log(report(), late());
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Probe, Lazy } from "./beta.ts";
|
||||
|
||||
export { Probe };
|
||||
export const label = "ready";
|
||||
export const other = "other";
|
||||
|
||||
export function later(): string {
|
||||
return `${Lazy.describe()} ${other}`;
|
||||
}
|
||||
|
||||
console.log("alpha body", Probe.result, Lazy.value);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { label, other } from "./alpha.ts";
|
||||
|
||||
function guard(read: () => string): string {
|
||||
try {
|
||||
return read();
|
||||
} catch (error) {
|
||||
return error instanceof ReferenceError ? `ReferenceError: ${error.message}` : "other error";
|
||||
}
|
||||
}
|
||||
|
||||
export class Probe {
|
||||
static helper(): string {
|
||||
return guard(() => `label ${label}`);
|
||||
}
|
||||
static result = this.helper();
|
||||
}
|
||||
|
||||
export class Lazy {
|
||||
static read(): string {
|
||||
return guard(() => `other ${other}`);
|
||||
}
|
||||
static describe(): string {
|
||||
return `lazy ${other}`;
|
||||
}
|
||||
static value = [0].map(() => this.read()).join("");
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// The first cycle member to run builds classes whose static initializers
|
||||
// call a method through `this`, directly and from an arrow function; both
|
||||
// methods read constants of the other member before it declared them:
|
||||
// Node's temporal-dead-zone ReferenceError. A third method reads a constant
|
||||
// only after the cycle finished.
|
||||
import { label, Probe, later } from "./alpha.ts";
|
||||
|
||||
console.log(label, Probe.result, later());
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Probe, Lazy } from "./beta.ts";
|
||||
|
||||
export { Probe };
|
||||
export const label = "ready";
|
||||
export const other = "other";
|
||||
|
||||
export function later(): string {
|
||||
return `${Lazy.describe()} ${other}`;
|
||||
}
|
||||
|
||||
console.log("alpha body", Probe.result, Lazy.value);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { label, other } from "./alpha.ts";
|
||||
|
||||
function guard(read: () => string): string {
|
||||
try {
|
||||
return read();
|
||||
} catch (error) {
|
||||
return error instanceof ReferenceError ? `ReferenceError: ${error.message}` : "other error";
|
||||
}
|
||||
}
|
||||
|
||||
export const Probe = class ProbeImpl {
|
||||
static helper(): string {
|
||||
return guard(() => `label ${label}`);
|
||||
}
|
||||
static result = ProbeImpl.helper();
|
||||
};
|
||||
|
||||
export const Lazy = class LazyImpl {
|
||||
static read(): string {
|
||||
return guard(() => `other ${other}`);
|
||||
}
|
||||
static describe(): string {
|
||||
return `lazy ${other}`;
|
||||
}
|
||||
static value = [0].map(() => LazyImpl.read()).join("");
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
// The first cycle member to run builds classes from named class
|
||||
// expressions whose static initializers call a method through the inner
|
||||
// name, directly and from an arrow function; both methods read constants
|
||||
// of the other member before it declared them: Node's temporal-dead-zone
|
||||
// ReferenceError. A third method reads a constant only after the cycle
|
||||
// finished.
|
||||
import { label, Probe, later } from "./alpha.ts";
|
||||
|
||||
console.log(label, Probe.result, later());
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2500, "san": 3500 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1800, "san": 2600 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1800, "san": 2600 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1800, "san": 2600 }
|
||||
Reference in New Issue
Block a user