mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
952 lines
31 KiB
TypeScript
952 lines
31 KiB
TypeScript
import * as t from "@babel/types";
|
|
import { isIntrinsicMapLookup } from "./javascriptSemanticMapOperations.js";
|
|
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
|
|
|
import { htmlUrlText, stripQueryAndFragment } from "../artifactPathSyntax.js";
|
|
import { sanitizeEndpointCandidate } from "../browserObservation.js";
|
|
import {
|
|
addFindingOnce,
|
|
addLocatedFinding,
|
|
} from "./javascriptStaticAnalysisFindings.js";
|
|
import {
|
|
argumentNode,
|
|
argumentValue,
|
|
calleeName,
|
|
endpointArgument as endpointArgumentHelper,
|
|
prefixedArgument,
|
|
range,
|
|
rangeForOffsets,
|
|
sourceSlice,
|
|
staticPath,
|
|
staticPathResolutionContext,
|
|
storageKind,
|
|
stringValue,
|
|
} from "./javascriptStaticAnalysisHelpers.js";
|
|
import {
|
|
semanticStaticPropertyName,
|
|
unwrapJavaScriptExpression,
|
|
} from "./javascriptAstValues.js";
|
|
import type {
|
|
JavaScriptAnalysisAccumulator as AnalysisAccumulator,
|
|
JavaScriptEndpointInput as EndpointInput,
|
|
JavaScriptFindingContext as FindingContext,
|
|
JavaScriptReferenceInput as ReferenceInput,
|
|
} from "./javascriptStaticAnalysisState.js";
|
|
import {
|
|
resolveSemanticBindingState,
|
|
semanticStableInitializer,
|
|
type JavaScriptBindingIndex,
|
|
type JavaScriptSemanticAnalysisState,
|
|
} from "./javascriptSemanticState.js";
|
|
import {
|
|
isUnmodifiedGlobalReference,
|
|
isUnmodifiedImportMetaUrl,
|
|
} from "./javascriptSemanticAmbientWrites.js";
|
|
import {
|
|
evaluateSemanticExpression,
|
|
evaluateSemanticExpressionProvenance,
|
|
} from "./javascriptSemanticValues.js";
|
|
import { semanticTopLevelEffectFollowsCapture } from "./javascriptSemanticMutationInitializers.js";
|
|
import {
|
|
semanticFactoryLoaderMemberAt,
|
|
semanticRequireCallAt,
|
|
semanticModuleRequireCallAt,
|
|
semanticModuleRequireReceiver,
|
|
} from "./javascriptSemanticRequireOrigin.js";
|
|
|
|
type JavaScriptCallLike =
|
|
| t.CallExpression
|
|
| t.OptionalCallExpression
|
|
| t.NewExpression;
|
|
|
|
/** Inspect one call or new expression for references, endpoints, storage, and roles. */
|
|
export const inspectCall = (
|
|
source: string,
|
|
node: JavaScriptCallLike,
|
|
context: FindingContext,
|
|
bindings: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
const { accumulator } = context;
|
|
const name = calleeName(node.callee);
|
|
const first = stringValue(node.arguments[0]);
|
|
const deferredModuleRequire =
|
|
semanticModuleRequireReceiver(node) !== undefined ||
|
|
semanticModuleRequireCallAt(node, bindings, false);
|
|
if (deferredModuleRequire) accumulator.pendingModuleRequireCalls.push(node);
|
|
const requireCall = deferredModuleRequire
|
|
? null
|
|
: requireCallSpecifier(node, bindings, context);
|
|
if (requireCall !== null)
|
|
addReference(context, {
|
|
node,
|
|
kind: "require",
|
|
specifier: requireCall.specifier,
|
|
});
|
|
const chunkLoad = chunkLoadSpecifier(node, bindings, context);
|
|
if (chunkLoad !== null)
|
|
addReference(context, {
|
|
node,
|
|
kind: "dynamic-import",
|
|
specifier: chunkLoad.specifier,
|
|
});
|
|
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 =
|
|
worker === undefined
|
|
? undefined
|
|
: workerConstructorKind(worker.callee, bindings);
|
|
if (
|
|
workerConstructor !== undefined ||
|
|
name === "Worker" ||
|
|
name.endsWith(".Worker")
|
|
)
|
|
addReference(context, {
|
|
node,
|
|
kind: "worker",
|
|
// A string path is relative to the document base URL in a browser and
|
|
// to the process cwd in Node; the analyzed source's artifact location
|
|
// establishes neither. Only `new URL(path, import.meta.url)` names a
|
|
// location relative to this module.
|
|
specifier:
|
|
workerConstructor === undefined || worker === undefined
|
|
? undefined
|
|
: moduleUrlSpecifier(worker, bindings),
|
|
});
|
|
// Service worker strings also use the document base URL. The complete called
|
|
// path must retain its platform identity, including the register method.
|
|
if (
|
|
!t.isNewExpression(node) &&
|
|
isUnmodifiedGlobalReference(node.callee, bindings, [
|
|
"navigator",
|
|
"serviceWorker",
|
|
"register",
|
|
])
|
|
)
|
|
addReference(context, {
|
|
node,
|
|
kind: "service-worker",
|
|
specifier: moduleUrlSpecifier(node, bindings),
|
|
});
|
|
inspectEndpointCall(node, name, first, context, bindings);
|
|
inspectStorageCall(node, name, first, context);
|
|
if (first === "audio" || first === "video")
|
|
noteMediaElementPreloads(node, accumulator);
|
|
if (
|
|
(name === "loadFile" || name.endsWith(".loadFile")) &&
|
|
first !== undefined
|
|
)
|
|
addLocatedFinding(context, {
|
|
collection: accumulator.roles,
|
|
key: `role\0renderer\0${first}`,
|
|
node,
|
|
value: {
|
|
role: "renderer",
|
|
path: first,
|
|
resolution_context: "filesystem-expression",
|
|
mechanism: `call:${name}`,
|
|
module_key: null,
|
|
location: range(node),
|
|
},
|
|
});
|
|
if ((name === "loadURL" || name.endsWith(".loadURL")) && first !== undefined)
|
|
addLocatedFinding(context, {
|
|
collection: accumulator.roles,
|
|
key: `role\0renderer\0${first}`,
|
|
node,
|
|
value: {
|
|
role: "renderer",
|
|
path: first,
|
|
resolution_context: "module-specifier",
|
|
mechanism: `call:${name}`,
|
|
module_key: null,
|
|
location: range(node),
|
|
},
|
|
});
|
|
};
|
|
|
|
const moduleUrlSpecifier = (
|
|
call: JavaScriptCallLike,
|
|
bindings: JavaScriptSemanticAnalysisState,
|
|
): string | undefined => {
|
|
const argumentExpression = argumentNode(call.arguments[0]);
|
|
if (argumentExpression === undefined) return undefined;
|
|
const node = unwrapJavaScriptExpression(argumentExpression).node;
|
|
if (!t.isNewExpression(node)) return undefined;
|
|
const callee = unwrapJavaScriptExpression(node.callee).node;
|
|
if (
|
|
!isUnmodifiedGlobalReference(callee, bindings, "URL") ||
|
|
!isUnmodifiedImportMetaUrl(argumentNode(node.arguments[1]), bindings)
|
|
)
|
|
return undefined;
|
|
const specifier = stringValue(node.arguments[0]);
|
|
if (specifier === undefined) return undefined;
|
|
const pathname = stripQueryAndFragment(specifier);
|
|
// The module URL's scheme and filesystem/origin root are unavailable. Only
|
|
// relative pathnames can establish a location within the artifact; retain
|
|
// rooted paths and paths requiring URL text normalization as expressions.
|
|
// Malformed percent escapes survive HTTP URL parsing but cannot be converted
|
|
// from a file URL to a Node Worker path. Without the base scheme, keep that
|
|
// ambiguity unknown; query and fragment bytes do not affect pathname lookup.
|
|
return pathname.length > 0 &&
|
|
!pathname.startsWith("/") &&
|
|
!/%(?![0-9a-f]{2})/iu.test(pathname) &&
|
|
stripQueryAndFragment(htmlUrlText(specifier)) === pathname
|
|
? specifier
|
|
: undefined;
|
|
};
|
|
|
|
const workerConstructorKind = (
|
|
callee: t.Node,
|
|
bindings: JavaScriptSemanticAnalysisState,
|
|
): "browser" | "node" | undefined => {
|
|
const node = unwrapJavaScriptExpression(callee).node;
|
|
const member =
|
|
t.isMemberExpression(node) &&
|
|
semanticStaticPropertyName(node.property, node.computed) === "Worker";
|
|
const root = unwrapJavaScriptExpression(member ? node.object : node).node;
|
|
if (!t.isIdentifier(root)) return undefined;
|
|
const browserGlobal = member
|
|
? ["globalThis", "window", "self"].includes(root.name)
|
|
: root.name === "Worker";
|
|
if (browserGlobal && isUnmodifiedGlobalReference(node, bindings, "Worker"))
|
|
return "browser";
|
|
const binding = resolveSemanticBindingState(bindings, root, root.name);
|
|
const [origin] = binding?.directOrigins ?? [];
|
|
// Named ESM exports and module namespace exports are read-only. Default
|
|
// imports and CommonJS objects can replace Worker, so their identity is unknown.
|
|
return binding?.runtimeImport === true &&
|
|
binding.definitions.length === 1 &&
|
|
binding.directOrigins.length === 1 &&
|
|
origin !== undefined &&
|
|
(origin.specifier === "worker_threads" ||
|
|
origin.specifier === "node:worker_threads") &&
|
|
(member
|
|
? origin.importedPath.length === 0
|
|
: origin.importedPath.length === 1 && origin.importedPath[0] === "Worker")
|
|
? "node"
|
|
: undefined;
|
|
};
|
|
|
|
// Element factories such as jsx("audio", props) and createElement("video",
|
|
// props) carry the HTML media `preload` hint ("none", "metadata", "auto"),
|
|
// which never names an Electron preload script.
|
|
const noteMediaElementPreloads = (
|
|
node: JavaScriptCallLike,
|
|
accumulator: AnalysisAccumulator,
|
|
): void => {
|
|
const props = node.arguments[1];
|
|
if (!t.isObjectExpression(props)) return;
|
|
for (const property of props.properties)
|
|
if (
|
|
t.isObjectProperty(property) &&
|
|
semanticStaticPropertyName(property.key, property.computed) === "preload"
|
|
)
|
|
accumulator.mediaElementPreloads.add(property);
|
|
};
|
|
|
|
const requireCallSpecifier = (
|
|
node: JavaScriptCallLike,
|
|
bindings: JavaScriptSemanticAnalysisState,
|
|
context: FindingContext,
|
|
): { readonly specifier: string | undefined } | null => {
|
|
const call = semanticRequireCallAt(
|
|
node,
|
|
bindings,
|
|
context.accumulator.factoryRequireBindingIds,
|
|
);
|
|
if (call === undefined || call.loader === "local") return null;
|
|
if (call.loader === "unknown" || !call.sourceRelative) {
|
|
context.accumulator.unknownFindings += 1;
|
|
return { specifier: undefined };
|
|
}
|
|
return { specifier: argumentValue(call.arguments?.[0]) };
|
|
};
|
|
|
|
/** Examine member-loader receivers only after shared alias mutation facts settle. */
|
|
export const finishModuleRequireCalls = (
|
|
context: FindingContext,
|
|
bindings: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
for (const node of context.accumulator.pendingModuleRequireCalls) {
|
|
const call = requireCallSpecifier(node, bindings, context);
|
|
if (call !== null)
|
|
addReference(context, {
|
|
node,
|
|
kind: "require",
|
|
specifier: call.specifier,
|
|
});
|
|
}
|
|
context.accumulator.pendingModuleRequireCalls.length = 0;
|
|
context.accumulator.references.sort(
|
|
(left, right) => left.offset - right.offset,
|
|
);
|
|
};
|
|
|
|
const chunkLoadSpecifier = (
|
|
node: JavaScriptCallLike,
|
|
bindings: JavaScriptSemanticAnalysisState,
|
|
context: FindingContext,
|
|
): { readonly specifier: string | undefined } | null => {
|
|
const loader = semanticFactoryLoaderMemberAt(
|
|
node,
|
|
bindings,
|
|
context.accumulator.factoryRequireBindingIds,
|
|
"e",
|
|
);
|
|
if (loader === undefined) return null;
|
|
if (loader === "unknown") return { specifier: undefined };
|
|
return { specifier: prefixedArgument("chunk:", node.arguments[0]) };
|
|
};
|
|
|
|
const vitePreloadDependencySpecifiers = (
|
|
node: JavaScriptCallLike,
|
|
name: string,
|
|
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;
|
|
// 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 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[] = [];
|
|
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];
|
|
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[] = [];
|
|
traverseJavaScriptAst(helper, {
|
|
skip: (node) => t.isFunction(node),
|
|
enter: (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);
|
|
},
|
|
});
|
|
if (tables.length !== 1) return [];
|
|
const table = tables[0];
|
|
if (table === undefined) return [];
|
|
// A spread contributes an unknown number of entries, so every later slot
|
|
// has an unknown index; leave those out instead of shifting them.
|
|
const spread = table.elements.findIndex((element) =>
|
|
t.isSpreadElement(element),
|
|
);
|
|
const dependencies = (
|
|
spread === -1 ? table.elements : table.elements.slice(0, spread)
|
|
).map((element) =>
|
|
element === null || t.isSpreadElement(element)
|
|
? undefined
|
|
: stringValue(element),
|
|
);
|
|
let escapedOrMutated = false;
|
|
traverseJavaScriptAst(program, {
|
|
skip: (node) => node === helper,
|
|
enter: (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;
|
|
},
|
|
});
|
|
return escapedOrMutated ? dependencies.map(() => undefined) : dependencies;
|
|
};
|
|
|
|
const inspectEndpointCall = (
|
|
node: JavaScriptCallLike,
|
|
name: string,
|
|
first: string | undefined,
|
|
context: FindingContext,
|
|
bindings: JavaScriptSemanticAnalysisState,
|
|
): void => {
|
|
if (isKnownNetworkRequest(node, bindings)) {
|
|
const target = argumentNode(node.arguments[0]);
|
|
if (target === undefined) return;
|
|
const literal = stringValue(target);
|
|
if (literal !== undefined) {
|
|
addEndpoint(context, {
|
|
node,
|
|
kind: "network",
|
|
value: literal,
|
|
mechanism: `call:${name}`,
|
|
});
|
|
return;
|
|
}
|
|
const reference = requestReferenceNode(target);
|
|
const unwrapped = unwrapJavaScriptExpression(reference).node;
|
|
if (
|
|
t.isIdentifier(unwrapped) ||
|
|
t.isMemberExpression(unwrapped) ||
|
|
t.isOptionalMemberExpression(unwrapped)
|
|
) {
|
|
context.accumulator.pendingNetworkCalls.push({ node, name });
|
|
return;
|
|
}
|
|
addPartialNetworkEndpoint(context, node, name, target);
|
|
return;
|
|
}
|
|
// A local or shadowed function with a familiar network name is not a known
|
|
// browser request API. Do not let the literal fallback turn it into one.
|
|
if (isNetworkRequestSpelling(name)) return;
|
|
if (first !== undefined && /(?:^|\.)(?:get|delete)$/u.test(name)) {
|
|
// Apply existing literal exclusions before requesting whole-source alias
|
|
// mutation facts. Inline receivers and unbound names need no such index.
|
|
if (
|
|
endpointArgumentHelper(
|
|
name,
|
|
node.arguments,
|
|
node.callee,
|
|
context.openReceiverFacts?.get(node.start ?? -1),
|
|
) === undefined
|
|
)
|
|
return;
|
|
const mapLookup = isIntrinsicMapLookup(node, bindings);
|
|
if (mapLookup === true) return;
|
|
if (mapLookup === undefined) {
|
|
context.accumulator.pendingNetworkCalls.push({ node, name });
|
|
return;
|
|
}
|
|
}
|
|
inspectLiteralEndpointCall(node, name, context);
|
|
};
|
|
|
|
const inspectLiteralEndpointCall = (
|
|
node: JavaScriptCallLike,
|
|
name: string,
|
|
context: FindingContext,
|
|
): void => {
|
|
const receiverFact = context.openReceiverFacts?.get(node.start ?? -1);
|
|
const endpoint = endpointArgumentHelper(
|
|
name,
|
|
node.arguments,
|
|
node.callee,
|
|
receiverFact,
|
|
);
|
|
if (endpoint !== undefined)
|
|
addEndpoint(context, {
|
|
node,
|
|
kind: "network",
|
|
value: endpoint.value,
|
|
mechanism: `call:${name}`,
|
|
...(endpoint.kind === "candidate" ? { interpretation: "candidate" } : {}),
|
|
});
|
|
};
|
|
|
|
const isNetworkRequestSpelling = (name: string): boolean =>
|
|
name === "fetch" ||
|
|
name === "WebSocket" ||
|
|
name === "EventSource" ||
|
|
name === "sendBeacon" ||
|
|
name === "window.fetch" ||
|
|
name === "globalThis.fetch" ||
|
|
name === "self.fetch" ||
|
|
name === "navigator.sendBeacon";
|
|
|
|
/** Confirm that the call resolves to a built-in network API at this source site. */
|
|
const isKnownNetworkRequest = (
|
|
node: JavaScriptCallLike,
|
|
bindings: JavaScriptSemanticAnalysisState,
|
|
): boolean => {
|
|
if (
|
|
(t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
|
|
isUnmodifiedGlobalReference(node.callee, bindings, "fetch")
|
|
)
|
|
return true;
|
|
if (
|
|
t.isNewExpression(node) &&
|
|
(isUnmodifiedGlobalReference(node.callee, bindings, "WebSocket") ||
|
|
isUnmodifiedGlobalReference(node.callee, bindings, "EventSource"))
|
|
)
|
|
return true;
|
|
return (
|
|
(t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
|
|
isUnmodifiedGlobalReference(node.callee, bindings, [
|
|
"navigator",
|
|
"sendBeacon",
|
|
])
|
|
);
|
|
};
|
|
|
|
/** Finish queued targets after shared member-mutation facts are indexed. */
|
|
export const finishPartialNetworkEndpoints = (
|
|
context: FindingContext,
|
|
bindings: JavaScriptSemanticAnalysisState,
|
|
index: JavaScriptBindingIndex,
|
|
): void => {
|
|
for (const { node, name } of context.accumulator.pendingNetworkCalls) {
|
|
if (!isKnownNetworkRequest(node, bindings)) {
|
|
if (!isIntrinsicMapLookup(node, bindings, index))
|
|
inspectLiteralEndpointCall(node, name, context);
|
|
continue;
|
|
}
|
|
const target = argumentNode(node.arguments[0]);
|
|
if (target === undefined) continue;
|
|
const resolved = resolvedRequestTarget(target, node, bindings, index);
|
|
if (resolved !== undefined)
|
|
addEndpoint(context, {
|
|
node,
|
|
kind: "network",
|
|
value: resolved,
|
|
mechanism: `call:${name}`,
|
|
});
|
|
else addPartialNetworkEndpoint(context, node, name, target);
|
|
}
|
|
context.accumulator.pendingNetworkCalls.length = 0;
|
|
context.accumulator.endpoints.sort(
|
|
(left, right) => left.offset - right.offset,
|
|
);
|
|
};
|
|
|
|
const addPartialNetworkEndpoint = (
|
|
context: FindingContext,
|
|
node: JavaScriptCallLike,
|
|
name: string,
|
|
target: t.Node,
|
|
): void =>
|
|
addEndpoint(context, {
|
|
node,
|
|
kind: "network",
|
|
// Partial values preserve the original expression text, byte for byte.
|
|
value: sourceSlice(context.source, target),
|
|
mechanism: `call:${name}`,
|
|
resolution: "partial",
|
|
});
|
|
|
|
const resolvedRequestTarget = (
|
|
node: t.Node,
|
|
site: t.Node,
|
|
bindings: JavaScriptSemanticAnalysisState,
|
|
index: JavaScriptBindingIndex,
|
|
): string | undefined => {
|
|
const reference = requestReferenceNode(node);
|
|
// A toString result is selected before the enclosing request invocation.
|
|
const expression = unwrapJavaScriptExpression(node).node;
|
|
const capture = reference === expression ? site : expression;
|
|
if (index.referenceMayHaveChangedAt(capture, reference)) return undefined;
|
|
if (!requestInitializerPrecedesSite(reference, capture, bindings, index))
|
|
return undefined;
|
|
const evaluated = evaluateSemanticExpression(node, bindings);
|
|
if (evaluated.status === "literal" && typeof evaluated.value === "string")
|
|
return evaluated.value;
|
|
return urlObjectRequestTarget(node, capture, bindings, index);
|
|
};
|
|
|
|
const requestInitializerPrecedesSite = (
|
|
reference: t.Node,
|
|
site: t.Node,
|
|
bindings: JavaScriptSemanticAnalysisState,
|
|
index: JavaScriptBindingIndex,
|
|
): boolean => {
|
|
const binding = index.bindingForReferenceAt(reference);
|
|
const initializer = semanticStableInitializer(binding, bindings);
|
|
return (
|
|
initializer !== undefined &&
|
|
semanticTopLevelEffectFollowsCapture(
|
|
initializer.node,
|
|
site,
|
|
bindings.parentsByNode,
|
|
)
|
|
);
|
|
};
|
|
|
|
const requestReferenceNode = (node: t.Node): t.Node => {
|
|
const target = unwrapJavaScriptExpression(node).node;
|
|
if (
|
|
(t.isCallExpression(target) || t.isOptionalCallExpression(target)) &&
|
|
target.arguments.length === 0
|
|
) {
|
|
const callee = unwrapJavaScriptExpression(target.callee).node;
|
|
if (
|
|
(t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
|
|
semanticStaticPropertyName(callee.property, callee.computed) ===
|
|
"toString"
|
|
)
|
|
return unwrapJavaScriptExpression(callee.object).node;
|
|
}
|
|
return target;
|
|
};
|
|
|
|
/** Resolve an immutable URL object or alias only while shared effects say it is unchanged. */
|
|
const urlObjectRequestTarget = (
|
|
node: t.Node,
|
|
site: t.Node,
|
|
bindings: JavaScriptSemanticAnalysisState,
|
|
index: JavaScriptBindingIndex,
|
|
seen = new Set<string>(),
|
|
): string | undefined => {
|
|
let target = unwrapJavaScriptExpression(node).node;
|
|
if (
|
|
(t.isCallExpression(target) || t.isOptionalCallExpression(target)) &&
|
|
target.arguments.length === 0
|
|
) {
|
|
const callee = unwrapJavaScriptExpression(target.callee).node;
|
|
if (
|
|
(t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
|
|
semanticStaticPropertyName(callee.property, callee.computed) ===
|
|
"toString"
|
|
)
|
|
target = unwrapJavaScriptExpression(callee.object).node;
|
|
}
|
|
if (!t.isIdentifier(target) || index.referenceMayHaveChangedAt(site, target))
|
|
return undefined;
|
|
const binding = index.bindingForReferenceAt(target);
|
|
if (binding === undefined || seen.has(binding.bindingId)) return undefined;
|
|
seen.add(binding.bindingId);
|
|
const stable = semanticStableInitializer(binding, bindings);
|
|
if (
|
|
stable === undefined ||
|
|
!semanticTopLevelEffectFollowsCapture(
|
|
stable.node,
|
|
site,
|
|
bindings.parentsByNode,
|
|
)
|
|
)
|
|
return undefined;
|
|
const initializer = stable.node;
|
|
if (initializer === undefined) return undefined;
|
|
const initialized = unwrapJavaScriptExpression(initializer).node;
|
|
if (t.isIdentifier(initialized))
|
|
return urlObjectRequestTarget(initialized, site, bindings, index, seen);
|
|
if (!t.isNewExpression(initialized)) return undefined;
|
|
if (!isUnmodifiedGlobalReference(initialized.callee, bindings, "URL"))
|
|
return undefined;
|
|
if (
|
|
["URL", "Object"].some((owner) =>
|
|
["toString", "valueOf"].some((method) =>
|
|
bindings.ambientWrites.covers(["global", owner, "prototype", method]),
|
|
),
|
|
)
|
|
)
|
|
return undefined;
|
|
const input = stringValue(initialized.arguments[0]);
|
|
if (input === undefined) return undefined;
|
|
if (initialized.arguments.length === 1) {
|
|
try {
|
|
return new URL(input).href;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
if (initialized.arguments.length !== 2) return undefined;
|
|
const base = stringValue(initialized.arguments[1]);
|
|
if (base === undefined) return undefined;
|
|
try {
|
|
return new URL(input, base).href;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
/** Record tRPC procedures, tRPC link URLs and named GraphQL operations. */
|
|
export const inspectRpcNode = (node: t.Node, context: FindingContext): void => {
|
|
const rpc = context.rpcScan.inspect(node);
|
|
if (rpc !== undefined)
|
|
addEndpoint(context, { node, kind: "network", ...rpc });
|
|
};
|
|
|
|
/** Resolve deferred Source bodies after their document owners have been seen. */
|
|
export function* finishRpcScanSteps(
|
|
context: FindingContext,
|
|
): Generator<void, void> {
|
|
for (const rpc of context.rpcScan.finish()) {
|
|
if (rpc !== undefined) addEndpoint(context, { kind: "network", ...rpc });
|
|
yield;
|
|
}
|
|
}
|
|
|
|
export const inspectRoleProperty = (
|
|
node: t.ObjectProperty,
|
|
context: FindingContext,
|
|
): void => {
|
|
if (semanticStaticPropertyName(node.key, node.computed) !== "preload") return;
|
|
if (context.accumulator.mediaElementPreloads.has(node)) return;
|
|
const path = staticPath(node.value, context.nodePathResolve);
|
|
if (path === undefined) return;
|
|
addLocatedFinding(context, {
|
|
collection: context.accumulator.roles,
|
|
key: `role\0preload\0${path}`,
|
|
node,
|
|
value: {
|
|
role: "preload",
|
|
path,
|
|
resolution_context: staticPathResolutionContext(
|
|
node.value,
|
|
context.nodePathResolve,
|
|
),
|
|
mechanism: "property:preload",
|
|
module_key: null,
|
|
location: range(node),
|
|
},
|
|
});
|
|
};
|
|
|
|
const inspectStorageCall = (
|
|
node: JavaScriptCallLike,
|
|
name: string,
|
|
first: string | undefined,
|
|
context: FindingContext,
|
|
): void => {
|
|
if (name === "Database" || name.endsWith(".Database")) {
|
|
context.accumulator.pendingSqliteCalls.push({ node, name });
|
|
return;
|
|
}
|
|
const receiverFact = context.openReceiverFacts?.get(node.start ?? -1);
|
|
const storage =
|
|
receiverFact === "local" ||
|
|
receiverFact === "local-indexed-db" ||
|
|
receiverFact === "local-cache-storage"
|
|
? undefined
|
|
: receiverFact === "indexed-db"
|
|
? "indexed-db"
|
|
: receiverFact === "cache-storage"
|
|
? "cache-storage"
|
|
: storageKind(name);
|
|
if (storage === undefined) return;
|
|
addLocatedFinding(context, {
|
|
collection: context.accumulator.storage,
|
|
key: `storage\0${storage}\0${first ?? ""}`,
|
|
node,
|
|
value: {
|
|
kind: storage,
|
|
name: first ?? null,
|
|
mechanism: `call:${name}`,
|
|
module_key: null,
|
|
location: range(node),
|
|
},
|
|
});
|
|
};
|
|
|
|
/** Constructor spelling selects a candidate; source provenance owns certainty. */
|
|
export const finishSqliteStorageCalls = (
|
|
context: FindingContext,
|
|
bindings: JavaScriptSemanticAnalysisState,
|
|
index: JavaScriptBindingIndex,
|
|
): void => {
|
|
for (const { node, name } of context.accumulator.pendingSqliteCalls) {
|
|
const callee = unwrapJavaScriptExpression(node.callee).node;
|
|
const provenance = evaluateSemanticExpressionProvenance(callee, bindings);
|
|
const changed = index.referenceMayHaveChangedAt(node, callee);
|
|
if (!changed && t.isIdentifier(callee) && provenance.status === "local")
|
|
continue;
|
|
const origin =
|
|
provenance.status === "module" ? provenance.origins[0] : undefined;
|
|
const path = origin?.importedPath;
|
|
const imported = origin?.resolutionMode !== "require";
|
|
const known =
|
|
!changed &&
|
|
origin !== undefined &&
|
|
((origin.specifier === "better-sqlite3" &&
|
|
((origin.resolutionMode === "require" && path?.length === 0) ||
|
|
(imported && path?.length === 1 && path[0] === "default"))) ||
|
|
(origin.specifier === "sqlite3" &&
|
|
((path?.length === 1 && path[0] === "Database") ||
|
|
(imported &&
|
|
path?.length === 2 &&
|
|
(path[0] === "default" || path[0] === "*") &&
|
|
path[1] === "Database"))));
|
|
const first = stringValue(node.arguments[0]);
|
|
addLocatedFinding(context, {
|
|
collection: context.accumulator.storage,
|
|
key: `storage\0sqlite\0${known ? "api" : "candidate"}\0${first ?? ""}`,
|
|
node,
|
|
value: {
|
|
kind: "sqlite",
|
|
name: first ?? null,
|
|
mechanism: `call:${name}`,
|
|
...(known ? {} : { interpretation: "candidate" as const }),
|
|
module_key: null,
|
|
location: range(node),
|
|
},
|
|
});
|
|
}
|
|
context.accumulator.pendingSqliteCalls.length = 0;
|
|
context.accumulator.storage.sort((left, right) => left.offset - right.offset);
|
|
};
|
|
|
|
export const addReference = (
|
|
context: FindingContext,
|
|
input: ReferenceInput,
|
|
): void => {
|
|
const specifier = input.specifier ?? null;
|
|
const reference =
|
|
specifier === null
|
|
? {
|
|
specifier: null,
|
|
expression: sourceSlice(context.source, input.node),
|
|
}
|
|
: { specifier, expression: null };
|
|
addLocatedFinding(context, {
|
|
collection: context.accumulator.references,
|
|
key: `reference\0${input.kind}\0${reference.specifier ?? reference.expression}`,
|
|
node: input.node,
|
|
value: {
|
|
kind: input.kind,
|
|
...reference,
|
|
module_key: null,
|
|
location: range(input.node),
|
|
},
|
|
});
|
|
};
|
|
|
|
export const addEndpoint = (
|
|
context: FindingContext,
|
|
input: EndpointInput,
|
|
): void =>
|
|
addLocatedFinding(context, {
|
|
collection: context.accumulator.endpoints,
|
|
key: `endpoint\0${input.kind}\0${input.interpretation ?? "literal"}\0${input.resolution ?? "exact"}\0${input.value}`,
|
|
node: input.node,
|
|
value: {
|
|
kind: input.kind,
|
|
value:
|
|
input.resolution === "partial"
|
|
? input.value
|
|
: sanitizeEndpointCandidate(input.value),
|
|
mechanism: input.mechanism,
|
|
...(input.interpretation === undefined
|
|
? {}
|
|
: { interpretation: input.interpretation }),
|
|
...(input.resolution === undefined
|
|
? {}
|
|
: { resolution: input.resolution }),
|
|
module_key: null,
|
|
location: range(input.node),
|
|
},
|
|
});
|
|
|
|
export const addSourceMapDirectives = (
|
|
source: string,
|
|
comments: readonly t.Comment[],
|
|
accumulator: AnalysisAccumulator,
|
|
): void => {
|
|
// Read real comment tokens only; quoted text can contain identical markers.
|
|
// Keep both current/legacy line annotations and minifier block annotations.
|
|
for (const comment of comments) {
|
|
const start = comment.start;
|
|
const end = comment.end;
|
|
if (
|
|
start === undefined ||
|
|
start === null ||
|
|
end === undefined ||
|
|
end === null
|
|
)
|
|
continue;
|
|
// The parser owns comment boundaries. Exclude only the closing block
|
|
// delimiter; an asterisk elsewhere remains part of the declared URL.
|
|
const commentSource = source.slice(
|
|
start,
|
|
comment.type === "CommentBlock" ? end - 2 : end,
|
|
);
|
|
if (
|
|
!(commentSource.startsWith("//") || commentSource.startsWith("/*")) ||
|
|
!(commentSource[2] === "#" || commentSource[2] === "@")
|
|
)
|
|
continue;
|
|
// Single-class searches avoid regex backtracking over unbounded whitespace
|
|
// or inline URLs, which can exhaust the engine stack on two-byte strings.
|
|
const nameOffset = commentSource.slice(3).search(/\S/u);
|
|
if (nameOffset === -1) continue;
|
|
const nameStart = 3 + nameOffset;
|
|
if (!commentSource.startsWith("sourceMappingURL", nameStart)) continue;
|
|
const nameEnd = nameStart + "sourceMappingURL".length;
|
|
const equalsOffset = commentSource.slice(nameEnd).search(/\S/u);
|
|
if (equalsOffset === -1) continue;
|
|
const equalsStart = nameEnd + equalsOffset;
|
|
if (!commentSource.startsWith("=", equalsStart)) continue;
|
|
const urlOffset = commentSource.slice(equalsStart + 1).search(/\S/u);
|
|
if (urlOffset === -1) continue;
|
|
const declaredStart = equalsStart + 1 + urlOffset;
|
|
const terminator = commentSource.slice(declaredStart).search(/\s/u);
|
|
const declaredEnd =
|
|
terminator === -1 ? commentSource.length : declaredStart + terminator;
|
|
if (declaredEnd === declaredStart) continue;
|
|
const declared = commentSource.slice(declaredStart, declaredEnd);
|
|
const location = rangeForOffsets(source, start, start + declaredEnd);
|
|
addFindingOnce(accumulator, `source-map\0${declared}`, () =>
|
|
accumulator.sourceMaps.push({ declared_url: declared, location }),
|
|
);
|
|
}
|
|
};
|