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:
ShuaixinHuang
2026-10-15 14:59:06 +08:00
committed by GitHub
parent 3e88e21705
commit ce89706145
7 changed files with 323 additions and 8 deletions
@@ -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",
@@ -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();",