mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
ce89706145
* fix(javascript): preserve Electron options with static empty keys * fix(javascript): retain exact computed member keys
189 lines
5.9 KiB
TypeScript
189 lines
5.9 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;
|
|
};
|
|
|
|
/** Find the effective explicit property, preserving unresolved overrides. */
|
|
export const objectProperty = (
|
|
object: t.Node | undefined,
|
|
name: string,
|
|
):
|
|
| { readonly status: "missing" | "dynamic" }
|
|
| { readonly status: "explicit"; readonly property: t.ObjectProperty } => {
|
|
if (object === undefined) return { status: "missing" };
|
|
if (!t.isObjectExpression(object)) return { status: "dynamic" };
|
|
for (const property of object.properties.toReversed()) {
|
|
if (t.isSpreadElement(property)) return { status: "dynamic" };
|
|
const key = semanticStaticPropertyKey(property.key, property.computed);
|
|
if (key === null) return { status: "dynamic" };
|
|
if (key !== name) continue;
|
|
return t.isObjectProperty(property)
|
|
? { status: "explicit", property }
|
|
: { status: "dynamic" };
|
|
}
|
|
return { status: "missing" };
|
|
};
|
|
|
|
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";
|
|
}
|
|
|
|
/** 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 >= 8) {
|
|
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 };
|
|
};
|