perf(javascript): skip nested callables during fingerprint traversal (#1871)
This commit is contained in:
parent
e4ac7bbfc2
commit
097f0d54af
|
|
@ -179,6 +179,34 @@ describe("JavaScript semantic analysis: calls 2", () => {
|
|||
).not.toBe(left.functionFingerprints[0]?.components.literalSetSha256);
|
||||
});
|
||||
|
||||
it("keeps nested callable syntax out of its owner's fingerprint", () => {
|
||||
const analyze = (nested: number, owned: number) =>
|
||||
analyzeJavaScriptSemantics(`
|
||||
function outer() {
|
||||
function inner() { return ${nested}; }
|
||||
const object = { method() { return ${nested}; } };
|
||||
class Nested { method() { return ${nested}; } }
|
||||
return ${owned};
|
||||
}
|
||||
`);
|
||||
const first = analyze(1, 2);
|
||||
const changedNested = analyze(3, 2);
|
||||
const changedOwner = analyze(1, 4);
|
||||
const syntax = (ir: typeof first) =>
|
||||
ir.functionFingerprints.map(({ components }) => ({
|
||||
ast: components.normalizedAstSha256,
|
||||
control: components.controlFlowSha256,
|
||||
literals: components.literalSetSha256,
|
||||
}));
|
||||
const original = syntax(first);
|
||||
expect(original).toHaveLength(4);
|
||||
expect(syntax(changedNested)[0]).toEqual(original[0]);
|
||||
for (const index of [1, 2, 3])
|
||||
expect(syntax(changedNested)[index]).not.toEqual(original[index]);
|
||||
expect(syntax(changedOwner)[0]).not.toEqual(original[0]);
|
||||
expect(syntax(changedOwner).slice(1)).toEqual(original.slice(1));
|
||||
});
|
||||
|
||||
it("keeps fingerprint relations ordered and effects scoped to their owner", () => {
|
||||
const ordered = analyzeJavaScriptSemantics(`
|
||||
function first() {}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,35 @@ it("traverses a large tree in steps with the synchronous visit order (#1462)", (
|
|||
expect(stepped).toEqual(synchronous);
|
||||
});
|
||||
|
||||
it("skips child subtrees without changing sibling ancestry or exit callbacks", () => {
|
||||
const parsed = parseJavaScriptSource("[1, [2], 3]", "selection.js");
|
||||
if (parsed === null) throw new Error("Expected parsed source");
|
||||
const entered: number[] = [];
|
||||
const exited: number[] = [];
|
||||
const parents: string[][] = [];
|
||||
const steps = traverseJavaScriptAstSteps(parsed.program, {
|
||||
skip: (node) => node.type === "ArrayExpression" && node.start === 4,
|
||||
enter: (node, parent, readAncestors) => {
|
||||
if (node.type !== "NumericLiteral") return;
|
||||
entered.push(node.value);
|
||||
expect(readAncestors().at(-1)).toBe(parent);
|
||||
parents.push(readAncestors().map(({ type }) => type));
|
||||
},
|
||||
exit: (node) => {
|
||||
if (node.type === "NumericLiteral") exited.push(node.value);
|
||||
},
|
||||
});
|
||||
while (steps.next().done !== true) {
|
||||
// Complete the same cooperative traversal used by fingerprint collection.
|
||||
}
|
||||
expect(entered).toEqual([1, 3]);
|
||||
expect(exited).toEqual([1, 3]);
|
||||
expect(parents).toEqual([
|
||||
["Program", "ExpressionStatement", "ArrayExpression"],
|
||||
["Program", "ExpressionStatement", "ArrayExpression"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves receiver and Electron alias scope across traversal pauses (#1462)", () => {
|
||||
const padding = Array.from(
|
||||
{ length: 400 },
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export function* collectJavaScriptSemanticFingerprintsSteps(
|
|||
const node = state.callableNodesById.get(callable.callableId);
|
||||
if (node === undefined) continue;
|
||||
yield;
|
||||
const syntax = yield* fingerprintSyntaxSteps(node, callable.callableId);
|
||||
const syntax = yield* fingerprintSyntaxSteps(node);
|
||||
const promiseEffects = promiseEffectsByOwner.get(callable.callableId) ?? [];
|
||||
const eventEffects = eventEffectsByOwner.get(callable.callableId) ?? [];
|
||||
const timerEffects = timerEffectsByOwner.get(callable.callableId) ?? [];
|
||||
|
|
@ -164,28 +164,19 @@ interface FingerprintSyntax {
|
|||
|
||||
function* fingerprintSyntaxSteps(
|
||||
root: t.Node,
|
||||
targetCallableId: string,
|
||||
): Generator<void, FingerprintSyntax> {
|
||||
const normalizedTokens: string[] = [];
|
||||
const controlFlowTokens: string[] = [];
|
||||
const literals: string[] = [];
|
||||
const callableStack: string[] = [];
|
||||
yield* traverseJavaScriptAstSteps(root, {
|
||||
skip: (node) => semanticCallableIdForNode(node) !== null,
|
||||
enter: (node, parent) => {
|
||||
const callableId = semanticCallableIdForNode(node);
|
||||
if (callableId !== null) callableStack.push(callableId);
|
||||
if (callableStack.at(-1) !== targetCallableId) return;
|
||||
normalizedTokens.push(normalizedNodeToken(node, parent));
|
||||
const control = controlFlowToken(node);
|
||||
if (control !== null) controlFlowTokens.push(control);
|
||||
const literal = semanticLiteral(node);
|
||||
if (literal !== null) literals.push(literal);
|
||||
},
|
||||
exit: (node) => {
|
||||
const callableId = semanticCallableIdForNode(node);
|
||||
if (callableId !== null && callableStack.at(-1) === callableId)
|
||||
callableStack.pop();
|
||||
},
|
||||
});
|
||||
return { normalizedTokens, controlFlowTokens, literals };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as t from "@babel/types";
|
||||
|
||||
/**
|
||||
* Visit every Babel AST node in deterministic source-tree order.
|
||||
* Visit Babel AST nodes in deterministic source-tree order, excluding skipped subtrees.
|
||||
*
|
||||
* `readAncestors` returns a snapshot during `enter`, outermost-first and
|
||||
* ending with the direct parent. Callers
|
||||
|
|
@ -17,6 +17,8 @@ export const traverseJavaScriptAst = (
|
|||
|
||||
/** Callbacks for one deterministic AST traversal. */
|
||||
export interface JavaScriptAstVisitor {
|
||||
/** Omit a child and its subtree without entering or exiting it. */
|
||||
readonly skip?: (node: t.Node) => boolean;
|
||||
readonly enter: (
|
||||
node: t.Node,
|
||||
parent: t.Node | null,
|
||||
|
|
@ -50,6 +52,7 @@ export function* traverseJavaScriptAstSteps(
|
|||
current.nextIndex += 1;
|
||||
if (visited % 1024 === 0) yield;
|
||||
visited += 1;
|
||||
if (visitor.skip?.(child) === true) continue;
|
||||
pending.push(enter(child, current.node));
|
||||
} else {
|
||||
visitor.exit?.(current.node, current.parent);
|
||||
|
|
|
|||
Loading…
Reference in New Issue