mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(javascript): preserve exact static property keys (#3480)
* fix(javascript): preserve Electron options with static empty keys * fix(javascript): retain exact computed member keys
This commit is contained in:
@@ -112,3 +112,137 @@ describe("Electron option evidence", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Electron empty option keys", () => {
|
||||
describe.each([
|
||||
["plain empty string", '[""]'],
|
||||
["empty template", "[``]"],
|
||||
["wrapped empty string", '["" as const]'],
|
||||
])("unrelated %s option keys", (_label, key) => {
|
||||
it("retains an earlier explicit webPreferences object", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
const { BrowserWindow } = require("electron");
|
||||
new BrowserWindow({
|
||||
webPreferences: { preload: "./kept.js", sandbox: true },
|
||||
${key}: true
|
||||
});
|
||||
`);
|
||||
|
||||
expect(analysis.parse_error_count).toBe(0);
|
||||
expect(analysis.parse_status).toBe("complete");
|
||||
expect(analysis.electron.browser_windows).toMatchObject([
|
||||
{
|
||||
web_preferences_status: "object-literal",
|
||||
preload_path: "./kept.js",
|
||||
preload_resolution_context: "module-specifier",
|
||||
web_preferences: [
|
||||
{
|
||||
name: "preload",
|
||||
value: { status: "literal", value: "./kept.js" },
|
||||
},
|
||||
{ name: "sandbox", value: { status: "literal", value: true } },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("retains an earlier explicit preload within webPreferences", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
const { BrowserWindow } = require("electron");
|
||||
new BrowserWindow({ webPreferences: {
|
||||
preload: "./kept.js", sandbox: true, ${key}: false
|
||||
} });
|
||||
`);
|
||||
|
||||
expect(analysis.parse_error_count).toBe(0);
|
||||
expect(analysis.parse_status).toBe("complete");
|
||||
expect(analysis.electron.browser_windows).toMatchObject([
|
||||
{
|
||||
web_preferences_status: "object-literal",
|
||||
preload_path: "./kept.js",
|
||||
preload_resolution_context: "module-specifier",
|
||||
web_preferences: [
|
||||
{ name: "", value: { status: "literal", value: false } },
|
||||
{
|
||||
name: "preload",
|
||||
value: { status: "literal", value: "./kept.js" },
|
||||
},
|
||||
{ name: "sandbox", value: { status: "literal", value: true } },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("retains an earlier explicit utility-process service name", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
const { utilityProcess } = require("electron");
|
||||
utilityProcess.fork("./worker.js", [], {
|
||||
serviceName: "kept", ${key}: true
|
||||
});
|
||||
`);
|
||||
|
||||
expect(analysis.parse_error_count).toBe(0);
|
||||
expect(analysis.parse_status).toBe("complete");
|
||||
expect(analysis.electron.utility_processes).toMatchObject([
|
||||
{ module_path: "./worker.js", service_name: "kept" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["dynamic key", "[key]: null"],
|
||||
["spread", "...options"],
|
||||
])(
|
||||
"keeps a preceding %s override unknown after an unrelated empty template key",
|
||||
(_label, override) => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
const { BrowserWindow, utilityProcess } = require("electron");
|
||||
new BrowserWindow({
|
||||
webPreferences: { preload: "./maybe.js" },
|
||||
${override}, [\`\`]: true
|
||||
});
|
||||
new BrowserWindow({ webPreferences: {
|
||||
preload: "./maybe.js", ${override}, [\`\`]: true
|
||||
} });
|
||||
utilityProcess.fork("./worker.js", [], {
|
||||
serviceName: "maybe", ${override}, [\`\`]: true
|
||||
});
|
||||
`);
|
||||
|
||||
expect(analysis.parse_error_count).toBe(0);
|
||||
expect(analysis.parse_status).toBe("partial");
|
||||
expect(analysis.electron.browser_windows).toMatchObject([
|
||||
{
|
||||
web_preferences_status: "dynamic",
|
||||
web_preferences: [],
|
||||
preload_path: null,
|
||||
preload_resolution_context: null,
|
||||
},
|
||||
{
|
||||
web_preferences_status: "object-literal",
|
||||
preload_path: null,
|
||||
preload_resolution_context: null,
|
||||
},
|
||||
]);
|
||||
expect(analysis.electron.browser_windows[1]?.web_preferences).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
name: "",
|
||||
value: { status: "literal", value: true, expression: null },
|
||||
},
|
||||
{
|
||||
name: "preload",
|
||||
value: { status: "literal", value: "./maybe.js", expression: null },
|
||||
},
|
||||
expect.objectContaining({
|
||||
name: expect.stringMatching(/^\[(?:dynamic|spread)@/u),
|
||||
value: expect.objectContaining({ status: "dynamic", value: null }),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(analysis.electron.utility_processes).toMatchObject([
|
||||
{ module_path: "./worker.js", service_name: null },
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -65,9 +65,8 @@ export const objectProperty = (
|
||||
if (!t.isObjectExpression(object)) return { status: "dynamic" };
|
||||
for (const property of object.properties.toReversed()) {
|
||||
if (t.isSpreadElement(property)) return { status: "dynamic" };
|
||||
const key = semanticStaticPropertyName(property.key, property.computed);
|
||||
if (key === "" && !t.isStringLiteral(property.key))
|
||||
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 }
|
||||
|
||||
@@ -134,11 +134,19 @@ it("keeps a shadowed require local through a deep member initializer", () => {
|
||||
|
||||
it.each([
|
||||
["root.first.last()", "root.first.last"],
|
||||
["root[`first`].last()", "root.first.last"],
|
||||
['root["first" as const].last()', "root.first.last"],
|
||||
['root[""].last()', "root..last"],
|
||||
["root[``].last()", "root..last"],
|
||||
['root["" as const].last()', "root..last"],
|
||||
['("")[""].last()', "last"],
|
||||
['("")[""]()', ""],
|
||||
["root[0].last()", "root.0.last"],
|
||||
["root[0x1n].last()", "root.1.last"],
|
||||
["root[-1n].last()", "root.-1.last"],
|
||||
["root[key].last()", "root.[computed@5].last"],
|
||||
["root[`first${key}`].last()", "root.[computed@5].last"],
|
||||
["root[`\\unicode`].last()", "root.[computed@5].last"],
|
||||
["root?.first.last()", "root.first.last"],
|
||||
[`root${ordinaryMemberChain}.last()`, `root${ordinaryMemberChain}.last`],
|
||||
])("preserves exact callee syntax (case %#)", (source, expected) => {
|
||||
|
||||
@@ -2,6 +2,173 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
||||
|
||||
it("retains endpoint observations for exact template and TypeScript member keys", () => {
|
||||
for (const [get, beacon] of [
|
||||
['"get"', '"sendBeacon"'],
|
||||
["`get`", "`sendBeacon`"],
|
||||
['"get" as const', '"sendBeacon" as const'],
|
||||
]) {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
import * as vscode from "vscode";
|
||||
client[${get}]("/api/items");
|
||||
navigator[${beacon}]("/api/telemetry", payload);
|
||||
client[${get}]("setting");
|
||||
new Headers()[${get}]("content-type");
|
||||
new URLSearchParams()[${get}]("q");
|
||||
vscode.workspace.getConfiguration("editor")[${get}]("tabSize");
|
||||
`);
|
||||
|
||||
expect.soft(analysis.parse_error_count).toBe(0);
|
||||
expect.soft(analysis.parse_status).toBe("complete");
|
||||
expect
|
||||
.soft(
|
||||
analysis.endpoints.map(({ kind, value, mechanism, interpretation }) => [
|
||||
kind,
|
||||
value,
|
||||
mechanism,
|
||||
interpretation ?? null,
|
||||
]),
|
||||
)
|
||||
.toEqual([
|
||||
["network", "/api/items", "call:client.get", null],
|
||||
["network", "/api/telemetry", "call:navigator.sendBeacon", null],
|
||||
["network", "setting", "call:client.get", "candidate"],
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves preload and utility paths through exact computed join members", () => {
|
||||
for (const key of ['"join"', "`join`", '"join" as const']) {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
const { BrowserWindow, utilityProcess } = require("electron");
|
||||
const path = require("node:path");
|
||||
new BrowserWindow({ webPreferences: {
|
||||
preload: path[${key}](__dirname, "preload.js")
|
||||
} });
|
||||
utilityProcess.fork(path[${key}](__dirname, "worker.js"), [], {
|
||||
serviceName: "kept"
|
||||
});
|
||||
`);
|
||||
|
||||
expect.soft(analysis.parse_error_count).toBe(0);
|
||||
expect.soft(analysis.parse_status).toBe("complete");
|
||||
expect
|
||||
.soft(
|
||||
analysis.role_paths.map(
|
||||
({ role, path, resolution_context, mechanism }) => [
|
||||
role,
|
||||
path,
|
||||
resolution_context,
|
||||
mechanism,
|
||||
],
|
||||
),
|
||||
)
|
||||
.toEqual([
|
||||
["preload", "preload.js", "filesystem-expression", "property:preload"],
|
||||
]);
|
||||
expect
|
||||
.soft(
|
||||
analysis.electron.browser_windows.map(
|
||||
({ location: _location, ...finding }) => finding,
|
||||
),
|
||||
)
|
||||
.toEqual([
|
||||
{
|
||||
options_status: "object-literal",
|
||||
web_preferences_status: "object-literal",
|
||||
web_preferences: [
|
||||
{
|
||||
name: "preload",
|
||||
value: {
|
||||
status: "literal",
|
||||
value: "preload.js",
|
||||
expression: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
preload_path: "preload.js",
|
||||
preload_resolution_context: "filesystem-expression",
|
||||
module_key: null,
|
||||
},
|
||||
]);
|
||||
expect
|
||||
.soft(
|
||||
analysis.electron.utility_processes.map(
|
||||
({ location: _location, ...finding }) => finding,
|
||||
),
|
||||
)
|
||||
.toEqual([
|
||||
{
|
||||
module_path: "worker.js",
|
||||
module_resolution_context: "filesystem-expression",
|
||||
module_expression: null,
|
||||
service_name: "kept",
|
||||
module_key: null,
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps dynamic, interpolated, and invalid template keys unresolved", () => {
|
||||
for (const { key, errors } of [
|
||||
{ key: "method", errors: 0 },
|
||||
{ key: "`join${suffix}`", errors: 0 },
|
||||
{ key: ["`", String.raw`\unicode`, "`"].join(""), errors: 4 },
|
||||
]) {
|
||||
const preload = `path[${key}](__dirname, "preload.js")`;
|
||||
const worker = `path[${key}](__dirname, "worker.js")`;
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
const { BrowserWindow, utilityProcess } = require("electron");
|
||||
const path = require("node:path");
|
||||
client[${key}]("/api/items");
|
||||
navigator[${key}]("/api/telemetry", payload);
|
||||
new BrowserWindow({ webPreferences: { preload: ${preload} } });
|
||||
utilityProcess.fork(${worker}, [], { serviceName: "kept" });
|
||||
`);
|
||||
|
||||
expect.soft(analysis.parse_error_count).toBe(errors);
|
||||
expect.soft(analysis.parse_status).toBe("partial");
|
||||
expect.soft(analysis.endpoints).toEqual([]);
|
||||
expect.soft(analysis.role_paths).toEqual([]);
|
||||
expect
|
||||
.soft(
|
||||
analysis.electron.browser_windows.map(
|
||||
({ location: _location, ...finding }) => finding,
|
||||
),
|
||||
)
|
||||
.toEqual([
|
||||
{
|
||||
options_status: "object-literal",
|
||||
web_preferences_status: "object-literal",
|
||||
web_preferences: [
|
||||
{
|
||||
name: "preload",
|
||||
value: { status: "dynamic", value: null, expression: preload },
|
||||
},
|
||||
],
|
||||
preload_path: null,
|
||||
preload_resolution_context: null,
|
||||
module_key: null,
|
||||
},
|
||||
]);
|
||||
expect
|
||||
.soft(
|
||||
analysis.electron.utility_processes.map(
|
||||
({ location: _location, ...finding }) => finding,
|
||||
),
|
||||
)
|
||||
.toEqual([
|
||||
{
|
||||
module_path: null,
|
||||
module_resolution_context: null,
|
||||
module_expression: worker,
|
||||
service_name: "kept",
|
||||
module_key: null,
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
describe("static callee boundary matching", () => {
|
||||
it("matches complete callee members without suffix collisions", () => {
|
||||
const analysis = analyzeJavaScriptStaticSource(`
|
||||
|
||||
@@ -693,11 +693,8 @@ export const calleeName = (node: t.Node): string => {
|
||||
const memberPropertyName = (
|
||||
node: t.MemberExpression | t.OptionalMemberExpression,
|
||||
): string =>
|
||||
node.computed &&
|
||||
!t.isStringLiteral(node.property) &&
|
||||
!t.isNumericLiteral(node.property)
|
||||
? `[computed@${String(node.property.start ?? -1)}]`
|
||||
: semanticStaticPropertyName(node.property, node.computed);
|
||||
semanticStaticPropertyKey(node.property, node.computed) ??
|
||||
(node.computed ? `[computed@${String(node.property.start ?? -1)}]` : "");
|
||||
|
||||
/**
|
||||
* Return a string literal value without evaluating an expression. A template
|
||||
|
||||
@@ -23,6 +23,7 @@ const absoluteParts = [
|
||||
|
||||
const anchoredExpressions = [
|
||||
['path.resolve(__dirname, "preload.cjs")', "preload.cjs"],
|
||||
['path[`resolve`](__dirname, "preload.js")', "preload.js"],
|
||||
['path.posix.resolve("ignored", __dirname, "preload.js")', "preload.js"],
|
||||
['path.resolve("/ignored", __dirname, "preload.js")', "preload.js"],
|
||||
[
|
||||
@@ -45,6 +46,8 @@ const anchoredExpressions = [
|
||||
] as const;
|
||||
|
||||
const unknownExpressions = [
|
||||
'path[method](__dirname, "preload.js")',
|
||||
'path[`join${suffix}`](__dirname, "preload.js")',
|
||||
"path.resolve()",
|
||||
"path.posix.resolve()",
|
||||
'path.resolve("sub", "../preload.js")',
|
||||
@@ -106,6 +109,11 @@ describe("inert JavaScript path resolution", () => {
|
||||
"preload.js",
|
||||
"filesystem-expression",
|
||||
],
|
||||
[
|
||||
'path[`join`](__dirname, "preload.js")',
|
||||
"preload.js",
|
||||
"filesystem-expression",
|
||||
],
|
||||
[
|
||||
'path.join(__dirname, "/preload.js")',
|
||||
"preload.js",
|
||||
|
||||
+2
@@ -54,6 +54,8 @@ const graphIdentities = async (source: string) => {
|
||||
// source locations and every derived identity stay comparable.
|
||||
it.each([
|
||||
"fetch('https://api.example.com/v1/users');",
|
||||
"client['get']('/api/items');",
|
||||
"navigator['sendBeacon']('/api/events', payload);",
|
||||
"localStorage.setItem('token', 1);",
|
||||
"new Worker(new URL('./worker.js', import.meta.url));",
|
||||
"const dep = require('./dep.js'); dep();",
|
||||
|
||||
Reference in New Issue
Block a user