mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Count repeated calls through constructors, apply and per-value callbacks
The constant-graph pass treats a function whose call edges all run once at initialization as init-only. Some calls recorded no edge or a once-only one: - `new F()` of an ES5 constructor and `f.apply(thisArg, args)` bound the arguments without a call edge, so a function also called directly once was init-only even when a loop constructed or applied it. - Callbacks of Array.from, toSorted, replace and replaceAll run once per element, comparison or match, but those methods were missing from the repeating callback set. - JSON.parse revivers, JSON.stringify replacers and toJSON methods run once per value; their edges at the JSON call site now count as repeated, including toJSON calls resolved after the call's handling. Objects these functions write or allocate were reported as constant, and their reads as foldable. constant-graphs.test.ts covers each case.
This commit is contained in:
@@ -252,6 +252,10 @@ const LOOP_CALLBACK_METHODS: ReadonlySet<string> = new Set([
|
||||
"reduceRight",
|
||||
"flatMap",
|
||||
"sort",
|
||||
"toSorted",
|
||||
"from",
|
||||
"replace",
|
||||
"replaceAll",
|
||||
]);
|
||||
|
||||
export function analyzeShapes(program: ts.Program, checker: ts.TypeChecker): ShapeAnalysis {
|
||||
@@ -298,12 +302,17 @@ class Analyzer {
|
||||
this.siteCtx.set(site, { caller: this.currentFn, inLoop: this.loopDepth > 0 });
|
||||
}
|
||||
|
||||
/** Builtin calls that invoke their callbacks once per value
|
||||
* (JSON.parse revivers, JSON.stringify replacers and toJSON methods),
|
||||
* which may run after the call's own handling. */
|
||||
private readonly repeatingSites = new Set<Node>();
|
||||
|
||||
edgeFor(site: Node, callee: FunctionInfo): void {
|
||||
const ctx = this.siteCtx.get(site);
|
||||
this.calls.push({
|
||||
caller: ctx?.caller ?? null,
|
||||
callee,
|
||||
inLoop: (ctx?.inLoop ?? false) || this.forceLoop,
|
||||
inLoop: (ctx?.inLoop ?? false) || this.forceLoop || this.repeatingSites.has(site),
|
||||
site,
|
||||
});
|
||||
}
|
||||
@@ -2547,6 +2556,7 @@ class Analyzer {
|
||||
this.add(info.thisNode, inst);
|
||||
this.bindArgs(info, args, spread);
|
||||
this.edge(info.ret, dst);
|
||||
this.edgeFor(e, info);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -2894,6 +2904,7 @@ class Analyzer {
|
||||
for (const p of info.params)
|
||||
if (args[1] !== undefined) this.iterateInto(args[1], p, site);
|
||||
this.edge(info.ret, dst);
|
||||
this.edgeFor(site, info);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -3034,6 +3045,7 @@ class Analyzer {
|
||||
// result.
|
||||
const revived = this.newNode();
|
||||
this.add(revived, this.top);
|
||||
this.repeatingSites.add(site);
|
||||
if (args[1] !== undefined)
|
||||
this.on(args[1], (f) => {
|
||||
if (f.kind === "function")
|
||||
@@ -3057,6 +3069,7 @@ class Analyzer {
|
||||
// Their results are serialized in turn.
|
||||
const serialized = this.newNode();
|
||||
this.childrenInto(serialized);
|
||||
this.repeatingSites.add(site);
|
||||
this.edge(arg(0), serialized);
|
||||
const key = this.constNode(this.prim("string"));
|
||||
this.on(serialized, (v) => this.callToJSON(v, key, serialized, site));
|
||||
|
||||
@@ -104,3 +104,40 @@ test("constant graphs: namespaces, revealing modules and prototypes, with near m
|
||||
// A factory called from two init sites: one object per call site.
|
||||
expect(text).toMatch(/near miss ns-fixture\/index\.js:20: init-factory \(.*2 call sites\)/);
|
||||
});
|
||||
|
||||
// Functions that one call site runs repeatedly: an ES5 constructor and an
|
||||
// apply call in a loop, and builtin callbacks that run once per element,
|
||||
// match or parsed value.
|
||||
const repeatedSource = `
|
||||
var lib = {};
|
||||
lib.newState = { v: 0 };
|
||||
function bumpNew() { lib.newState.v++; }
|
||||
bumpNew();
|
||||
lib.applyState = { v: 0 };
|
||||
function bumpApply() { lib.applyState.v++; }
|
||||
bumpApply();
|
||||
lib.repeat = function () { for (var i = 0; i < 3; i++) { new bumpNew(); bumpApply.apply(null, []); } };
|
||||
lib.seen = { match: null, pair: null, mapped: null, key: null };
|
||||
lib.replaced = "a-b".replace(/\\\\w/g, function (m) { lib.seen.match = { m: m }; return m; });
|
||||
lib.sorted = [3, 1, 2].toSorted(function (a, b) { lib.seen.pair = { a: a }; return a - b; });
|
||||
lib.mapped = Array.from([1, 2], function (x) { lib.seen.mapped = { x: x }; return x; });
|
||||
lib.revived = JSON.parse('{"a":{"b":1}}', function (k, v) { lib.seen.key = { k: k }; return v; });
|
||||
module.exports = { lib };
|
||||
`;
|
||||
|
||||
test("constant graphs: constructors, apply calls and callbacks that one site runs repeatedly", async () => {
|
||||
await writeFile(join(dir, "node_modules", "ns-fixture", "index.js"), repeatedSource);
|
||||
await writeFile(
|
||||
join(dir, "main.cjs"),
|
||||
'const { lib } = require("ns-fixture");\nlib.repeat();\nconsole.log(lib.newState.v, lib.applyState.v, lib.replaced, lib.sorted, lib.mapped, lib.revived);\n',
|
||||
);
|
||||
const text = (await report()).join("\n");
|
||||
// `new bumpNew()` and `bumpApply.apply(...)` in a loop write after init.
|
||||
expect(text).toMatch(/near miss ns-fixture\/index\.js:3 \.v: written-after-init/);
|
||||
expect(text).toMatch(/near miss ns-fixture\/index\.js:6 \.v: written-after-init/);
|
||||
// Each callback runs once per match, comparison, element or parsed value.
|
||||
for (const field of ["match", "pair", "mapped", "key"])
|
||||
expect(text).toMatch(
|
||||
new RegExp(`near miss ns-fixture/index\\.js:10 \\.${field}: written-after-init`),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user