Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions src/domain/javascript/electronStaticAnalysisOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,141 @@ describe("Electron option captures and initialization", () => {
});
});

it("resolves module-level webPreferences used inside a function", () => {
const analysis = analyzeJavaScriptStaticSource(`
const { BrowserWindow } = require("electron");
const webPreferences = { nodeIntegration: true, contextIsolation: false };
function createWindow() {
return new BrowserWindow({ width: 800, webPreferences });
}
createWindow();
`);
const window = analysis.electron.browser_windows[0];
expect(window?.web_preferences_status).toBe("object-literal");
expect(window?.web_preferences).toEqual(
expect.arrayContaining([
{
name: "nodeIntegration",
value: expect.objectContaining({ status: "literal", value: true }),
},
{
name: "contextIsolation",
value: expect.objectContaining({ status: "literal", value: false }),
},
]),
);
});
it("keeps a module-level option unresolved when called before initialization", () => {
const analysis = analyzeJavaScriptStaticSource(`
const { BrowserWindow } = require("electron");
function createWindow() {
return new BrowserWindow({ webPreferences });
}
createWindow();
const webPreferences = { nodeIntegration: true };
`);
expect(analysis.electron.browser_windows[0]).toMatchObject({
web_preferences_status: "dynamic",
web_preferences: [],
});
});
it.each([
{ label: "a direct call", before: "createWindow();", after: "" },
{
label: "an aliased call",
before: "const early = createWindow; early();",
after: "",
},
{
label: "a forwarding function",
before: "start();",
after: "function start() { createWindow(); }",
},
])(
"does not initialize a hoisted window function reached by $label",
({ before, after }) => {
const source = `
const { BrowserWindow } = require("electron");
${before}
const webPreferences = { nodeIntegration: true };
function createWindow() {
return new BrowserWindow({ webPreferences });
}
${after}
`;
expect(() =>
runInNewContext(source, {
require: () => ({ BrowserWindow: class {} }),
}),
).toThrow(/before initialization/);
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "dynamic",
web_preferences: [],
});
},
);

it("resolves a hoisted window function called after module initialization", () => {
const source = `
const { BrowserWindow } = require("electron");
function createWindow() {
return new BrowserWindow({ webPreferences });
}
const webPreferences = { nodeIntegration: true };
createWindow();
`;
expect(
analyzeJavaScriptStaticSource(source).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "object-literal",
web_preferences: [
{ name: "nodeIntegration", value: { status: "literal", value: true } },
],
});
});

it.each([
"for (const item of [0])",
"for (let index = 0; index < 1; index++)",
"while (true)",
])(
"keeps function initialization coverage separate from %s ordering",
(loop) => {
const source = (early: boolean) => `
const { BrowserWindow } = require("electron");
${early ? "createWindow();" : ""}
const webPreferences = { nodeIntegration: true };
function createWindow() {
${loop} {
return new BrowserWindow({ webPreferences });
}
}
${early ? "" : "createWindow();"}
`;
const runtime = () => ({ require: () => ({ BrowserWindow: class {} }) });
expect(() => runInNewContext(source(false), runtime())).not.toThrow();
expect(
analyzeJavaScriptStaticSource(source(false)).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "object-literal",
web_preferences: [
{ name: "nodeIntegration", value: { status: "literal", value: true } },
],
});
expect(() => runInNewContext(source(true), runtime())).toThrow(
/before initialization/,
);
expect(
analyzeJavaScriptStaticSource(source(true)).electron.browser_windows[0],
).toMatchObject({
web_preferences_status: "dynamic",
web_preferences: [],
});
},
);

describe("Electron option evidence", () => {
it.each([
'({ webPreferences: { preload: "./preload.js", sandbox: true } } as const)',
Expand Down
143 changes: 143 additions & 0 deletions src/domain/javascript/javascriptSemanticMutationInitializers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as t from "@babel/types";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";

import {
delayedInstanceFieldValue,
Expand Down Expand Up @@ -54,6 +55,11 @@ interface InitializerPositionCache {
readonly mutation: WeakMap<t.Node, StatementPosition | null>;
readonly reference: WeakMap<t.Node, StatementPosition | null>;
readonly read: WeakMap<t.Node, BindingReadPosition>;
readonly enclosingFunction: WeakMap<t.Node, t.Function | null>;
readonly moduleFunctionReads: WeakMap<
readonly t.Statement[],
WeakMap<t.FunctionDeclaration, number | null>
>;
readonly evaluationChildren: WeakMap<
t.Node,
ReadonlyMap<t.Node, number> | null
Expand All @@ -78,6 +84,8 @@ const initializerPositionCache = (
mutation: new WeakMap(),
reference: new WeakMap(),
read: new WeakMap(),
enclosingFunction: new WeakMap(),
moduleFunctionReads: new WeakMap(),
evaluationChildren: new WeakMap(),
expressionAncestry: undefined,
};
Expand Down Expand Up @@ -709,6 +717,35 @@ export const semanticMutationInitializers = (
};
};

/** Function ownership is independent of uncertain ordering within a loop. */
const bindingEnclosingFunction = (
read: t.Node,
parents: WeakMap<t.Node, t.Node>,
): t.Function | undefined => {
const cache = initializerPositionCache(parents).enclosingFunction;
const path: t.Node[] = [];
let current = read;
let result: t.Function | null = null;
while (true) {
const cached = cache.get(current);
if (cached !== undefined) {
result = cached;
break;
}
path.push(current);
const parent = parents.get(current);
if (parent === undefined || delayedInstanceFieldValue(parent, current))
break;
if (t.isFunction(parent) && !semanticComputedMethodKey(parent, current)) {
if (current === parent.body) result = parent;
break;
}
current = parent;
}
for (const node of path) cache.set(node, result);
return result ?? undefined;
};

/** Reuse immutable ancestry facts across reads nested in the same expression. */
const bindingReadPosition = (
read: t.Node,
Expand Down Expand Up @@ -798,6 +835,70 @@ type SemanticBindingRead = {
readonly binding: JavaScriptSemanticBindingState;
};

/** A hoisted function needs a module-level use after initialization, not a later declaration. */
const moduleFunctionFirstRead = (
declaration: t.FunctionDeclaration,
body: readonly t.Statement[],
parents: WeakMap<t.Node, t.Node>,
): number | null => {
const cache = initializerPositionCache(parents).moduleFunctionReads;
let reads = cache.get(body);
if (reads === undefined) {
reads = new WeakMap();
cache.set(body, reads);
const functions = new Map<string, t.FunctionDeclaration | null>();
for (const statement of body) {
const exported =
t.isExportNamedDeclaration(statement) ||
t.isExportDefaultDeclaration(statement);
const node = exported ? statement.declaration : statement;
if (!t.isFunctionDeclaration(node)) continue;
reads.set(node, exported ? null : Infinity);
if (node.id == null) continue;
const name = node.id.name;
if (functions.has(name)) {
const previous = functions.get(name);
if (previous != null) reads.set(previous, null);
reads.set(node, null);
functions.set(name, null);
} else functions.set(name, node);
}
const indexedReads = reads;
const active = new Set<t.Function>();
for (let index = 0; index < body.length; index++) {
const statement = body[index];
if (statement === undefined) continue;
traverseJavaScriptAst(statement, {
enter(node, parent) {
if (t.isFunction(node)) active.add(node);
if (
!t.isIdentifier(node) ||
parent === null ||
!t.isReferenced(node, parent)
)
return;
const fn = functions.get(node.name);
if (fn == null || active.has(fn)) return;
const current = indexedReads.get(fn);
if (current === null || current === undefined) return;
// Indirect activations and live exports have no proven module timing.
indexedReads.set(
fn,
active.size > 0 || t.isExportSpecifier(parent)
? null
: Math.min(current, index),
);
},
exit(node) {
if (t.isFunction(node)) active.delete(node);
},
});
}
}
const first = reads.get(declaration);
return first !== undefined && Number.isFinite(first) ? first : null;
};

export const semanticBindingAtRead = (
binding: JavaScriptSemanticBindingState,
read: t.Node,
Expand Down Expand Up @@ -897,11 +998,53 @@ export const semanticBindingAtRead = (
binding,
hasDefiniteInitializer,
};
const moduleConstBinding =
binding.scopeId.startsWith("scope:program:") &&
binding.declarationKinds.size === 1 &&
binding.declarationKinds.has("const") &&
!binding.mutable;
const enclosingFunction = moduleConstBinding
? bindingEnclosingFunction(read, parents)
: undefined;
const functionStatementPosition = (): StatementPosition | null => {
if (enclosingFunction === undefined) return null;
if (t.isStatement(enclosingFunction))
return statementPosition(enclosingFunction, parents);
for (
let owner = parents.get(enclosingFunction);
owner !== undefined;
owner = parents.get(owner)
)
if (t.isStatement(owner)) return statementPosition(owner, parents);
return null;
};
const functionPosition = moduleConstBinding
? functionStatementPosition()
: null;
const firstFunctionRead =
moduleConstBinding &&
sourcePosition !== null &&
functionPosition !== null &&
sourcePosition.body === functionPosition.body &&
t.isFunctionDeclaration(enclosingFunction)
? moduleFunctionFirstRead(enclosingFunction, sourcePosition.body, parents)
: null;
const moduleConstInitializedBeforeActivation =
moduleConstBinding &&
sourcePosition !== null &&
functionPosition !== null &&
sourcePosition.body === functionPosition.body &&
(t.isFunctionDeclaration(enclosingFunction)
? firstFunctionRead !== null && sourcePosition.index < firstFunctionRead
: positionPrecedes(sourcePosition, functionPosition));
const uncertainOrder =
repeatingLoop ||
sourcePosition === null ||
position === null ||
sourcePosition.body !== position.body;
if (uncertainOrder && moduleConstInitializedBeforeActivation)
// Initialization coverage does not order a capture in another activation.
return { status: "unresolved", binding, hasDefiniteInitializer: true };
if (uncertainOrder)
return { status: "unresolved", binding, hasDefiniteInitializer };
const selected = selectInitializers(binding, position, read, parents);
Expand Down
Loading
Loading