diff --git a/src/domain/javascript/javascriptSemanticRequireOrigin.ts b/src/domain/javascript/javascriptSemanticRequireOrigin.ts index a4a153597..b11d64af2 100644 --- a/src/domain/javascript/javascriptSemanticRequireOrigin.ts +++ b/src/domain/javascript/javascriptSemanticRequireOrigin.ts @@ -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)[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; }; diff --git a/src/domain/javascript/javascriptStaticAnalysisCalls.ts b/src/domain/javascript/javascriptStaticAnalysisCalls.ts index b031aea9e..b9bc3c96e 100644 --- a/src/domain/javascript/javascriptStaticAnalysisCalls.ts +++ b/src/domain/javascript/javascriptStaticAnalysisCalls.ts @@ -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)[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,