mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Check every early reference to a cycle class
The references of a cycle class were found by their spelling, so a class
imported under another name (`import { Sink as Alias }`) was never seen as
reached early, and neither was a let or const imported under another
name. An `extends` clause only counted when its expression was the bare
class name. A static method read as a value (`const f = Sink.m`) built a
closure that no check covered.
The early-binding scan and the class reference scan now also match each
file's local import names that resolve to the binding, and record them:
Node's ReferenceError names the binding the access spells, so the check
names the alias. The heritage check walks the whole `extends` expression.
A closure of a flagged class's static method checks the class like a
static call does.
This commit is contained in:
@@ -37,6 +37,8 @@ export interface CycleInitFlag {
|
||||
/** The declaring module. */
|
||||
module: ts.SourceFile;
|
||||
settled: ReadonlySet<ts.SourceFile>;
|
||||
/** Local names other files import the binding under, by file name. */
|
||||
localNames?: ReadonlyMap<string, string>;
|
||||
/** True for a binding of an import cycle. A binding flagged only for its
|
||||
* own module's early calls is checked opportunistically: whatever cannot
|
||||
* be checked keeps the unchecked behavior instead of refusing. */
|
||||
@@ -49,11 +51,19 @@ export interface CycleInitFlag {
|
||||
* static field storage (`global` is the flag itself). */
|
||||
className?: string;
|
||||
staticGlobals?: ReadonlySet<string>;
|
||||
/** The source references to the class outside its own body. Only IR
|
||||
* built for one of them is checked: compiler-generated dispatch (error
|
||||
* property helpers, prototype data, reflection) also names the class
|
||||
* and must keep working before the class statement ran. */
|
||||
references?: readonly SrcLoc[];
|
||||
/** The source references to the class outside its own body, with the
|
||||
* local name each one spells (an import can rename the class; Node's
|
||||
* ReferenceError names that binding). Only IR built for one of them is
|
||||
* checked: compiler-generated dispatch (error property helpers,
|
||||
* prototype data, reflection) also names the class and must keep
|
||||
* working before the class statement ran. */
|
||||
references?: readonly ClassReference[];
|
||||
}
|
||||
|
||||
/** A source reference to a flagged class and the local name it spells. */
|
||||
interface ClassReference {
|
||||
loc: SrcLoc;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** Registers the initialization flags after global collection. Bindings
|
||||
@@ -182,33 +192,75 @@ function registerClassInitFlag(
|
||||
function earlyHeritageReference(lowerer: Lowerer, binding: CycleEarlyBinding): ts.Node | null {
|
||||
const declaration = binding.name.parent;
|
||||
for (const sf of lowerer.program.getImplementationSourceFiles()) {
|
||||
if (
|
||||
sf.isDeclarationFile ||
|
||||
sf === binding.module ||
|
||||
binding.settled.has(sf) ||
|
||||
!sf.text.includes(binding.name.text)
|
||||
)
|
||||
continue;
|
||||
if (sf.isDeclarationFile || sf === binding.module || binding.settled.has(sf)) continue;
|
||||
const names = classLocalNames(lowerer, sf, binding.symbol, binding.name.text, true);
|
||||
if (names.size === 0) continue;
|
||||
let found: ts.Node | null = null;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (found !== null || node === declaration) return "skip";
|
||||
if (ts.isHeritageClause(node)) {
|
||||
for (const type of node.types.nodes) {
|
||||
let referenced = ts.isIdentifier(type.expression)
|
||||
? lowerer.checker.getSymbolAtLocation(type.expression)
|
||||
: undefined;
|
||||
if (referenced !== undefined && referenced.flags & ts.SymbolFlags.Alias)
|
||||
referenced = lowerer.checker.getAliasedSymbol(referenced);
|
||||
if (referenced === binding.symbol) found = type.expression;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
if (!ts.isHeritageClause(node)) return undefined;
|
||||
// Node evaluates the whole clause when the class statement runs:
|
||||
// any reference inside it (parenthesized, a factory argument)
|
||||
// reaches the class.
|
||||
ts.walkPreorder(node, (inner) => {
|
||||
if (found !== null) return "skip";
|
||||
if (
|
||||
ts.isIdentifier(inner) &&
|
||||
names.has(inner.text) &&
|
||||
resolvesTo(lowerer, inner, binding.symbol, true)
|
||||
)
|
||||
found = inner;
|
||||
return undefined;
|
||||
});
|
||||
return "skip";
|
||||
});
|
||||
if (found !== null) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether `node` names `symbol`, through import aliases when `aliases`. */
|
||||
function resolvesTo(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Identifier,
|
||||
symbol: ts.Symbol,
|
||||
aliases: boolean,
|
||||
): boolean {
|
||||
let referenced = lowerer.checker.getSymbolAtLocation(node);
|
||||
if (aliases && referenced !== undefined && referenced.flags & ts.SymbolFlags.Alias)
|
||||
referenced = lowerer.checker.getAliasedSymbol(referenced);
|
||||
return referenced === symbol;
|
||||
}
|
||||
|
||||
/** The local names under which `sf` can spell the class: its declared name
|
||||
* (when the file's text contains it) and, with `aliases`, the local names
|
||||
* of the file's imports that resolve to it (`import { Sink as Alias }`, a
|
||||
* default import, a re-exported name). */
|
||||
function classLocalNames(
|
||||
lowerer: Lowerer,
|
||||
sf: ts.SourceFile,
|
||||
symbol: ts.Symbol,
|
||||
declared: string,
|
||||
aliases: boolean,
|
||||
): Set<string> {
|
||||
const names = new Set<string>();
|
||||
if (sf.text.includes(declared)) names.add(declared);
|
||||
if (!aliases) return names;
|
||||
for (const statement of sf.statements.nodes) {
|
||||
if (!ts.isImportDeclaration(statement)) continue;
|
||||
const clause = statement.importClause;
|
||||
if (clause === undefined) continue;
|
||||
const locals: ts.Identifier[] = [];
|
||||
if (clause.name !== undefined) locals.push(clause.name);
|
||||
const bindings = clause.namedBindings;
|
||||
if (bindings !== undefined && ts.isNamedImports(bindings))
|
||||
for (const element of bindings.elements.nodes) locals.push(element.name);
|
||||
for (const local of locals)
|
||||
if (!names.has(local.text) && resolvesTo(lowerer, local, symbol, true)) names.add(local.text);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/** The source spans of the identifiers naming the class outside its own
|
||||
* declaration, in every program file. For a class of an import cycle that
|
||||
* includes other modules' import bindings; another module's references to a
|
||||
@@ -219,31 +271,36 @@ function classReferences(
|
||||
symbol: ts.Symbol,
|
||||
name: ts.Identifier,
|
||||
cycle: boolean,
|
||||
): SrcLoc[] {
|
||||
): ClassReference[] {
|
||||
const declaration = name.parent;
|
||||
const out: SrcLoc[] = [];
|
||||
const out: ClassReference[] = [];
|
||||
for (const sf of lowerer.program.getImplementationSourceFiles()) {
|
||||
if (sf.isDeclarationFile || !sf.text.includes(name.text)) continue;
|
||||
if (sf.isDeclarationFile) continue;
|
||||
// Another module names the class through its import bindings, under
|
||||
// the class's name or a local one.
|
||||
const names = classLocalNames(lowerer, sf, symbol, name.text, cycle);
|
||||
if (names.size === 0) continue;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (node === declaration) return "skip";
|
||||
if (ts.isIdentifier(node) && node.text === name.text) {
|
||||
// Another module names the class through its import binding.
|
||||
let referenced = lowerer.checker.getSymbolAtLocation(node);
|
||||
if (cycle && referenced !== undefined && referenced.flags & ts.SymbolFlags.Alias)
|
||||
referenced = lowerer.checker.getAliasedSymbol(referenced);
|
||||
if (referenced === symbol) out.push(locOf(node));
|
||||
}
|
||||
if (ts.isIdentifier(node) && names.has(node.text) && resolvesTo(lowerer, node, symbol, cycle))
|
||||
out.push({ loc: locOf(node), name: node.text });
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The source reference to the flagged class that `loc`'s IR was built
|
||||
* for, or undefined. */
|
||||
function coveredReference(flag: CycleInitFlag, loc: SrcLoc): ClassReference | undefined {
|
||||
return (flag.references ?? []).find(
|
||||
(r) => r.loc.file === loc.file && loc.start <= r.loc.start && r.loc.end <= loc.end,
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether `loc` is the IR of a source reference to the flagged class. */
|
||||
function coversReference(flag: CycleInitFlag, loc: SrcLoc): boolean {
|
||||
return (flag.references ?? []).some(
|
||||
(r) => r.file === loc.file && loc.start <= r.start && r.end <= loc.end,
|
||||
);
|
||||
return coveredReference(flag, loc) !== undefined;
|
||||
}
|
||||
|
||||
/** The class statement's static initialization, followed by the store of
|
||||
@@ -271,6 +328,7 @@ function registerInitFlag(
|
||||
name: binding.name.text,
|
||||
module: binding.module,
|
||||
settled: binding.settled,
|
||||
...(binding.localNames ? { localNames: binding.localNames } : {}),
|
||||
cycle,
|
||||
};
|
||||
lowerer.cycleInitFlags.set(global.id, flag);
|
||||
@@ -444,18 +502,30 @@ function rewriteFunction(
|
||||
return flag;
|
||||
}
|
||||
return undefined;
|
||||
case "closure":
|
||||
// A static method read as a value (`const f = K.m`) reads K.
|
||||
for (const [className, flag] of classes) {
|
||||
if (e.fnName.startsWith(`%${className}.static:`) && coversReference(flag, e.loc))
|
||||
return flag;
|
||||
}
|
||||
return undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
let temps = 0;
|
||||
const check = (flag: CycleInitFlag, loc: SrcLoc): IrStmt => {
|
||||
// Node's ReferenceError names the binding the reference spells.
|
||||
const name =
|
||||
flag.className !== undefined
|
||||
? (coveredReference(flag, loc)?.name ?? flag.name)
|
||||
: (flag.localNames?.get(loc.file) ?? flag.name);
|
||||
const expr: IrExpr = {
|
||||
kind: "intrinsic",
|
||||
name: "module.tdzCheck",
|
||||
args: [
|
||||
{ kind: "varRef", localId: flag.flagId, type: BOOL, loc },
|
||||
{ kind: "strLit", value: flag.name, type: STRING, loc },
|
||||
{ kind: "strLit", value: name, type: STRING, loc },
|
||||
],
|
||||
type: VOID,
|
||||
loc,
|
||||
|
||||
@@ -3309,6 +3309,9 @@ export interface CycleEarlyBinding {
|
||||
/** Modules whose TOP-LEVEL code runs only after the declaration ran:
|
||||
* modules outside the cycle and cycle members evaluated later. */
|
||||
settled: ReadonlySet<ts.SourceFile>;
|
||||
/** The local name each importing file gives the binding when it differs
|
||||
* from the declared one, by file name: Node's ReferenceError names it. */
|
||||
localNames?: ReadonlyMap<string, string>;
|
||||
}
|
||||
|
||||
/** Every binding of the program's exactly-initialized cycles that can be
|
||||
@@ -3429,6 +3432,7 @@ function computeCycleEarlyBindings7(
|
||||
end: number;
|
||||
reference: ts.Identifier | null;
|
||||
folded: ts.Identifier | null;
|
||||
localNames: Map<string, string>;
|
||||
}
|
||||
const candidates = new Map<ts.Symbol, Candidate>();
|
||||
const names = new Set<string>();
|
||||
@@ -3468,6 +3472,7 @@ function computeCycleEarlyBindings7(
|
||||
end: stmt.getEnd(),
|
||||
reference: null,
|
||||
folded: null,
|
||||
localNames: new Map(),
|
||||
});
|
||||
names.add(name.text);
|
||||
}
|
||||
@@ -3476,13 +3481,33 @@ function computeCycleEarlyBindings7(
|
||||
});
|
||||
if (candidates.size === 0) continue;
|
||||
const rankOf = new Map(evaluation.map((m, i) => [m, i] as const));
|
||||
// References: every identifier spelled like a candidate, resolved
|
||||
// through import aliases to the declaration's symbol.
|
||||
// References: every identifier spelled like a candidate or like a
|
||||
// local name the file imports one under (`import { x as y }`, a
|
||||
// default import), resolved through import aliases to the
|
||||
// declaration's symbol.
|
||||
for (const sf of order) {
|
||||
const local = new Set<string>();
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (!ts.isImportDeclaration(stmt) || stmt.importClause === undefined) continue;
|
||||
const clause = stmt.importClause;
|
||||
const bound: ts.Identifier[] = clause.name !== undefined ? [clause.name] : [];
|
||||
if (clause.namedBindings !== undefined && ts.isNamedImports(clause.namedBindings))
|
||||
for (const element of clause.namedBindings.elements.nodes) bound.push(element.name);
|
||||
for (const name of bound) {
|
||||
if (names.has(name.text)) continue;
|
||||
const imported = checker.getSymbolAtLocation(name);
|
||||
const candidate =
|
||||
imported === undefined ? undefined : candidates.get(checker.getAliasedSymbol(imported));
|
||||
if (candidate === undefined) continue;
|
||||
local.add(name.text);
|
||||
candidate.localNames.set(sf.fileName, name.text);
|
||||
}
|
||||
}
|
||||
const refs: ts.Identifier[] = [];
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (ts.isImportDeclaration(node)) return "skip";
|
||||
if (ts.isIdentifier(node) && names.has(node.text)) refs.push(node);
|
||||
if (ts.isIdentifier(node) && (names.has(node.text) || local.has(node.text)))
|
||||
refs.push(node);
|
||||
return undefined;
|
||||
});
|
||||
if (refs.length === 0) continue;
|
||||
@@ -3534,6 +3559,7 @@ function computeCycleEarlyBindings7(
|
||||
reference: c.reference,
|
||||
...(c.folded !== null ? { folded: c.folded } : {}),
|
||||
settled,
|
||||
...(c.localNames.size > 0 ? { localNames: c.localNames } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ export class Sink {
|
||||
constructor(tag: string) {
|
||||
this.tag = tag;
|
||||
}
|
||||
static describe(): string {
|
||||
return "sink";
|
||||
}
|
||||
}
|
||||
|
||||
export const limit = 3;
|
||||
|
||||
export const probed = probe();
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// A class of an import cycle reached before its module body ran: Node
|
||||
// throws the temporal-dead-zone ReferenceError at the access, and the
|
||||
// same class works once its module finished.
|
||||
import { Sink } from "./log.ts";
|
||||
import { early } from "./stats.ts";
|
||||
import { Sink, probed } from "./log.ts";
|
||||
import { early, earlyMethod, earlyRenamed, earlyCap } from "./stats.ts";
|
||||
|
||||
console.log("early:", early);
|
||||
console.log("late:", new Sink("late").tag);
|
||||
console.log("early method:", earlyMethod);
|
||||
console.log("early renamed:", earlyRenamed);
|
||||
console.log("early renamed constant:", earlyCap);
|
||||
console.log("probed:", probed);
|
||||
console.log("late:", new Sink("late").tag, Sink.describe());
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import { Sink } from "./log.ts";
|
||||
import { Sink, Sink as Renamed, limit as cap } from "./log.ts";
|
||||
|
||||
function tryMake(): string {
|
||||
function attempt(run: () => string): string {
|
||||
try {
|
||||
return new Sink("early").tag;
|
||||
return run();
|
||||
} catch (error) {
|
||||
return `${(error as Error).name}: ${(error as Error).message}`;
|
||||
}
|
||||
}
|
||||
|
||||
export const early = tryMake();
|
||||
// Each early access throws Node's ReferenceError, naming the binding the
|
||||
// access spells: construction, a static method read as a value, the class
|
||||
// and a constant through renamed imports.
|
||||
export const early = attempt(() => new Sink("early").tag);
|
||||
export const earlyMethod = attempt(() => {
|
||||
const describe = Sink.describe;
|
||||
return typeof describe;
|
||||
});
|
||||
export const earlyRenamed = attempt(() => new Renamed("renamed").tag);
|
||||
export const earlyCap = attempt(() => String(cap));
|
||||
|
||||
export function probe(): string {
|
||||
return new Sink("probe").tag;
|
||||
return `${new Sink("probe").tag} ${Sink.describe()} ${new Renamed("probe").tag} ${cap}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user