Keep one source label per node in the shape analyzer

This commit is contained in:
Chris Tate
2026-10-25 20:31:37 -05:00
parent 7be83d8e11
commit 6aaaea04e4
@@ -317,6 +317,14 @@ export function analyzeShapes(program: ts.Program, checker: ts.TypeChecker): Sha
}
class Analyzer {
/** locLabel per node: sink reasons and atom labels name the same sites
* over and over, and each label reads the file's text for its line. */
private readonly locLabels = new Map<Node, string>();
private loc(node: Node): string {
let label = this.locLabels.get(node);
if (label === undefined) this.locLabels.set(node, (label = locLabel(node)));
return label;
}
readonly atoms = new AtomTable();
readonly nodes: ValueNode[] = [];
readonly functions = new Map<string, FunctionInfo>();
@@ -830,7 +838,7 @@ class Analyzer {
if (this.traceTop)
this.nodeLabel.set(
id,
`binding ${(decl as { name?: { text?: string } }).name?.text ?? "?"}@${locLabel(decl)}${this.ctx !== "" ? ` [${this.ctx}]` : ""}`,
`binding ${(decl as { name?: { text?: string } }).name?.text ?? "?"}@${this.loc(decl)}${this.ctx !== "" ? ` [${this.ctx}]` : ""}`,
);
}
return id;
@@ -908,7 +916,7 @@ class Analyzer {
const nameNode = (fn as { name?: Node }).name;
const name = nameNode !== undefined ? (propertyNameText(nameNode) ?? "<computed>") : null;
const owner = ownerClass !== null ? this.className(ownerClass) : null;
const label = `${owner !== null ? `${owner}.` : ""}${name ?? (ts.isConstructorDeclaration(node) ? "constructor" : "<fn>")}@${locLabel(node)}`;
const label = `${owner !== null ? `${owner}.` : ""}${name ?? (ts.isConstructorDeclaration(node) ? "constructor" : "<fn>")}@${this.loc(node)}`;
const info: FunctionInfo = {
node,
ctx,
@@ -952,7 +960,7 @@ class Analyzer {
if (p.dotDotDotToken !== undefined) {
const arr = this.atoms.intern(`rest:${this.nodes.length}`, () => ({
kind: "array",
label: `rest@${locLabel(p)}`,
label: `rest@${this.loc(p)}`,
node: p,
}));
this.add(this.binding(p), arr);
@@ -998,7 +1006,7 @@ class Analyzer {
const p = decl.parent;
if (p !== undefined && ts.isVariableDeclaration(p) && ts.isIdentifier(p.name))
return p.name.text;
return `<class@${locLabel(decl)}>`;
return `<class@${this.loc(decl)}>`;
}
classAtoms(decl: Node): { cls: Atom; inst: Atom } {
@@ -1039,7 +1047,7 @@ class Analyzer {
ctx: "",
root: null,
walked: true,
label: `${label}.${suffix}@${locLabel(decl)}`,
label: `${label}.${suffix}@${this.loc(decl)}`,
params: [],
rest: -1,
thisNode: this.newNode(),
@@ -1187,7 +1195,7 @@ class Analyzer {
if (this.isFactory(info.node)) {
const d = this.atoms.intern(`dict:${this.nodeKey(site)}`, () => ({
kind: "dictionary",
label: `Object.create@${locLabel(site)}`,
label: `Object.create@${this.loc(site)}`,
node: site,
}));
this.add(dst, d);
@@ -1253,7 +1261,7 @@ class Analyzer {
syntheticArray(site: Node, tag: string): Atom {
return this.atoms.intern(`arr:${tag}:${this.nodeKey(site)}`, () => ({
kind: "array",
label: `${tag}[]@${locLabel(site)}`,
label: `${tag}[]@${this.loc(site)}`,
node: site,
}));
}
@@ -1361,7 +1369,7 @@ class Analyzer {
return;
}
if (ts.isThrowStatement(s)) {
this.sink(this.expr(s.expression), `thrown at ${locLabel(s)}`);
this.sink(this.expr(s.expression), `thrown at ${this.loc(s)}`);
return;
}
if (
@@ -1437,7 +1445,7 @@ class Analyzer {
Symbol.iterator member have a computed key; they are G sites. */
} else {
this.add(dst, this.top);
if (a.kind !== "top") this.escape(a, `iterated at ${locLabel(site)}`);
if (a.kind !== "top") this.escape(a, `iterated at ${this.loc(site)}`);
}
});
}
@@ -1453,7 +1461,7 @@ class Analyzer {
if (this.traceTop && !this.nodeLabel.has(id))
this.nodeLabel.set(
id,
`expr ${e.getText().slice(0, 40).replace(/\s+/g, " ")}@${locLabel(e)}`,
`expr ${e.getText().slice(0, 40).replace(/\s+/g, " ")}@${this.loc(e)}`,
);
return id;
}
@@ -1586,7 +1594,7 @@ class Analyzer {
if (ts.isTaggedTemplateExpression(e)) return this.taggedTemplate(e);
if (ts.isYieldExpression(e)) {
if (e.expression !== undefined)
this.sink(this.expr(e.expression), `yielded at ${locLabel(e)}`);
this.sink(this.expr(e.expression), `yielded at ${this.loc(e)}`);
return this.constNode(this.top);
}
if (ts.isMetaProperty(e)) return this.constNode(this.top);
@@ -1841,7 +1849,7 @@ class Analyzer {
const spreads: number[] = [];
const inPackage = isPackageJsFile(e.getSourceFile());
const label = (): string =>
`{${keys.slice(0, 6).join(",")}${keys.length > 6 ? ",…" : ""}${keysKnown ? "" : ",…"}}@${locLabel(e)}`;
`{${keys.slice(0, 6).join(",")}${keys.length > 6 ? ",…" : ""}${keysKnown ? "" : ",…"}}@${this.loc(e)}`;
// keys first, so the label is complete
for (const p of list(e.properties)) {
if (ts.isSpreadAssignment(p)) {
@@ -2053,7 +2061,7 @@ class Analyzer {
arrayLiteral(e: ts.ArrayLiteralExpression): number {
const atom = this.atoms.intern(`arr:${this.nodeKey(e)}`, () => ({
kind: "array",
label: `[]@${locLabel(e)}`,
label: `[]@${this.loc(e)}`,
node: e,
}));
const n = this.newNode();
@@ -2175,7 +2183,7 @@ class Analyzer {
if (ts.isIdentifier(t)) {
const decl = this.resolve(t);
if (decl === undefined || decl.getSourceFile().isDeclarationFile) {
this.sink(value, `global write at ${locLabel(t)}`);
this.sink(value, `global write at ${this.loc(t)}`);
return;
}
this.edge(value, this.binding(decl));
@@ -2421,7 +2429,7 @@ class Analyzer {
switch (a.kind) {
case "top":
this.noteTopWrite(name, site);
this.sink(value, `stored into ⊤.${name} at ${locLabel(site)}`);
this.sink(value, `stored into ⊤.${name} at ${this.loc(site)}`);
return;
case "instance": {
const member = this.lookupMember(a.node!, name, false);
@@ -2448,7 +2456,7 @@ class Analyzer {
case "prim":
return;
default:
this.sink(value, `stored into ${a.label} at ${locLabel(site)}`);
this.sink(value, `stored into ${a.label} at ${this.loc(site)}`);
}
});
}
@@ -2497,7 +2505,7 @@ class Analyzer {
switch (a.kind) {
case "top":
this.noteTopWrite(null, site);
this.sink(value, `stored into ⊤[k] at ${locLabel(site)}`);
this.sink(value, `stored into ⊤[k] at ${this.loc(site)}`);
return;
case "array":
// An index, or a named property under an unknown key.
@@ -2515,7 +2523,7 @@ class Analyzer {
return;
default:
// As a store under a name: a builtin, promise or collection keeps it where it isn't tracked.
this.sink(value, `stored into ${a.label}[k] at ${locLabel(site)}`);
this.sink(value, `stored into ${a.label}[k] at ${this.loc(site)}`);
}
});
}
@@ -2591,7 +2599,7 @@ class Analyzer {
if (inst.kind === "instance") this.callConstructorChain(base, inst, args, spread, e);
});
} else if (base === undefined)
for (const a of args) this.sink(a, `super(...) at ${locLabel(e)}`);
for (const a of args) this.sink(a, `super(...) at ${this.loc(e)}`);
return dst;
}
// CommonJS `require("<spec>")` of an analyzed module: its exports (the
@@ -2686,9 +2694,9 @@ class Analyzer {
case "prim":
return;
default:
for (const x of args) this.sink(x, `argument of ⊤ call at ${locLabel(site)}`);
for (const x of args) this.sink(x, `argument of ⊤ call at ${this.loc(site)}`);
if (recv !== null && a.kind === "top")
this.sink(recv, `receiver of ⊤ call at ${locLabel(site)}`);
this.sink(recv, `receiver of ⊤ call at ${this.loc(site)}`);
this.add(dst, this.top);
}
});
@@ -2731,7 +2739,7 @@ class Analyzer {
this.constructBuiltin(a.path ?? "", args, dst, e);
return;
default:
for (const x of args) this.sink(x, `argument of ⊤ constructor at ${locLabel(e)}`);
for (const x of args) this.sink(x, `argument of ⊤ constructor at ${this.loc(e)}`);
this.add(dst, this.top);
}
});
@@ -2746,7 +2754,7 @@ class Analyzer {
case "WeakSet": {
const c = this.atoms.intern(`coll:${this.nodeKey(site)}`, () => ({
kind: "collection",
label: `${path}@${locLabel(site)}`,
label: `${path}@${this.loc(site)}`,
node: site,
}));
this.add(dst, c);
@@ -2760,7 +2768,7 @@ class Analyzer {
this.add(dst, this.builtin("Error.instance"));
return;
case "Promise": {
const p = this.promiseAtom(site, locLabel(site));
const p = this.promiseAtom(site, this.loc(site));
this.add(this.field(p, "value"), this.top);
this.add(dst, p);
if (args[0] !== undefined) {
@@ -2793,11 +2801,11 @@ class Analyzer {
// The handler's traps run on every operation with the target.
if (args[0] !== undefined)
this.ops.push({ kind: "proxy", recv: args[0], name: null, node: site });
for (const x of args) this.sink(x, `argument of new Proxy at ${locLabel(site)}`);
for (const x of args) this.sink(x, `argument of new Proxy at ${this.loc(site)}`);
this.add(dst, this.top);
return;
default:
for (const x of args) this.sink(x, `argument of new ${path} at ${locLabel(site)}`);
for (const x of args) this.sink(x, `argument of new ${path} at ${this.loc(site)}`);
this.add(dst, this.top);
}
}
@@ -2824,13 +2832,13 @@ class Analyzer {
this.callFunction(this.infoOfAtom(f)!, null, cbArgs, false, ret ?? this.newNode(), site);
this.forceLoop = saved;
} else if (f.kind === "top" || f.kind === "builtin") {
for (const x of cbArgs) this.sink(x, `argument of ⊤ callback at ${locLabel(site)}`);
for (const x of cbArgs) this.sink(x, `argument of ⊤ callback at ${this.loc(site)}`);
if (ret !== null) this.add(ret, this.top);
}
});
};
const escapeAll = (): void => {
for (const x of args) this.sink(x, `argument of ${path} at ${locLabel(site)}`);
for (const x of args) this.sink(x, `argument of ${path} at ${this.loc(site)}`);
this.add(dst, this.top);
};
// Methods of builtin receivers.
@@ -2948,7 +2956,7 @@ class Analyzer {
return;
}
escapeAll();
this.escape(recv, `array.${method} at ${locLabel(site)}`);
this.escape(recv, `array.${method} at ${this.loc(site)}`);
return;
}
if (path.startsWith("string.") || path.startsWith("number.") || path.startsWith("boolean.")) {
@@ -2977,7 +2985,7 @@ class Analyzer {
return;
}
if (path.startsWith("promise.")) {
const out = this.promiseAtom(site, locLabel(site));
const out = this.promiseAtom(site, this.loc(site));
const value = this.field(recv, "value");
const ret = this.newNode();
if (method === "then") {
@@ -3047,7 +3055,7 @@ class Analyzer {
if (info !== undefined && args.length > 1) {
this.activate(info);
for (const p of info.params) this.add(p, this.top);
for (const x of args.slice(1)) this.sink(x, `bound argument at ${locLabel(site)}`);
for (const x of args.slice(1)) this.sink(x, `bound argument at ${this.loc(site)}`);
}
this.add(dst, recv);
return;
@@ -3145,7 +3153,7 @@ class Analyzer {
for (const s of args.slice(1)) this.copyFields(s, t, site);
if (t.kind === "top")
for (const s of args.slice(1))
this.sink(s, `Object.assign into ⊤ at ${locLabel(site)}`);
this.sink(s, `Object.assign into ⊤ at ${this.loc(site)}`);
});
this.edge(arg(0), dst);
return;
@@ -3154,20 +3162,20 @@ class Analyzer {
case "Object.defineProperties":
case "Reflect.defineProperty":
this.ops.push({ kind: "defineProperty", recv: arg(0), name: null, node: site });
this.sink(arg(2), `property descriptor at ${locLabel(site)}`);
this.sink(arg(1), `property descriptor at ${locLabel(site)}`);
this.sink(arg(2), `property descriptor at ${this.loc(site)}`);
this.sink(arg(1), `property descriptor at ${this.loc(site)}`);
this.edge(arg(0), dst);
return;
case "Object.setPrototypeOf":
case "Reflect.setPrototypeOf":
this.ops.push({ kind: "setPrototypeOf", recv: arg(0), name: null, node: site });
this.sink(arg(1), `prototype at ${locLabel(site)}`);
this.sink(arg(1), `prototype at ${this.loc(site)}`);
this.edge(arg(0), dst);
return;
case "Object.create": {
const d = this.atoms.intern(`dict:${this.nodeKey(site)}`, () => ({
kind: "dictionary",
label: `Object.create@${locLabel(site)}`,
label: `Object.create@${this.loc(site)}`,
node: site,
}));
this.add(dst, d);
@@ -3176,7 +3184,7 @@ class Analyzer {
case "Object.fromEntries": {
const d = this.atoms.intern(`dict:${this.nodeKey(site)}`, () => ({
kind: "dictionary",
label: `fromEntries@${locLabel(site)}`,
label: `fromEntries@${this.loc(site)}`,
node: site,
}));
const pairs = this.newNode();
@@ -3225,7 +3233,7 @@ class Analyzer {
site,
);
else if (f.kind === "top")
this.sink(revived, `JSON.parse reviver at ${locLabel(site)}`);
this.sink(revived, `JSON.parse reviver at ${this.loc(site)}`);
});
this.edge(revived, dst);
return;
@@ -3252,7 +3260,7 @@ class Analyzer {
site,
);
else if (f.kind === "top")
this.sink(serialized, `JSON.stringify replacer at ${locLabel(site)}`);
this.sink(serialized, `JSON.stringify replacer at ${this.loc(site)}`);
});
prim("string");
prim("undefined");
@@ -3281,7 +3289,7 @@ class Analyzer {
if (c.kind === "builtin" && c.path === "Proxy")
this.ops.push({ kind: "proxy", recv: listed, name: null, node: site });
});
this.sink(listed, `argument of ${path} at ${locLabel(site)}`);
this.sink(listed, `argument of ${path} at ${this.loc(site)}`);
escapeAll();
return;
}
@@ -3310,23 +3318,23 @@ class Analyzer {
prim("bigint");
return;
case "Promise.resolve": {
const p = this.promiseAtom(site, locLabel(site));
const p = this.promiseAtom(site, this.loc(site));
if (args[0] !== undefined) this.awaitInto(args[0], this.field(p, "value"));
else this.add(this.field(p, "value"), this.prim("undefined"));
this.add(dst, p);
return;
}
case "Promise.reject": {
const p = this.promiseAtom(site, locLabel(site));
const p = this.promiseAtom(site, this.loc(site));
this.add(dst, p);
if (args[0] !== undefined) this.sink(args[0], `rejection at ${locLabel(site)}`);
if (args[0] !== undefined) this.sink(args[0], `rejection at ${this.loc(site)}`);
return;
}
case "Promise.all":
case "Promise.allSettled":
case "Promise.race":
case "Promise.any": {
const p = this.promiseAtom(site, locLabel(site));
const p = this.promiseAtom(site, this.loc(site));
const items = this.newNode();
if (args[0] !== undefined) this.iterateInto(args[0], items, site);
if (path === "Promise.all") {
@@ -3340,7 +3348,7 @@ class Analyzer {
return;
}
case "promise.resolver":
for (const x of args) this.sink(x, `promise resolution at ${locLabel(site)}`);
for (const x of args) this.sink(x, `promise resolution at ${this.loc(site)}`);
return;
}
if (path.startsWith("console.")) {