Files
reverse-engineering-anything/src/domain/referenceSourceRequireBindings.ts
T
morlutoandShuaixinHuang b46f4fcebd fix(javascript): preserve loader authority for exact suffixes (#9477)
* fix(javascript): require exact loader suffix case

* fix(javascript): share exact loader mode across import resolution

* test(process): align symlink fixture with filesystem checkpoints

* test(javascript): normalize the module resolution oracle path separators

* test(frida): assert the CLI spawn Evidence contract

* test(mcp): use an unsupported analysis-view parent

* test(browser): separate source-location line bounds from parity

* fix(javascript): preserve uncertain loader provenance

* test(javascript): cover uncertain require resolution

* fix(javascript): retain conditional Electron evidence

* fix(javascript): preserve timed semantic provenance

* fix(javascript): retain conditional builtin effects

* test(javascript): distinguish bundled host authority

* fix(javascript): preserve nested loader invocation identity

* test(javascript): qualify Electron fixture authority

* fix(javascript): preserve loader invocation authority

* fix(javascript): respect source order for resolver escapes

* fix(javascript): check nested loader callable mutations

* fix(javascript): retain conditional constructor requests

* fix(reference): retain constructor loader relationships

* fix(reference): follow nested loader callable paths

* refactor(javascript): share callable mutation interpretation

* fix(javascript): require exact package loader suffixes

* refactor(javascript): unify conditional relation evidence

* refactor(javascript): unify conditional node evidence

* fix(javascript): retain unknown authority for mixed case sources

* fix(javascript): preserve loader uncertainty for dotfiles

* fix(reference): classify javascript suffix dotfiles

* test(reference): cover suffix dotfiles through cli

* fix(javascript): retain equivalent conditional built-in origins

* fix(javascript): preserve wrapped resolver operation

* fix(javascript): retain nested conditional built-in receivers

* test(javascript): declare CommonJS in native addon fixtures

* fix(javascript): retain wrapped resolver candidates

* style: format merged native command schema

* fix(javascript): preserve uncertain loader authority

* test(javascript): require candidate preload evidence

* test(javascript): cover reference loader member writes

---------

Co-authored-by: ShuaixinHuang <1836656514@qq.com>
2026-11-09 13:21:24 +08:00

235 lines
8.4 KiB
TypeScript

import * as t from "@babel/types";
import { posix } from "node:path";
import {
semanticMemberChain,
semanticStaticPropertyKey,
unwrapJavaScriptExpression,
} from "./javascript/javascriptAstValues.js";
import { isUnmodifiedGlobalReference } from "./javascript/javascriptSemanticAmbientWrites.js";
import {
indexJavaScriptBindingsWithReceiversSteps,
type JavaScriptReceiverIndex,
} from "./javascript/javascriptSemanticAnalysis.js";
import type { JavaScriptSemanticAnalysisState } from "./javascript/javascriptSemanticState.js";
import {
semanticRequireCallAt,
semanticRequireLoaderAt,
semanticSelectedMemberMayHaveChanged,
type JavaScriptRequireCallFact,
} from "./javascript/javascriptSemanticRequireOrigin.js";
import {
completeSemanticSteps,
traverseJavaScriptAst,
} from "./javascript/javascriptSemanticTraversal.js";
import {
javaScriptRuntimeContext,
type ParsedJavaScriptSource,
} from "./javascript/javascriptSourceParser.js";
export type ReferenceSourceRequireResolutionBase =
| "source-file"
| { readonly kind: "source-relative-file"; readonly path: string }
| "unknown";
export interface ReferenceSourceRequireCall {
readonly arguments: JavaScriptRequireCallFact["arguments"];
readonly resolutionBase: ReferenceSourceRequireResolutionBase;
readonly limitation?: string;
}
export type ReferenceSourceRequireInvocation =
| t.CallExpression
| t.OptionalCallExpression
| t.NewExpression;
/** Identify exact calls through a source-owned Node loader and their resolver base. */
export const referenceSourceRequireCalls = (
file: Pick<ParsedJavaScriptSource, "program">,
path: string,
): ReadonlyMap<
ReferenceSourceRequireInvocation,
ReferenceSourceRequireCall
> => {
// Unmarked paths depend on package scope or compiler options. Preserve their
// candidate CommonJS edges; explicit module paths have no wrapper loader.
const runtimeContext = javaScriptRuntimeContext(file.program);
const index = completeSemanticSteps(
indexJavaScriptBindingsWithReceiversSteps(
file.program,
runtimeContext === "module" ? undefined : "commonjs-wrapper",
),
);
const factoryBindings: ReadonlySet<string> = new Set();
const candidates: {
readonly call: ReferenceSourceRequireInvocation;
readonly arguments: JavaScriptRequireCallFact["arguments"];
readonly resolutionBase: ReferenceSourceRequireResolutionBase;
readonly limitation?: string;
}[] = [];
traverseJavaScriptAst(file.program, {
enter: (node) => {
if (
!t.isCallExpression(node) &&
!t.isOptionalCallExpression(node) &&
!t.isNewExpression(node)
)
return;
if (callReceiverIdentifier(node.callee) === undefined) return;
let fact = semanticRequireLoaderAt(node, index.bindings, factoryBindings);
const callee = unwrapJavaScriptExpression(node.callee).node;
if (
fact === "create-require" ||
((t.isMemberExpression(callee) ||
t.isOptionalMemberExpression(callee)) &&
["call", "apply", "resolve"].includes(
semanticStaticPropertyKey(callee.property, callee.computed) ?? "",
))
) {
// Definitions identify a possible factory. Its mutable member reads
// need the shared write index before they can prove an association.
completeSemanticSteps(index.indexMemberMutationFactsSteps());
fact = semanticRequireLoaderAt(node, index.bindings, factoryBindings);
}
const callFact = semanticRequireCallAt(
node,
index.bindings,
factoryBindings,
index,
);
if (callFact === undefined || callFact.loader === "local") return;
if (
fact !== "create-require" &&
fact !== "commonjs-loader" &&
callFact.loader !== "create-require" &&
callFact.loader !== "commonjs-loader"
)
return;
const sourceRelativePath = callFact.sourceRelativePath;
const normalizedSourceRelativePath =
sourceRelativePath === undefined
? undefined
: posix.normalize(
posix.join(posix.dirname(path), sourceRelativePath),
);
const base: ReferenceSourceRequireResolutionBase =
runtimeContext === "unknown"
? "unknown"
: callFact.loader === "create-require"
? sourceRelativePath === undefined
? callFact.sourceRelative === true
? "source-file"
: "unknown"
: normalizedSourceRelativePath === undefined ||
normalizedSourceRelativePath === ".." ||
normalizedSourceRelativePath.startsWith("../")
? "unknown"
: normalizedSourceRelativePath === path
? "source-file"
: {
kind: "source-relative-file",
path: normalizedSourceRelativePath,
}
: callFact.loader === "commonjs-loader" &&
runtimeContext === "commonjs"
? "source-file"
: "unknown";
const searchBase = requireResolveSearchBase(
callFact.operation,
callFact.arguments,
index.bindings,
base,
runtimeContext === "commonjs",
);
candidates.push({
call: node,
arguments: callFact.arguments,
resolutionBase: searchBase,
...(runtimeContext === "unknown"
? {
limitation: `Module context is unknown in ${path}; the requested dependency was retained without a proven loader base.`,
}
: {}),
});
},
});
if (
candidates.some(
({ call }) =>
!t.isIdentifier(unwrapJavaScriptExpression(call.callee).node),
)
)
completeSemanticSteps(index.indexMemberMutationFactsSteps());
return new Map(
candidates.map(
({ call, arguments: callArguments, resolutionBase, limitation }) =>
[
call,
{
arguments: callArguments,
...(limitation === undefined ? {} : { limitation }),
resolutionBase:
!t.isIdentifier(unwrapJavaScriptExpression(call.callee).node) &&
hasPriorMemberMutation(index, call)
? "unknown"
: resolutionBase,
},
] as const,
),
);
};
/** `require.resolve` options replace the default search paths unless exact current-directory paths are supplied. */
const requireResolveSearchBase = (
operation: JavaScriptRequireCallFact["operation"],
callArguments: JavaScriptRequireCallFact["arguments"],
bindings: JavaScriptSemanticAnalysisState,
loaderBase: ReferenceSourceRequireResolutionBase,
hasCommonJsWrapper: boolean,
): ReferenceSourceRequireResolutionBase => {
const arguments_ = callArguments ?? [];
if (operation !== "resolve" || arguments_.length < 2) return loaderBase;
if (arguments_.length !== 2) return "unknown";
const options = t.isExpression(arguments_[1]) ? arguments_[1] : undefined;
if (options === undefined || !t.isObjectExpression(options)) return "unknown";
if (options.properties.length === 0) return loaderBase;
if (options.properties.length !== 1) return "unknown";
const property = options.properties[0];
if (!t.isObjectProperty(property)) return "unknown";
if (semanticStaticPropertyKey(property.key, property.computed) !== "paths")
return "unknown";
const value = t.isExpression(property.value) ? property.value : undefined;
if (!t.isArrayExpression(value) || value.elements.length !== 1)
return "unknown";
const directory = value.elements[0];
return hasCommonJsWrapper &&
t.isIdentifier(directory, { name: "__dirname" }) &&
isUnmodifiedGlobalReference(directory, bindings, "__dirname")
? "source-file"
: "unknown";
};
const hasPriorMemberMutation = (
index: JavaScriptReceiverIndex,
site: ReferenceSourceRequireInvocation,
): boolean => {
const callee = unwrapJavaScriptExpression(site.callee).node;
return (
semanticMemberChain(callee) === undefined ||
semanticSelectedMemberMayHaveChanged(index, site, callee)
);
};
const callReceiverIdentifier = (
callee: t.Node | null | undefined,
): t.Identifier | undefined => {
if (!t.isNode(callee)) return undefined;
let expression = unwrapJavaScriptExpression(callee).node;
while (
t.isMemberExpression(expression) ||
t.isOptionalMemberExpression(expression)
)
expression = unwrapJavaScriptExpression(expression.object).node;
return t.isIdentifier(expression) ? expression : undefined;
};