Files
rea/src/domain/javascript/electronStaticAnalysisValues.ts
T
Kaoru0822 04fd41b288 fix(electron): bind each contextBridge helper invocation to its own API (#6284)
Wrapper substitution keyed contextBridge findings only by the inner
exposure call's range, so every helper invocation replaced the previous
one while unioning its members: the last key survived with every
invocation's members. Substituted members also listed only immediate
object keys, dropping nested paths that direct syntax retains.

Record substituted exposures per site and per world and key, and replace
the site's syntax observation once after traversal. Invocations exposing
one key at one site describe one API covering each alternative object; a
traced invocation whose key, object or world stays unresolved or guarded
keeps the site's own unresolved observation. Enumerate member paths
through resolved objects with the direct syntax depth bound, which also
retains the depth-limit unknown that substitution previously dropped.

Fixes #5871
Fixes #5870

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:29:39 -07:00

174 lines
5.3 KiB
TypeScript

import * as t from "@babel/types";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import type { ElectronStaticValue } from "./electronStaticAnalysisTypes.js";
import {
readExactJavaScriptLiteral,
semanticStaticPropertyKey,
semanticStaticPropertyName,
} from "./javascriptAstValues.js";
import { sourceSlice } from "./javascriptStaticAnalysisHelpers.js";
import type { JavaScriptFindingContext } from "./javascriptStaticAnalysisState.js";
/** Resolve a callee only when its root has a proven Electron module identity. */
export const electronCalleePath = (
callee: t.Node,
context: JavaScriptFindingContext,
): readonly string[] | undefined => {
const members: string[] = [];
let root = callee;
while (t.isMemberExpression(root) || t.isOptionalMemberExpression(root)) {
const member = semanticStaticPropertyName(root.property, root.computed);
if (member === "" || !t.isNode(root.object)) return undefined;
members.push(member);
root = root.object;
}
const exported = context.electronBindings?.get(root.start ?? -1);
return exported === undefined
? undefined
: [...exported, ...members.toReversed()];
};
/** Preserve one literal value or the exact inert expression. */
export const electronStaticValue = (
source: string,
node: t.Node | null | undefined,
): ElectronStaticValue => {
const literal = literalValue(node);
return literal.found
? { status: "literal", value: literal.value, expression: null }
: {
status: "dynamic",
value: null,
expression: boundedExpression(source, node),
};
};
/** Return an actionable expression without evaluating it. */
export const boundedExpression = (
source: string,
node: t.Node | null | undefined,
): string => {
if (node === null || node === undefined) return "[missing-expression]";
const expression = sourceSlice(source, node).trim();
return expression === "" ? `[${node.type}]` : expression;
};
type PresentHandlerKind =
| "inline-function"
| "identifier"
| "member-expression"
| "dynamic-expression";
/** Classify a handler argument while retaining its exact source range. */
export function handlerKind(node: t.Node): PresentHandlerKind;
export function handlerKind(node: null | undefined): "missing";
export function handlerKind(
node: t.Node | null | undefined,
): PresentHandlerKind | "missing" {
if (node === null || node === undefined) return "missing";
if (
t.isArrowFunctionExpression(node) ||
t.isFunctionExpression(node) ||
t.isFunctionDeclaration(node)
)
return "inline-function";
if (t.isIdentifier(node)) return "identifier";
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node))
return "member-expression";
return "dynamic-expression";
}
/**
* Nesting bound for dotted contextBridge member paths. Deeper objects count as
* one unknown member rather than being enumerated.
*/
export const contextBridgeMemberPathDepth = 8;
/** Collect statically visible dotted keys from one literal contextBridge API object. */
export const collectContextBridgeMembers = (
node: t.Node | null | undefined,
): {
readonly status: "object-literal" | "dynamic" | "missing";
readonly members: readonly string[];
readonly unknown: number;
} => {
if (node === null || node === undefined)
return {
status: "missing",
members: [],
unknown: 0,
};
if (!t.isObjectExpression(node))
return {
status: "dynamic",
members: [],
unknown: 1,
};
const state: { members: string[]; unknown: number } = {
members: [],
unknown: 0,
};
collectMembersAt(node, [], state);
const members = [...new Set(state.members)].sort(compareUnicodeCodePoints);
return {
status: "object-literal",
members,
unknown: state.unknown,
};
};
const collectMembersAt = (
object: t.ObjectExpression,
prefix: readonly string[],
state: { members: string[]; unknown: number },
): void => {
if (prefix.length >= contextBridgeMemberPathDepth) {
state.unknown += 1;
return;
}
for (const property of object.properties) {
if (t.isSpreadElement(property)) {
state.unknown += 1;
continue;
}
const name = semanticStaticPropertyKey(property.key, property.computed);
if (name === null) {
state.unknown += 1;
continue;
}
if (
t.isObjectProperty(property) &&
!property.computed &&
!property.shorthand &&
name === "__proto__"
)
continue;
const segments = [...prefix, name];
const path = segments.join(".");
state.members.push(path);
if (t.isObjectProperty(property) && t.isObjectExpression(property.value))
collectMembersAt(property.value, segments, state);
}
};
const literalValue = (
node: t.Node | null | undefined,
):
| { readonly found: true; readonly value: string | number | boolean | null }
| { readonly found: false } => {
if (node === null || node === undefined) return { found: false };
const literal = readExactJavaScriptLiteral(node);
if (literal.found) return literal;
if (
t.isUnaryExpression(node, { operator: "-" }) &&
t.isNumericLiteral(node.argument)
) {
const value = -node.argument.value;
return Number.isFinite(value) && !Object.is(value, -0)
? { found: true, value }
: { found: false };
}
return { found: false };
};