refactor(javascript): reuse canonical AST traversal

Remove the separate Vite walker and raw visitor-key require scan. Keep subtree selection at the shared traversal owner. All 103 focused builtin/Vite tests and the complete static and architecture checks pass.
This commit is contained in:
morluto 2026-10-14 08:51:32 +08:00
parent b58adafa4f
commit a769915f6e
2 changed files with 114 additions and 146 deletions

View File

@ -1,4 +1,5 @@
import * as t from "@babel/types";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { BUILTIN_RESOURCE_METHODS } from "./javascriptBuiltinResourceMethods.js";
import type { JavaScriptModuleOrigin } from "./javascriptSemanticIr.js";
@ -61,109 +62,105 @@ const writesModuleRequire = (
binding.initializers.every(({ node: init }) => isModuleObject(init, next))
);
};
const pending: t.Node[] = [state.program];
while (pending.length > 0) {
const node = pending.pop();
if (node === undefined) break;
const target = t.isAssignmentExpression(node)
? node.left
: t.isUpdateExpression(node) ||
t.isUnaryExpression(node, { operator: "delete" })
? node.argument
: undefined;
if (target !== undefined) {
const members: string[] = [];
let root: t.Node = target;
while (t.isMemberExpression(root) || t.isOptionalMemberExpression(root)) {
const key = semanticStaticPropertyKey(root.property, root.computed);
if (key === null) break;
members.unshift(key);
root = root.object;
}
if (
members.length === 1 &&
members[0] === "require" &&
isModuleObject(root)
)
writes.push(node);
else if (
members.length === 0 &&
(t.isMemberExpression(target) ||
t.isOptionalMemberExpression(target)) &&
target.computed &&
semanticStaticPropertyKey(target.property, true) === null &&
isModuleObject(target.object)
)
// An unknown computed key may be "require"; retain uncertainty rather
// than treating this direct delegate write as a proven unrelated slot.
writes.push(node);
}
if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
const invocation = interop.intrinsicInvocation(node);
const owner =
invocation?.path?.length === 3 ? invocation.path[1] : undefined;
const method = invocation?.path?.at(-1);
const args = invocation?.arguments;
if (
(owner === "Object" || owner === "Reflect") &&
method === "defineProperty" &&
isModuleObject(args?.[0] ?? t.nullLiteral())
) {
const key = interop.staticString(args?.[1]);
if (key === "require" || key === undefined) writes.push(node);
} else if (
owner === "Object" &&
method === "defineProperties" &&
isModuleObject(args?.[0] ?? t.nullLiteral())
) {
const descriptors = args?.[1];
traverseJavaScriptAst(state.program, {
enter: (node) => {
const target = t.isAssignmentExpression(node)
? node.left
: t.isUpdateExpression(node) ||
t.isUnaryExpression(node, { operator: "delete" })
? node.argument
: undefined;
if (target !== undefined) {
const members: string[] = [];
let root: t.Node = target;
while (
t.isMemberExpression(root) ||
t.isOptionalMemberExpression(root)
) {
const key = semanticStaticPropertyKey(root.property, root.computed);
if (key === null) break;
members.unshift(key);
root = root.object;
}
if (
!t.isObjectExpression(descriptors) ||
descriptors.properties.some(
(property) =>
t.isSpreadElement(property) ||
(t.isObjectProperty(property) &&
semanticStaticPropertyKey(property.key, property.computed) ===
"require"),
)
members.length === 1 &&
members[0] === "require" &&
isModuleObject(root)
)
writes.push(node);
} else if (
owner === "Object" &&
method === "assign" &&
args?.some((arg) => isModuleObject(arg))
) {
const target = args?.[0];
if (target !== undefined && isModuleObject(target)) {
const sourceHasRequire = args
?.slice(1)
.some(
(arg) =>
t.isObjectExpression(arg) &&
arg.properties.some(
(property) =>
t.isObjectProperty(property) &&
semanticStaticPropertyKey(
property.key,
property.computed,
) === "require",
),
);
else if (
members.length === 0 &&
(t.isMemberExpression(target) ||
t.isOptionalMemberExpression(target)) &&
target.computed &&
semanticStaticPropertyKey(target.property, true) === null &&
isModuleObject(target.object)
)
// An unknown computed key may be "require"; retain uncertainty rather
// than treating this direct delegate write as a proven unrelated slot.
writes.push(node);
}
if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
const invocation = interop.intrinsicInvocation(node);
const owner =
invocation?.path?.length === 3 ? invocation.path[1] : undefined;
const method = invocation?.path?.at(-1);
const args = invocation?.arguments;
if (
(owner === "Object" || owner === "Reflect") &&
method === "defineProperty" &&
isModuleObject(args?.[0] ?? t.nullLiteral())
) {
const key = interop.staticString(args?.[1]);
if (key === "require" || key === undefined) writes.push(node);
} else if (
owner === "Object" &&
method === "defineProperties" &&
isModuleObject(args?.[0] ?? t.nullLiteral())
) {
const descriptors = args?.[1];
if (
sourceHasRequire ||
args?.slice(1).some((arg) => !t.isObjectExpression(arg))
!t.isObjectExpression(descriptors) ||
descriptors.properties.some(
(property) =>
t.isSpreadElement(property) ||
(t.isObjectProperty(property) &&
semanticStaticPropertyKey(property.key, property.computed) ===
"require"),
)
)
writes.push(node);
} else if (
owner === "Object" &&
method === "assign" &&
args?.some((arg) => isModuleObject(arg))
) {
const target = args?.[0];
if (target !== undefined && isModuleObject(target)) {
const sourceHasRequire = args
?.slice(1)
.some(
(arg) =>
t.isObjectExpression(arg) &&
arg.properties.some(
(property) =>
t.isObjectProperty(property) &&
semanticStaticPropertyKey(
property.key,
property.computed,
) === "require",
),
);
if (
sourceHasRequire ||
args?.slice(1).some((arg) => !t.isObjectExpression(arg))
)
writes.push(node);
}
}
}
}
for (const key of t.VISITOR_KEYS[node.type] ?? []) {
const value = (node as unknown as Record<string, unknown>)[key];
if (Array.isArray(value)) {
for (const child of value) if (t.isNode(child)) pending.push(child);
} else if (t.isNode(value)) pending.push(value);
}
}
},
});
moduleRequireWrites.set(state, writes);
return writes;
};

View File

@ -1,4 +1,5 @@
import * as t from "@babel/types";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { htmlUrlText, stripQueryAndFragment } from "../artifactPathSyntax.js";
import { sanitizeEndpointCandidate } from "../browserObservation.js";
@ -302,17 +303,19 @@ export const indexVitePreloadDependencyTable = (
program: t.Program,
): readonly (string | undefined)[] | null => {
const helpers: ViteMapDepsFunction[] = [];
visitJavaScriptNodes(program, (node) => {
if (t.isFunctionDeclaration(node) && node.id?.name === "__vite__mapDeps")
helpers.push(node);
else if (
t.isVariableDeclarator(node) &&
t.isIdentifier(node.id, { name: "__vite__mapDeps" })
) {
const init = node.init;
if (t.isFunctionExpression(init) || t.isArrowFunctionExpression(init))
helpers.push(init);
}
traverseJavaScriptAst(program, {
enter: (node) => {
if (t.isFunctionDeclaration(node) && node.id?.name === "__vite__mapDeps")
helpers.push(node);
else if (
t.isVariableDeclarator(node) &&
t.isIdentifier(node.id, { name: "__vite__mapDeps" })
) {
const init = node.init;
if (t.isFunctionExpression(init) || t.isArrowFunctionExpression(init))
helpers.push(init);
}
},
});
if (helpers.length !== 1) return null;
const helper = helpers[0];
@ -323,9 +326,9 @@ export const indexVitePreloadDependencyTable = (
: mapParameter;
if (!t.isIdentifier(mapIdentifier)) return [];
const tables: t.ArrayExpression[] = [];
visitJavaScriptNodes(
helper,
(node) => {
traverseJavaScriptAst(helper, {
skip: (node) => t.isFunction(node),
enter: (node) => {
if (
t.isAssignmentExpression(node) &&
node.operator === "=" &&
@ -338,9 +341,7 @@ export const indexVitePreloadDependencyTable = (
)
tables.push(node.right);
},
undefined,
true,
);
});
if (tables.length !== 1) return [];
const table = tables[0];
if (table === undefined) return [];
@ -350,9 +351,9 @@ export const indexVitePreloadDependencyTable = (
: stringValue(element),
);
let escapedOrMutated = false;
visitJavaScriptNodes(
program,
(node, parent) => {
traverseJavaScriptAst(program, {
skip: (node) => node === helper,
enter: (node, parent) => {
if (!t.isIdentifier(node, { name: "__vite__mapDeps" })) return;
if (
(parent === helper &&
@ -370,40 +371,10 @@ export const indexVitePreloadDependencyTable = (
return;
escapedOrMutated = true;
},
helper,
);
});
return escapedOrMutated ? dependencies.map(() => undefined) : dependencies;
};
const visitJavaScriptNodes = (
root: t.Node,
visit: (node: t.Node, parent: t.Node | undefined) => void,
skip?: t.Node,
skipNestedFunctions = false,
): void => {
const pending: { readonly node: t.Node; readonly parent?: t.Node }[] = [
{ node: root },
];
while (pending.length > 0) {
const current = pending.pop();
if (current === undefined) continue;
const { node, parent } = current;
if (node === skip) continue;
visit(node, parent);
if (skipNestedFunctions && node !== root && t.isFunction(node)) continue;
const keys = t.VISITOR_KEYS[node.type] ?? [];
for (const key of keys) {
const child = (node as unknown as Record<string, unknown>)[key];
if (Array.isArray(child)) {
for (let index = child.length - 1; index >= 0; index -= 1) {
const item = child[index];
if (t.isNode(item)) pending.push({ node: item, parent: node });
}
} else if (t.isNode(child)) pending.push({ node: child, parent: node });
}
}
};
const inspectEndpointCall = (
node: JavaScriptCallLike,
name: string,