fix(javascript): align module lookup and source evidence

Preserve CommonJS literal paths and confined ESM URL semantics, including package main fallback, directory intent, Unicode identity, and present opaque modules. Parse extraction inputs at their AST/source owner, retain skipped HTML and source-map coverage, and distinguish endpoint candidates from proven receiver facts. Version structural fingerprints with computed literal contents and propagate them through worker contracts.

Reviewed fixes include #1810, #1840, #2642, #2700, #2702, #2714, #2803, #2960, #2968, #2986, #3035, and #3036.

Validation: 158 resolver tests passed; public CLI/MCP module lookup controls, package-main/Unicode workflows, and six builtin workflows passed in the current compiled lane. Worker schema/fingerprint regressions passed. Large public pdf.js completion and real Electron analysis remain unverified.
This commit is contained in:
morluto
2026-10-14 08:46:50 +08:00
parent 04672f2b91
commit b58adafa4f
63 changed files with 4066 additions and 326 deletions
@@ -71,7 +71,12 @@ export const inspectCall = (
kind: "dynamic-import",
specifier: chunkLoad.specifier,
});
for (const specifier of vitePreloadDependencySpecifiers(source, node, name))
for (const specifier of vitePreloadDependencySpecifiers(
node,
name,
context.vitePreloadDependencyTable,
bindings,
))
addReference(context, { node, kind: "dynamic-import", specifier });
const worker = t.isNewExpression(node) ? node : undefined;
const workerConstructor =
@@ -94,13 +99,19 @@ export const inspectCall = (
? undefined
: workerUrlSpecifier(worker, bindings)),
});
if (name.endsWith("serviceWorker.register"))
if (
name === "serviceWorker.register" ||
name.endsWith(".serviceWorker.register")
)
addReference(context, { node, kind: "service-worker", specifier: first });
inspectEndpointCall(node, name, first, context);
inspectStorageCall(node, name, first, context);
if (first === "audio" || first === "video")
noteMediaElementPreloads(node, accumulator);
if (name.endsWith("loadFile") && first !== undefined)
if (
(name === "loadFile" || name.endsWith(".loadFile")) &&
first !== undefined
)
addLocatedFinding(context, {
collection: accumulator.roles,
key: `role\0renderer\0${first}`,
@@ -114,7 +125,7 @@ export const inspectCall = (
location: range(node),
},
});
if (name.endsWith("loadURL") && first !== undefined)
if ((name === "loadURL" || name.endsWith(".loadURL")) && first !== undefined)
addLocatedFinding(context, {
collection: accumulator.roles,
key: `role\0renderer\0${first}`,
@@ -241,28 +252,156 @@ const chunkLoadSpecifier = (
};
const vitePreloadDependencySpecifiers = (
source: string,
node: JavaScriptCallLike,
name: string,
): readonly string[] => {
if (name !== "__vite__mapDeps" || !t.isArrayExpression(node.arguments[0]))
table: readonly (string | undefined)[] | null,
bindings: JavaScriptSemanticAnalysisState,
): readonly (string | undefined)[] => {
if (
name !== "__vite__mapDeps" ||
table === null ||
!t.isArrayExpression(node.arguments[0])
)
return [];
const indexes = node.arguments[0].elements.flatMap((element) =>
t.isNumericLiteral(element) ? [element.value] : [],
);
if (indexes.length !== node.arguments[0].elements.length) return [];
const table = vitePreloadDependencyTable(source);
return indexes.flatMap((index) => table[index] ?? []);
const indexes = node.arguments[0].elements;
// Explicit cache overrides change which table the helper reads. Retain the
// call slots as unknown unless every override is statically undefined.
if (
!node.arguments
.slice(1)
.every((argument) => isExplicitDefaultArgument(argument, bindings))
)
return indexes.map(() => undefined);
return indexes.map((element) => {
if (element === null || !t.isNumericLiteral(element)) return undefined;
const index = element.value;
return Number.isSafeInteger(index) && index >= 0 ? table[index] : undefined;
});
};
const vitePreloadDependencyTable = (source: string): readonly string[] => {
const match =
/\.\s*f\s*\|\|\s*\(\s*[^)]*?\.f\s*=\s*\[([\s\S]*?)\]\s*\)/u.exec(source);
const body = match?.[1];
if (body === undefined) return [];
return [
...body.matchAll(/"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'/gu),
].map((literal) => literal[1] ?? literal[2] ?? "");
const isExplicitDefaultArgument = (
argument: t.CallExpression["arguments"][number],
bindings: JavaScriptSemanticAnalysisState,
): boolean => {
if (!t.isNode(argument)) return false;
const value = unwrapJavaScriptExpression(argument).node;
return (
(t.isUnaryExpression(value) && value.operator === "void") ||
(t.isIdentifier(value, { name: "undefined" }) &&
isUnmodifiedGlobalReference(value, bindings, "undefined"))
);
};
type ViteMapDepsFunction =
| t.FunctionDeclaration
| t.FunctionExpression
| t.ArrowFunctionExpression;
/** Index the canonical Vite helper's cache initializer from parsed syntax. */
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);
}
});
if (helpers.length !== 1) return null;
const helper = helpers[0];
if (helper === undefined) return null;
const mapParameter = helper.params[1];
const mapIdentifier = t.isAssignmentPattern(mapParameter)
? mapParameter.left
: mapParameter;
if (!t.isIdentifier(mapIdentifier)) return [];
const tables: t.ArrayExpression[] = [];
visitJavaScriptNodes(
helper,
(node) => {
if (
t.isAssignmentExpression(node) &&
node.operator === "=" &&
(t.isMemberExpression(node.left) ||
t.isOptionalMemberExpression(node.left)) &&
t.isIdentifier(node.left.object, { name: mapIdentifier.name }) &&
semanticStaticPropertyName(node.left.property, node.left.computed) ===
"f" &&
t.isArrayExpression(node.right)
)
tables.push(node.right);
},
undefined,
true,
);
if (tables.length !== 1) return [];
const table = tables[0];
if (table === undefined) return [];
const dependencies = table.elements.map((element) =>
element === null || t.isSpreadElement(element)
? undefined
: stringValue(element),
);
let escapedOrMutated = false;
visitJavaScriptNodes(
program,
(node, parent) => {
if (!t.isIdentifier(node, { name: "__vite__mapDeps" })) return;
if (
(parent === helper &&
t.isFunctionDeclaration(parent) &&
parent.id === node) ||
(t.isVariableDeclarator(parent) &&
parent.init === helper &&
parent.id === node)
)
return;
if (
(t.isCallExpression(parent) || t.isOptionalCallExpression(parent)) &&
parent.callee === node
)
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 = (
@@ -292,8 +431,9 @@ const inspectEndpointCall = (
addEndpoint(context, {
node,
kind: "network",
value: endpoint,
value: endpoint.value,
mechanism: `call:${name}`,
...(endpoint.kind === "candidate" ? { interpretation: "candidate" } : {}),
});
};
@@ -401,12 +541,15 @@ export const addReference = (
const addEndpoint = (context: FindingContext, input: EndpointInput): void =>
addLocatedFinding(context, {
collection: context.accumulator.endpoints,
key: `endpoint\0${input.kind}\0${input.value}`,
key: `endpoint\0${input.kind}\0${input.interpretation ?? "literal"}\0${input.value}`,
node: input.node,
value: {
kind: input.kind,
value: sanitizeEndpointCandidate(input.value),
mechanism: input.mechanism,
...(input.interpretation === undefined
? {}
: { interpretation: input.interpretation }),
module_key: null,
location: range(input.node),
},