fix(electron): consume shared native loader and export facts

This commit is contained in:
morluto committed 2026-10-24 02:35:51 +08:00
1 parent 75daf0c7df
commit 5a7bddb3ae
7 files changed
+254 -52

No files matched your search

@@ -15,14 +15,23 @@ import {
import { addLocatedFinding } from "./javascriptStaticAnalysisFindings.js";
import {
argumentNode,
calleeName,
range,
stringValue,
} from "./javascriptStaticAnalysisHelpers.js";
import {
propertyName,
semanticStaticPropertyKey,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import { semanticRequireCallAt } from "./javascriptSemanticRequireOrigin.js";
import {
commonJsRuntimeExportValues,
indexCommonJsExportReceiversSteps,
type CommonJsExportReceiverEffect,
type CommonJsExportSite,
} from "./javascriptCommonJsExportReceivers.js";
import type { JavaScriptReceiverIndex } from "./javascriptSemanticAnalysis.js";
import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js";
import type { JavaScriptFindingContext } from "./javascriptStaticAnalysisState.js";
interface NativeBindingInput {
@@ -44,10 +53,44 @@ export const inspectElectronNativeNode = (
if (t.isImportDeclaration(node)) inspectImport(node, context);
else if (t.isExportNamedDeclaration(node)) inspectNamedExport(node, context);
else if (t.isExportAllDeclaration(node)) inspectExportAll(node, context);
else if (t.isVariableDeclarator(node)) inspectRequireBinding(node, context);
else if (t.isAssignmentExpression(node)) inspectReExport(node, context);
else if (t.isVariableDeclarator(node) || t.isAssignmentExpression(node)) {
const value = nativeCallReceiver(
t.isVariableDeclarator(node) ? node.init : node.right,
);
// Admission is syntax only; the shared call owner proves loader and arguments later.
if (
value !== undefined &&
value.arguments.some((argument) =>
t.isArrayExpression(argument)
? argument.elements.some((element) =>
isNativeSpecifier(stringValue(element) ?? "", "require"),
)
: isNativeSpecifier(stringValue(argument) ?? "", "require"),
)
)
context.accumulator.pendingNativeAddonBindings.push(node);
}
};
/** Shared loader, argument and receiver facts settle before native projection. */
export function* finishElectronNativeBindingsSteps(
context: JavaScriptFindingContext,
index: JavaScriptReceiverIndex,
): Generator<void, void> {
const pending = context.accumulator.pendingNativeAddonBindings;
if (pending.length === 0) return;
yield* index.indexMemberMutationFactsSteps();
const receivers = pending.some((node) => t.isAssignmentExpression(node))
? yield* indexCommonJsExportReceiversSteps(index.bindings)
: new Map<CommonJsExportSite, CommonJsExportReceiverEffect>();
for (const node of pending) {
if (t.isVariableDeclarator(node))
inspectRequireBinding(node, context, index.bindings);
else inspectReExport(node, context, index.bindings, receivers.get(node));
}
pending.length = 0;
}
const inspectImport = (
node: t.ImportDeclaration,
context: JavaScriptFindingContext,
@@ -140,8 +183,9 @@ const inspectExportAll = (
const inspectRequireBinding = (
node: t.VariableDeclarator,
context: JavaScriptFindingContext,
bindings: JavaScriptSemanticAnalysisState,
): void => {
const required = nativeRequire(node.init);
const required = nativeRequire(node.init, context, bindings);
if (required === undefined) return;
addBinding({
context,
@@ -156,9 +200,20 @@ const inspectRequireBinding = (
const inspectReExport = (
node: t.AssignmentExpression,
context: JavaScriptFindingContext,
bindings: JavaScriptSemanticAnalysisState,
receiver: CommonJsExportReceiverEffect | undefined,
): void => {
const required = nativeRequire(node.right);
if (required === undefined || !isModuleExport(node.left)) return;
const exported = commonJsRuntimeExportValues(node, bindings);
if (exported.length === 0 || receiver === "detached") return;
if (receiver === "uncertain") {
context.accumulator.unknownFindings += 1;
return;
}
// The export owner leaves arithmetic/logical stores without a selected RHS.
const value = exported[0]?.value;
if (value == null) return;
const required = nativeRequire(value, context, bindings);
if (required === undefined) return;
addBinding({
context,
node,
@@ -198,8 +253,22 @@ const addBinding = (input: NativeBindingInput): void => {
});
};
const nativeCallReceiver = (
node: t.Node | null | undefined,
): t.CallExpression | t.OptionalCallExpression | undefined => {
if (node == null) return undefined;
node = unwrapJavaScriptExpression(node).node;
while (t.isMemberExpression(node) || t.isOptionalMemberExpression(node))
node = unwrapJavaScriptExpression(node.object).node;
return t.isCallExpression(node) || t.isOptionalCallExpression(node)
? node
: undefined;
};
const nativeRequire = (
node: t.Node | null | undefined,
context: JavaScriptFindingContext,
bindings: JavaScriptSemanticAnalysisState,
):
| {
readonly specifier: string;
@@ -207,6 +276,8 @@ const nativeRequire = (
readonly access: NativeMemberAccess;
}
| undefined => {
if (node == null) return undefined;
node = unwrapJavaScriptExpression(node).node;
let member: string | null = null;
let hasMemberAccess = false;
while (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
@@ -215,28 +286,41 @@ const nativeRequire = (
// Walking inward leaves the first member consumed from the addon itself.
member = property;
hasMemberAccess = true;
node = node.object;
node = unwrapJavaScriptExpression(node.object).node;
}
if (!t.isCallExpression(node)) return undefined;
const name = calleeName(node.callee);
if (!t.isCallExpression(node) && !t.isOptionalCallExpression(node))
return undefined;
const call = semanticRequireCallAt(
node,
bindings,
context.accumulator.factoryRequireBindingIds,
);
if (
name !== "require" &&
!name.endsWith(".require") &&
!name.includes("__webpack_require__")
call === undefined ||
call.loader === "local" ||
call.operation !== "load"
)
return undefined;
const specifier = stringValue(argumentNode(node.arguments[0]));
return specifier !== undefined && isNativeSpecifier(specifier, "require")
? {
specifier,
hasMemberAccess,
access: {
members: member === null ? [] : [member],
namespaceAccess: !hasMemberAccess,
dynamicMemberAccess: hasMemberAccess && member === null,
},
}
: undefined;
const specifier = stringValue(argumentNode(call.arguments?.[0]));
if (specifier === undefined || !isNativeSpecifier(specifier, "require"))
return undefined;
if (
call.loader === "unknown" ||
!call.sourceRelative ||
call.sourceRelativePath !== undefined
) {
context.accumulator.unknownFindings += 1;
return undefined;
}
return {
specifier,
hasMemberAccess,
access: {
members: member === null ? [] : [member],
namespaceAccess: !hasMemberAccess,
dynamicMemberAccess: hasMemberAccess && member === null,
},
};
};
interface NativeMemberAccess {
@@ -263,18 +347,6 @@ const bindingMembers = (pattern: t.Node): NativeMemberAccess => {
return { members, namespaceAccess, dynamicMemberAccess };
};
const isModuleExport = (node: t.Node): boolean => {
const keys: (string | null)[] = [];
while (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
keys.unshift(semanticStaticPropertyKey(node.property, node.computed));
node = node.object;
}
if (!t.isIdentifier(node)) return false;
return node.name === "exports"
? keys.length > 0
: node.name === "module" && keys[0] === "exports";
};
const isNativeSpecifier = (
specifier: string,
moduleKind: ElectronNativeAddonBindingFinding["module_kind"],