diff --git a/src/domain/javascript/electronNativeLoaderPaths.test.ts b/src/domain/javascript/electronNativeLoaderPaths.test.ts index f586e4413d..352c640810 100644 --- a/src/domain/javascript/electronNativeLoaderPaths.test.ts +++ b/src/domain/javascript/electronNativeLoaderPaths.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; +import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; describe("native addon type-only TypeScript imports", () => { @@ -64,8 +65,23 @@ describe("native addon type-only re-exports", () => { }); describe("native addon loader provenance", () => { - it("distinguishes CommonJS and ESM re-exports", () => { + it("does not give ESM fixtures an ambient CommonJS loader", () => { const analysis = analyzeJavaScriptStaticSource(` + import imported from "./addon.node"; + const required = require("./other.node"); + module.exports = require("./exported.node"); + `); + expect(analysis.electron.native_addon_bindings).toEqual([ + expect.objectContaining({ + specifier: "./addon.node", + binding_kind: "import", + module_kind: "import", + }), + ]); + }); + + it("distinguishes CommonJS and ESM re-exports", () => { + const analysis = analyzeCommonJsStaticFixture(` const required = require("./addon.node#literal.node"); module.exports = require("./addon.node#literal.node"); import imported from "./addon.node#literal.node"; @@ -97,7 +113,7 @@ describe("native addon loader provenance", () => { it.each(["#", "?"])( "classifies require filenames literally for %s", (punctuation) => { - const analysis = analyzeJavaScriptStaticSource(` + const analysis = analyzeCommonJsStaticFixture(` const script = require("./addon.node${punctuation}helper.js"); import native from "./addon.node${punctuation}helper.js"; `); diff --git a/src/domain/javascript/electronStaticAnalysisExportedAppPath.test.ts b/src/domain/javascript/electronStaticAnalysisExportedAppPath.test.ts index dbb0806a98..d042dbbdaa 100644 --- a/src/domain/javascript/electronStaticAnalysisExportedAppPath.test.ts +++ b/src/domain/javascript/electronStaticAnalysisExportedAppPath.test.ts @@ -1,7 +1,7 @@ import { runInNewContext } from "node:vm"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; const cases = [ ["native setter", "const change=app.setAppPath;", true, [1]], @@ -47,14 +47,14 @@ it.each(cases)( runInNewContext("change.call(app,'./lib');", namespace); expect(changed).toBe(expected); } - const exported = analyzeJavaScriptStaticSource( + const exported = analyzeCommonJsStaticFixture( `${defining}export{change,app};`, ); expect(exported.parse_error_count).toBe(0); expect((exported.electron.app_path_changes ?? []).length > 0).toBe( possible, ); - const caller = analyzeJavaScriptStaticSource( + const caller = analyzeCommonJsStaticFixture( "import{change,app}from'./helper.mjs';change.call(app,'./lib');", ); expect(caller.parse_error_count).toBe(0); @@ -63,7 +63,7 @@ it.each(cases)( ); it("does not treat a type-only exported setter as a runtime escape", () => { - const analysis = analyzeJavaScriptStaticSource( + const analysis = analyzeCommonJsStaticFixture( "const{app}=require('electron');const change=app.setAppPath;export type{change};", ); expect(analysis.parse_error_count).toBe(0); @@ -78,7 +78,7 @@ it.each([ ] as const)( "selects the published runtime value in %s", (declaration, possible) => { - const analysis = analyzeJavaScriptStaticSource( + const analysis = analyzeCommonJsStaticFixture( `const{app}=require('electron');${declaration}`, ); expect(analysis.parse_error_count).toBe(0); @@ -105,7 +105,7 @@ it.each(cases)( runInNewContext("change.call(app,'./lib');", module.exports); expect(changed).toBe(expected); } - const analysis = analyzeJavaScriptStaticSource(defining); + const analysis = analyzeCommonJsStaticFixture(defining); expect(analysis.parse_error_count).toBe(0); expect((analysis.electron.app_path_changes ?? []).length > 0).toBe( possible, @@ -125,7 +125,7 @@ it.each([ ] as const)( "follows shared runtime export values and receivers in %s", (published, possible) => { - const analysis = analyzeJavaScriptStaticSource( + const analysis = analyzeCommonJsStaticFixture( `const {app}=require('electron');const change=app.setAppPath;${published}`, ); expect(analysis.parse_error_count).toBe(0); diff --git a/src/domain/javascript/electronStaticAnalysisOptions.test.ts b/src/domain/javascript/electronStaticAnalysisOptions.test.ts index 2670e35200..d7e4cd23aa 100644 --- a/src/domain/javascript/electronStaticAnalysisOptions.test.ts +++ b/src/domain/javascript/electronStaticAnalysisOptions.test.ts @@ -205,16 +205,17 @@ describe("Electron option evidence", () => { }, ); - it("does not read an audio or video preload attribute as a preload script", () => { + it("selects proven Electron preloads while ignoring media attributes and unrelated properties", () => { const analysis = analyzeJavaScriptStaticSource(` jsx("audio", { controls: true, preload: "metadata", src }); (0, React.createElement)(\`video\`, { preload: "auto" }); jsx("webview", { preload: "./guest.js" }); - const options = { preload: "./preload.js" }; + const options = { preload: "./unrelated.js" }; + const { BrowserWindow } = require("electron"); + new BrowserWindow({webPreferences: {preload: "./preload.js"}}); `); expect(analysis.role_paths.map(({ path }) => path)).toEqual([ - "./guest.js", "./preload.js", ]); }); diff --git a/src/domain/javascript/javascriptSemanticAnalysis.builtin_defaults.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.builtin_defaults.test.ts index 42c7cce6e4..6626bf03f7 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.builtin_defaults.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.builtin_defaults.test.ts @@ -1,28 +1,28 @@ import { expect, it } from "vitest"; -import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; +import { analyzeCommonJsSemanticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; it("shares required process writes with globals while keeping earlier observations", () => { - const ir = analyzeJavaScriptSemantics(`const proc=require('process'); + const ir = analyzeCommonJsSemanticFixture(`const proc=require('process'); process.cwd(); const before=proc.env.BEFORE; proc.env={}; const after=process.env.AFTER;`); expect(ir.configurationOperations.map(({ key }) => key)).toEqual(["BEFORE"]); }); it("does not propagate replacement-object writes from import-equals direct origins", () => { - const ir = analyzeJavaScriptSemantics(`import fs = require('fs'); + const ir = analyzeCommonJsSemanticFixture(`import fs = require('fs'); const other=require('fs'); fs={}; fs.readFileSync=custom; other.readFileSync('kept');`); expect(ir.configurationOperations.map(({ key }) => key)).toEqual(["kept"]); }); it("retains original import-equals aliases captured before reassignment", () => { - const ir = analyzeJavaScriptSemantics(`import fs = require('fs'); + const ir = analyzeCommonJsSemanticFixture(`import fs = require('fs'); const alias=fs; const other=require('fs'); fs={}; alias.readFileSync=custom; other.readFileSync('wrong');`); expect(ir.configurationOperations).toEqual([]); }); it("keeps computed namespace calls as unknown receiver escapes", () => { - const ir = analyzeJavaScriptSemantics( + const ir = analyzeCommonJsSemanticFixture( `const fs=require('fs'); fs.hook=custom; fs[key](); fs.readFileSync('wrong');`, ); expect(ir.configurationOperations).toEqual([]); @@ -31,7 +31,8 @@ it("keeps computed namespace calls as unknown receiver escapes", () => { it.each(["(0, fs.readFileSync)", "(assigned = fs.readFileSync)"])( "checks captured function validity through %s", (value) => { - const ir = analyzeJavaScriptSemantics(`const fs=require('fs'); let assigned; + const ir = + analyzeCommonJsSemanticFixture(`const fs=require('fs'); let assigned; const saved=${value}; fs.readFileSync=custom; const late=${value}; saved('kept'); late('wrong');`); expect(ir.configurationOperations.map(({ key }) => key)).toEqual(["kept"]); }, @@ -41,7 +42,7 @@ it.each(['process.env={MODE:"local"};', "process.argv=[];", "patch(process);"])( "shares unbound process object effects with required aliases: %s", (write) => { const ir = - analyzeJavaScriptSemantics(`${write} const proc=require('process'); + analyzeCommonJsSemanticFixture(`${write} const proc=require('process'); ${write.includes("argv") ? "const arg=proc.argv[2];" : "const mode=proc.env.MODE;"}`); expect(ir.configurationOperations).toEqual([]); }, @@ -51,14 +52,14 @@ it.each([ "function local(process) {process.env={};}", "process={}; process.env={};", ])("keeps unrelated process replacements separate: %s", (write) => { - const ir = analyzeJavaScriptSemantics( + const ir = analyzeCommonJsSemanticFixture( `${write} const proc=require('process'); const mode=proc.env.MODE;`, ); expect(ir.configurationOperations.map(({ key }) => key)).toEqual(["MODE"]); }); it("preserves exports captured before a later loader overwrite", () => { - const ir = analyzeJavaScriptSemantics(` + const ir = analyzeCommonJsSemanticFixture(` const fs=require('node:fs'); fs.readFileSync('before'); const read=fs.readFileSync; require('fs').readFileSync('direct-before'); @@ -82,9 +83,9 @@ it.each([ ])( "keeps repeated or already replaced loader captures uncertain: %s", (source) => { - expect(analyzeJavaScriptSemantics(source).configurationOperations).toEqual( - [], - ); + expect( + analyzeCommonJsSemanticFixture(source).configurationOperations, + ).toEqual([]); }, ); @@ -95,7 +96,7 @@ it.each([ ])( "shares require-mode import-equals effects in both directions: %s", (write, call) => { - const ir = analyzeJavaScriptSemantics(`import fs = require('node:fs'); + const ir = analyzeCommonJsSemanticFixture(`import fs = require('node:fs'); const other = require('fs'); other.readFileSync('before'); ${write} ${call}`); expect(ir.configurationOperations.map(({ key }) => key)).toEqual([ "before", @@ -110,7 +111,7 @@ it.each([ ])( "retains captured native function calls after argument escape: %s", (capture) => { - const ir = analyzeJavaScriptSemantics( + const ir = analyzeCommonJsSemanticFixture( `${capture} observe(read); read('kept');`, ); expect(ir.configurationOperations.map(({ key }) => key)).toEqual(["kept"]); @@ -118,20 +119,20 @@ it.each([ ); it("distinguishes escaping a method value from escaping its namespace", () => { - const ir = analyzeJavaScriptSemantics(`const fs=require('fs'); + const ir = analyzeCommonJsSemanticFixture(`const fs=require('fs'); observe(fs.readFileSync); fs.readFileSync('kept'); observe(fs); fs.readFileSync('wrong');`); expect(ir.configurationOperations.map(({ key }) => key)).toEqual(["kept"]); }); it("still invalidates escaped process configuration containers", () => { - const ir = analyzeJavaScriptSemantics(`const proc=require('process'); + const ir = analyzeCommonJsSemanticFixture(`const proc=require('process'); observe(proc.env); const mode=proc.env.MODE;`); expect(ir.configurationOperations).toEqual([]); }); it("retains direct require effects without poisoning later cached receivers", () => { - const ir = analyzeJavaScriptSemantics(` + const ir = analyzeCommonJsSemanticFixture(` require('node:fs').readFileSync('first'); const fs = require('fs'); fs.readFileSync('second'); require('node:fs').createReadStream('stream'); @@ -156,7 +157,7 @@ it.each([ "require('node:fs').promises.open = custom;", "const fsp = require('fs/promises'); fsp.open = custom;", ])("shares fs.promises object writes with fs/promises: %s", (write) => { - const ir = analyzeJavaScriptSemantics(` + const ir = analyzeCommonJsSemanticFixture(` const before = require('node:fs/promises'); before.open('before'); ${write} const after = require('node:fs/promises'); after.open('wrong'); @@ -169,7 +170,7 @@ it.each([ "({ loader: require } = other);", "for (require of loaders) {}", ])("rejects provenance from an overwritten ambient loader: %s", (write) => { - const ir = analyzeJavaScriptSemantics(`${write} + const ir = analyzeCommonJsSemanticFixture(`${write} const fs = require('node:fs'); fs.readFileSync('wrong'); require('node:fs').readFileSync('also-wrong'); `); @@ -180,7 +181,7 @@ it.each([ }); it("keeps local loader reassignment separate from the ambient loader", () => { - const ir = analyzeJavaScriptSemantics(` + const ir = analyzeCommonJsSemanticFixture(` function local(require) { require = replacement; } const fs = require('node:fs'); fs.readFileSync('kept'); `); @@ -188,7 +189,7 @@ it("keeps local loader reassignment separate from the ambient loader", () => { }); it("keeps wrapper require separate from global-object property writes", () => { - const ir = analyzeJavaScriptSemantics(` + const ir = analyzeCommonJsSemanticFixture(` globalThis.require = replacement; function polyfill(key) { globalThis[key] = replacement; } require('node:fs').readFileSync('kept'); @@ -199,7 +200,7 @@ it("keeps wrapper require separate from global-object property writes", () => { it.each(["import fs from 'node:fs';", "const fs = require('node:fs');"])( "retains reads and saved methods before a later write: %s", (declaration) => { - const ir = analyzeJavaScriptSemantics(`${declaration} + const ir = analyzeCommonJsSemanticFixture(`${declaration} fs.readFileSync('before'); const saved = fs.readFileSync; const { readFileSync: destructured } = fs; @@ -220,7 +221,7 @@ it.each([ "const { readFileSync: read } = require('fs');", "const read = require('fs').readFileSync;", ])("checks cached module writes at the projected capture: %s", (capture) => { - const ir = analyzeJavaScriptSemantics(` + const ir = analyzeCommonJsSemanticFixture(` const fs = require('node:fs'); const saved = require('fs').readFileSync; fs.readFileSync = custom; @@ -231,7 +232,7 @@ it.each([ }); it("retains captured methods across later escapes without freezing a namespace alias", () => { - const ir = analyzeJavaScriptSemantics(` + const ir = analyzeCommonJsSemanticFixture(` const fs = require('node:fs'); const saved = require('fs').readFileSync; const namespace = require('fs'); patch(fs); const late = require('fs').readFileSync; @@ -241,7 +242,7 @@ it("retains captured methods across later escapes without freezing a namespace a }); it("keeps writes in repeated callable and loop activations conservative", () => { - const ir = analyzeJavaScriptSemantics(` + const ir = analyzeCommonJsSemanticFixture(` const fs = require('node:fs'); function run() { fs.readFileSync('wrong'); fs.readFileSync = custom; } for (;;) { const read = require('fs').readFileSync; read('wrong'); fs.readFileSync = custom; } @@ -250,14 +251,14 @@ it("keeps writes in repeated callable and loop activations conservative", () => }); it("does not propagate a reassigned local object's writes into cached exports", () => { - const ir = analyzeJavaScriptSemantics( + const ir = analyzeCommonJsSemanticFixture( `let a = require('node:fs'); const b = require('node:fs'); a = {}; a.readFileSync = custom; b.readFileSync('kept');`, ); expect(ir.configurationOperations).toMatchObject([{ key: "kept" }]); }); it("shares repeated require mutations for every built-in effect family", () => { - const ir = analyzeJavaScriptSemantics(` + const ir = analyzeCommonJsSemanticFixture(` const fs = require('fs'); const otherFs = require('node:fs'); fs.createReadStream = custom; otherFs.createReadStream('wrong'); const cp = require('child_process'); const otherCp = require('node:child_process'); cp.spawn = custom; otherCp.spawn('wrong', []); const timers = require('timers'); const otherTimers = require('node:timers'); timers.setTimeout = custom; otherTimers.setTimeout(callback, 1); @@ -274,7 +275,7 @@ it("shares repeated require mutations for every built-in effect family", () => { it.each(["node:fs", "fs"])( "shares repeated built-in require identity across %s", (specifier) => { - const ir = analyzeJavaScriptSemantics( + const ir = analyzeCommonJsSemanticFixture( `const a = require('node:fs'); const b = require('${specifier}'); const alias = b; a.readFileSync = custom; b.createReadStream('kept'); alias.readFileSync('wrong');`, ); expect(ir.configurationOperations).toEqual([]); @@ -285,14 +286,14 @@ it.each(["node:fs", "fs"])( ); it("shares unknown escapes across repeated requires without losing earlier effects", () => { - const ir = analyzeJavaScriptSemantics( + const ir = analyzeCommonJsSemanticFixture( `const a = require('node:fs'); const b = require('fs'); b.readFileSync('before'); patch(a); b.readFileSync('after');`, ); expect(ir.configurationOperations.map(({ key }) => key)).toEqual(["before"]); }); it("preserves exact default ESM import mutation paths", () => { - const ir = analyzeJavaScriptSemantics( + const ir = analyzeCommonJsSemanticFixture( `import fs from 'node:fs'; fs.writeFileSync = custom; fs.readFileSync('kept'); fs.writeFileSync('replaced');`, ); expect(ir.configurationOperations).toMatchObject([ @@ -301,7 +302,7 @@ it("preserves exact default ESM import mutation paths", () => { }); it("retains built-in effects before later top-level escapes", () => { - const ir = analyzeJavaScriptSemantics(` + const ir = analyzeCommonJsSemanticFixture(` const fs = require('node:fs'); const cp = require('node:child_process'); const timers = require('node:timers'); const http = require('node:http'); fs.readFileSync('before'); fs.createReadStream('before'); cp.spawn('tool', []); timers.setTimeout(callback, 1); http.get('http://example.test'); patch(fs); patch(cp); patch(timers); patch(http); @@ -319,7 +320,7 @@ it("retains built-in effects before later top-level escapes", () => { it.each(["env", "argv"])( "does not treat a replaced process %s as configuration", (family) => { - const ir = analyzeJavaScriptSemantics( + const ir = analyzeCommonJsSemanticFixture( `const proc = require('node:process'); proc.${family} = replacement; const value = proc.${family}[${family === "env" ? "'MODE'" : "2"}];`, ); expect(ir.configurationOperations).toEqual([]); @@ -327,14 +328,14 @@ it.each(["env", "argv"])( ); it("does not infer temporal safety across repeated callable or loop activations", () => { - const ir = analyzeJavaScriptSemantics( + const ir = analyzeCommonJsSemanticFixture( `const fs = require('node:fs'); function run() { fs.readFileSync('callable'); patch(fs); } for (;;) { fs.readFileSync('loop'); patch(fs); }`, ); expect(ir.configurationOperations).toEqual([]); }); it("does not recover a replaced method through a subsequent alias", () => { - const ir = analyzeJavaScriptSemantics( + const ir = analyzeCommonJsSemanticFixture( `const fs = require('node:fs'); fs.readFileSync = custom; const read = fs.readFileSync; read('config.json');`, ); expect(ir.configurationOperations).toEqual([]); @@ -345,7 +346,7 @@ it.each([ "patch(fs); patch(cp); patch(timers); patch(http);", "const fsAlias = fs; fsAlias.readFileSync = custom; fsAlias.createReadStream = custom; const cpAlias = cp; cpAlias.spawn = custom; timers[key] = custom; delete http.get;", ])("excludes replaced or escaped CommonJS methods: %s", (mutation) => { - const ir = analyzeJavaScriptSemantics(` + const ir = analyzeCommonJsSemanticFixture(` const fs = require('node:fs'); const cp = require('node:child_process'); const timers = require('node:timers'); const http = require('node:http'); ${mutation} @@ -360,7 +361,7 @@ it.each([ }); it("preserves unaffected CommonJS methods after sibling property mutation", () => { - const ir = analyzeJavaScriptSemantics( + const ir = analyzeCommonJsSemanticFixture( `const fs = require('node:fs'); fs.writeFileSync = custom; fs.readFileSync('config.json');`, ); expect(ir.configurationOperations).toMatchObject([ @@ -378,7 +379,7 @@ it.each([ const { setTimeout: schedule } = require("timers"); const { get } = require("http");`, ])("recovers builtin effects from CommonJS provenance: %s", (declarations) => { - const ir = analyzeJavaScriptSemantics(`${declarations} + const ir = analyzeCommonJsSemanticFixture(`${declarations} const text = read("config.json", "utf8"); const stream = fs.createReadStream("config.json"); stream.destroy(); const child = launch("tool", []); child.kill("SIGTERM"); @@ -412,7 +413,7 @@ it.each([ }); it("does not promote ambiguous or non-namespace CommonJS origins to builtin effects", () => { - const ir = analyzeJavaScriptSemantics(` + const ir = analyzeCommonJsSemanticFixture(` const fake = require("./fs.js"); fake.readFileSync("external"); const absentDefault = require("node:fs").default; absentDefault.readFileSync("default"); const { constants } = require("node:fs"); constants.createReadStream("nested"); @@ -425,7 +426,7 @@ it("does not promote ambiguous or non-namespace CommonJS origins to builtin effe }); it("does not assign built-in APIs to unrelated or shadowed bindings", () => { - const ir = analyzeJavaScriptSemantics(` + const ir = analyzeCommonJsSemanticFixture(` import fs from "./fs.js"; import { constants as constantsFs } from "node:fs"; fs.createReadStream("config.json"); @@ -443,9 +444,9 @@ it.each([ `import proc from 'node:process'; proc.env = {}; const value = process.env.MODE;`, `import fs from 'node:fs'; fs.readFileSync = custom; const other = require('fs'); other.readFileSync('wrong');`, ])("keeps delayed or replaced built-in identities unknown: %s", (source) => { - expect(analyzeJavaScriptSemantics(source).configurationOperations).toEqual( - [], - ); + expect( + analyzeCommonJsSemanticFixture(source).configurationOperations, + ).toEqual([]); }); it.each([ @@ -454,7 +455,7 @@ it.each([ `import fs from 'node:fs'; fs.writeFileSync = custom; const other = require('fs'); other.readFileSync('kept');`, ])("keeps earlier and independent built-in observations: %s", (source) => { expect( - analyzeJavaScriptSemantics(source).configurationOperations.map( + analyzeCommonJsSemanticFixture(source).configurationOperations.map( ({ key }) => key, ), ).toEqual(["kept"]); @@ -469,7 +470,7 @@ it("shares equivalent alias subproblems and repeated intact receiver facts", () { length: 128 }, (_, index) => `a29.readFileSync('file-${index}');`, ); - const ir = analyzeJavaScriptSemantics( + const ir = analyzeCommonJsSemanticFixture( ["const fs = require('fs');", ...aliases, ...calls].join("\n"), ); expect(ir.configurationOperations).toHaveLength(128); @@ -481,7 +482,7 @@ it.each([ `const tls = require('tls'); const socket = tls.connect(443, 'example.test'); socket.end();`, ])("retains TLS acquisitions and their release: %s", (source) => { expect( - analyzeJavaScriptSemantics(source).resourceOperations.map( + analyzeCommonJsSemanticFixture(source).resourceOperations.map( ({ kind, method }) => ({ kind, method }), ), ).toEqual([ @@ -497,9 +498,9 @@ it.each([ `globalThis.process.env = { MODE: 'local' }; const proc = require('process'); const value = proc.env.MODE;`, `const proc = require('process'); proc.env.MODE = 'local'; const value = proc.env.MODE;`, ])("shares writes through the original process object: %s", (source) => { - expect(analyzeJavaScriptSemantics(source).configurationOperations).toEqual( - [], - ); + expect( + analyzeCommonJsSemanticFixture(source).configurationOperations, + ).toEqual([]); }); it.each([ @@ -519,7 +520,7 @@ it.each([ "retains reads before a later sequence operand: %s", (source, expectedKey) => { expect( - analyzeJavaScriptSemantics(source).configurationOperations.map( + analyzeCommonJsSemanticFixture(source).configurationOperations.map( ({ key }) => key, ), ).toEqual([expectedKey]); @@ -528,21 +529,21 @@ it.each([ it("does not use source offsets for destructuring assignment evaluation order", () => { const ir = - analyzeJavaScriptSemantics(`const fs = require('fs'); const target = {}; + analyzeCommonJsSemanticFixture(`const fs = require('fs'); const target = {}; [target[fs.readFileSync('wrong')]] = (fs.readFileSync = custom, []);`); expect(ir.configurationOperations).toEqual([]); }); it("keeps process reads after an alias is reassigned away from the global", () => { const ir = - analyzeJavaScriptSemantics(`let host = globalThis; host = {process: {env: {}}}; + analyzeCommonJsSemanticFixture(`let host = globalThis; host = {process: {env: {}}}; host.process.env = {}; const proc = require('process'); const value = proc.env.MODE;`); expect(ir.configurationOperations.map(({ key }) => key)).toEqual(["MODE"]); }); it("rejects CommonJS require after the wrapper module replaces its delegate", () => { const ir = - analyzeJavaScriptSemantics(`module.require = () => ({ readFileSync() {} }); + analyzeCommonJsSemanticFixture(`module.require = () => ({ readFileSync() {} }); const fs = require('node:fs'); fs.readFileSync('unknown');`); expect(ir.configurationOperations).toEqual([]); }); @@ -552,7 +553,7 @@ it.each([ `const owner = module; owner.require = () => ({}); const fs = require('fs'); fs.readFileSync('wrong');`, `Object.defineProperty(module, 'require', {value: () => ({})}); const fs = require('fs'); fs.readFileSync('wrong');`, ])("tracks only actual wrapper require replacements: %s", (source) => { - const ir = analyzeJavaScriptSemantics(source); + const ir = analyzeCommonJsSemanticFixture(source); expect(ir.configurationOperations.map(({ key }) => key)).toEqual( source.includes("extra") ? ["kept"] : [], ); @@ -560,7 +561,7 @@ it.each([ it("preserves a saved promises object after deleting its parent export", () => { const ir = - analyzeJavaScriptSemantics(`const fs = require('fs'); const fsp = fs.promises; + analyzeCommonJsSemanticFixture(`const fs = require('fs'); const fsp = fs.promises; delete fs.promises; const handle = fsp.open('kept'); handle.close();`); expect( ir.resourceOperations.map(({ kind, method }) => ({ kind, method })), @@ -571,7 +572,7 @@ it("preserves a saved promises object after deleting its parent export", () => { }); it("does not infer network acquisition methods from another built-in", () => { - const ir = analyzeJavaScriptSemantics( + const ir = analyzeCommonJsSemanticFixture( `const { open } = require('node:net'); open();`, ); expect(ir.resourceOperations).toEqual([]); @@ -579,7 +580,7 @@ it("does not infer network acquisition methods from another built-in", () => { it("recognizes nested ESM namespace receivers as original built-ins", () => { const ir = - analyzeJavaScriptSemantics(`import * as ns from 'node:fs'; import fs from 'node:fs'; + analyzeCommonJsSemanticFixture(`import * as ns from 'node:fs'; import fs from 'node:fs'; ns.default.readFileSync('first'); fs.readFileSync('second');`); expect(ir.configurationOperations.map(({ key }) => key)).toEqual([ "first", @@ -589,7 +590,7 @@ it("recognizes nested ESM namespace receivers as original built-ins", () => { it("keeps intact fs/promises identity after deleting the fs.promises slot", () => { const ir = - analyzeJavaScriptSemantics(`const fs=require('fs'); delete fs.promises; + analyzeCommonJsSemanticFixture(`const fs=require('fs'); delete fs.promises; const fsp=require('fs/promises'); const handle=fsp.open('kept'); handle.close();`); expect( ir.resourceOperations.map(({ kind, method }) => ({ kind, method })), @@ -604,11 +605,11 @@ it.each([ `const fs = require('fs'); patch(fs.promises); require('fs/promises').open('wrong');`, `const fs = require('fs'); patch(fs.promises); require('node:fs/promises').open('wrong');`, ])("keeps replaced or escaped promises receivers unknown: %s", (source) => { - expect(analyzeJavaScriptSemantics(source).resourceOperations).toEqual([]); + expect(analyzeCommonJsSemanticFixture(source).resourceOperations).toEqual([]); }); it("follows final sequence expressions for direct built-in method calls", () => { - const ir = analyzeJavaScriptSemantics(`const fs = require('fs'); + const ir = analyzeCommonJsSemanticFixture(`const fs = require('fs'); (0, fs.readFileSync)('kept'); fs.readFileSync = custom; (0, fs.readFileSync)('wrong');`); expect(ir.configurationOperations.map(({ key }) => key)).toEqual(["kept"]); @@ -620,35 +621,35 @@ it.each([ ])( "shares named process containers with their namespace aliases: %s", (source) => { - expect(analyzeJavaScriptSemantics(source).configurationOperations).toEqual( - [], - ); + expect( + analyzeCommonJsSemanticFixture(source).configurationOperations, + ).toEqual([]); }, ); it("retains uncertainty after a computed write to module's require delegate", () => { const ir = - analyzeJavaScriptSemantics(`const key = 'require'; module[key] = replacement; + analyzeCommonJsSemanticFixture(`const key = 'require'; module[key] = replacement; const fs = require('fs'); fs.readFileSync('wrong');`); expect(ir.configurationOperations).toEqual([]); }); it("retains a module namespace captured before its binding is reassigned", () => { const ir = - analyzeJavaScriptSemantics(`let fs = require('fs'); const saved = fs; + analyzeCommonJsSemanticFixture(`let fs = require('fs'); const saved = fs; fs = {}; saved.readFileSync('kept');`); expect(ir.configurationOperations.map(({ key }) => key)).toEqual(["kept"]); }); it("does not let own fs probe calls hide later builtin reads", () => { - const ir = analyzeJavaScriptSemantics( + const ir = analyzeCommonJsSemanticFixture( `const fs=require('fs'); fs.existsSync('probe'); fs.readFileSync('kept');`, ); expect(ir.configurationOperations.map(({ key }) => key)).toEqual(["kept"]); }); it("resolves nested ESM default namespace methods", () => { - const ir = analyzeJavaScriptSemantics( + const ir = analyzeCommonJsSemanticFixture( `import * as ns from 'node:fs'; ns.default.readFileSync('kept');`, ); expect(ir.configurationOperations.map(({ key }) => key)).toEqual(["kept"]); @@ -656,21 +657,21 @@ it("resolves nested ESM default namespace methods", () => { it("tracks process objects passed through qualified global references", () => { const ir = - analyzeJavaScriptSemantics(`patch(globalThis.process); const proc = require('process'); + analyzeCommonJsSemanticFixture(`patch(globalThis.process); const proc = require('process'); const mode = proc.env.MODE;`); expect(ir.configurationOperations).toEqual([]); }); it("checks a saved child object's method when that function is selected", () => { const ir = - analyzeJavaScriptSemantics(`const fs = require('fs'); const fsp = fs.promises; + analyzeCommonJsSemanticFixture(`const fs = require('fs'); const fsp = fs.promises; fs.promises.open = custom; const open = fsp.open; open('wrong');`); expect(ir.resourceOperations).toEqual([]); }); it("retains an earlier captured function after its object method is replaced", () => { const ir = - analyzeJavaScriptSemantics(`const fs = require('fs'); const fsp = fs.promises; + analyzeCommonJsSemanticFixture(`const fs = require('fs'); const fsp = fs.promises; const open = fsp.open; fs.promises.open = custom; open('kept');`); expect(ir.resourceOperations).toMatchObject([ { kind: "acquire", method: "open" }, diff --git a/src/domain/javascript/javascriptStaticPathResolution.test.ts b/src/domain/javascript/javascriptStaticPathResolution.test.ts index ed2a287bd3..948fd1f0f0 100644 --- a/src/domain/javascript/javascriptStaticPathResolution.test.ts +++ b/src/domain/javascript/javascriptStaticPathResolution.test.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url"; import { parseExpression } from "@babel/parser"; import { describe, expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; import { parserPluginsForPath } from "./javascriptSourceParser.js"; import { calleeName, @@ -358,7 +358,7 @@ it("decodes a source-relative file URL once before composing a filesystem path", it("retains Worker URL references through their separate URL-aware producer", () => { const specifier = "./worker%252Fentry.js?version=1#worker"; - const analysis = analyzeJavaScriptStaticSource(` + const analysis = analyzeCommonJsStaticFixture(` import { Worker } from "node:worker_threads"; new Worker(new URL(${JSON.stringify(specifier)}, import.meta.url)); `); @@ -404,7 +404,7 @@ const analyzePathConsumers = (expression: string) => { windowPrefix, windowExpression, utilityExpression, - analysis: analyzeJavaScriptStaticSource( + analysis: analyzeCommonJsStaticFixture( `${genericPrefix}${property} }; const { BrowserWindow, utilityProcess } = require("electron");\n${windowExpression};\n${utilityExpression};`, ), }; diff --git a/src/domain/javascript/javascriptStaticPathResolveOwners.test.ts b/src/domain/javascript/javascriptStaticPathResolveOwners.test.ts index 324ba9033d..be9ba7e9e7 100644 --- a/src/domain/javascript/javascriptStaticPathResolveOwners.test.ts +++ b/src/domain/javascript/javascriptStaticPathResolveOwners.test.ts @@ -2,14 +2,14 @@ import { posix } from "node:path"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; it.each(["/other", "relative"])( "keeps a shadowed source-directory parameter dynamic: %s", (directory) => { const actual = posix.resolve("/runtime/base", directory, "decoy.cjs"); expect(actual).not.toBe("/source/app/decoy.cjs"); - const analysis = analyzeJavaScriptStaticSource(` + const analysis = analyzeCommonJsStaticFixture(` const path = require('node:path'); const {BrowserWindow, utilityProcess} = require('electron'); function build(__dirname) { @@ -75,7 +75,7 @@ it.each(localResolvers)( false, ), ).toBe("/actual/runtime.cjs"); - const analysis = analyzeJavaScriptStaticSource(` + const analysis = analyzeCommonJsStaticFixture(` import {BrowserWindow, utilityProcess} from 'electron'; ${setup} new BrowserWindow({webPreferences:{preload:${expression}}}); @@ -126,7 +126,7 @@ it("does not treat a constructed Node resolver as a path call", () => { expect(() => Reflect.construct(posix.resolve, ["/app", "decoy.cjs"])).toThrow( TypeError, ); - const analysis = analyzeJavaScriptStaticSource(` + const analysis = analyzeCommonJsStaticFixture(` const helper = require('node:path'); const {BrowserWindow} = require('electron'); new BrowserWindow({webPreferences:{preload:new helper.resolve(runtimePath,__dirname,'decoy.cjs')}}); @@ -144,7 +144,7 @@ it.each(nodeResolvers)( posix.resolve("/actual/runtime.cjs", "/fixture/app", "decoy.cjs"), ).toBe("/fixture/app/decoy.cjs"); const expression = `${callee}(runtimePath, __dirname, "decoy.cjs")`; - const analysis = analyzeJavaScriptStaticSource(` + const analysis = analyzeCommonJsStaticFixture(` const {BrowserWindow, utilityProcess} = require('electron'); ${setup} new BrowserWindow({webPreferences:{preload:${expression}}}); diff --git a/src/domain/javascript/javascriptStaticPathSourceAnchors.test.ts b/src/domain/javascript/javascriptStaticPathSourceAnchors.test.ts index 6566050d82..c1cc55fa43 100644 --- a/src/domain/javascript/javascriptStaticPathSourceAnchors.test.ts +++ b/src/domain/javascript/javascriptStaticPathSourceAnchors.test.ts @@ -5,7 +5,7 @@ import { runInThisContext } from "node:vm"; import { expect, it } from "vitest"; -import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js"; it.each([false, true])( "preserves only untouched authored CommonJS dirname vars: assigned=%s", @@ -25,7 +25,7 @@ it.each([false, true])( expect(fixture.exports).toBe( assigned ? "relative/preload.cjs" : "/source/app/preload.cjs", ); - const analysis = analyzeJavaScriptStaticSource( + const analysis = analyzeCommonJsStaticFixture( `${setup}const{BrowserWindow}=require('electron');new BrowserWindow({webPreferences:{preload:path.join(__dirname,'preload.cjs')}});`, ); expect(analysis.electron.browser_windows).toMatchObject([ @@ -43,7 +43,7 @@ it("keeps local dirname methods from proving a source anchor", () => { "decoy.cjs", ), ).toBe("/other/decoy.cjs"); - const analysis = analyzeJavaScriptStaticSource( + const analysis = analyzeCommonJsStaticFixture( `import path from 'node:path';const helper={dirname:()=>'/other'};const{BrowserWindow}=require('electron');new BrowserWindow({webPreferences:{preload:path.resolve(runtimeBase,helper.dirname(import.meta.url),'decoy.cjs')}});`, ); expect(analysis.electron.browser_windows).toMatchObject([ @@ -57,7 +57,7 @@ it("retains a proven Node dirname of the decoded module URL", () => { fileURLToPath("file:///source/app/main.mjs", { windows: false }), ), ).toBe("/source/app"); - const analysis = analyzeJavaScriptStaticSource( + const analysis = analyzeCommonJsStaticFixture( `import path from 'node:path';import{fileURLToPath}from'node:url';const{BrowserWindow}=require('electron');new BrowserWindow({webPreferences:{preload:path.resolve(runtimeBase,path.dirname(fileURLToPath(import.meta.url)),'preload.cjs')}});`, ); expect(analysis.electron.browser_windows).toMatchObject([ @@ -75,7 +75,7 @@ it.each([false, true])( expect(local ? helper.dirname(decoded) : posix.dirname(decoded)).toBe( local ? "/other" : "/source/app", ); - const analysis = analyzeJavaScriptStaticSource(` + const analysis = analyzeCommonJsStaticFixture(` import path from 'node:path';import*as url from 'node:url'; const helper={dirname:()=>'/other'};const{BrowserWindow}=require('electron'); new BrowserWindow({webPreferences:{preload:path.resolve(runtimeBase,${local ? "helper" : "path"}.dirname(url.fileURLToPath(import.meta.url)),'preload.cjs')}}); @@ -88,7 +88,7 @@ it.each([false, true])( it("does not use a URL string as a filesystem source-file anchor", () => { expect(posix.dirname("file:///source/app/main.mjs")).not.toBe("/source/app"); - const analysis = analyzeJavaScriptStaticSource( + const analysis = analyzeCommonJsStaticFixture( `import path from 'node:path';const{BrowserWindow}=require('electron');new BrowserWindow({webPreferences:{preload:path.resolve(runtimeBase,path.dirname(import.meta.url),'decoy.cjs')}});`, ); expect(analysis.electron.browser_windows).toMatchObject([ @@ -99,7 +99,7 @@ it("does not use a URL string as a filesystem source-file anchor", () => { it.each([false, true])( "proves the CommonJS filename dirname owner: local=%s", (local) => { - const analysis = analyzeJavaScriptStaticSource( + const analysis = analyzeCommonJsStaticFixture( `const path=require('node:path');const helper={dirname:()=>'.'};const{BrowserWindow}=require('electron');new BrowserWindow({webPreferences:{preload:path.resolve(runtimeBase,${local ? "helper" : "path"}.dirname(__filename),'decoy.cjs')}});`, ); expect(analysis.electron.browser_windows).toMatchObject([ @@ -111,7 +111,7 @@ it.each([false, true])( it.each([false, true])( "preserves the utility-process dirname parameter until assigned: assigned=%s", (assigned) => { - const analysis = analyzeJavaScriptStaticSource( + const analysis = analyzeCommonJsStaticFixture( `var __dirname;${assigned ? "__dirname='relative';" : ""}const path=require('node:path');const{utilityProcess}=require('electron');utilityProcess.fork(path.join(__dirname,'worker.cjs'));`, ); expect(analysis.electron.utility_processes).toMatchObject([ diff --git a/tests/fixtures/javascriptCommonJsAnalysis.ts b/tests/fixtures/javascriptCommonJsAnalysis.ts new file mode 100644 index 0000000000..b300b406cb --- /dev/null +++ b/tests/fixtures/javascriptCommonJsAnalysis.ts @@ -0,0 +1,17 @@ +import { analyzeParsedJavaScriptSemantics } from "../../src/domain/javascript/javascriptSemanticAnalysis.js"; +import { analyzeParsedJavaScriptStaticSource } from "../../src/domain/javascript/javascriptStaticAnalysis.js"; +import { parseJavaScriptSource } from "../../src/domain/javascript/javascriptSourceParser.js"; + +// These fixtures model CommonJS TypeScript assets. The filename establishes +// wrapper authority while retaining type syntax and inert module declarations. +const parseCommonJsFixture = (source: string) => { + const parsed = parseJavaScriptSource(source, "fixture.cts"); + if (parsed === null) throw new Error("CommonJS fixture could not be parsed"); + return parsed; +}; + +export const analyzeCommonJsStaticFixture = (source: string) => + analyzeParsedJavaScriptStaticSource(source, parseCommonJsFixture(source)); + +export const analyzeCommonJsSemanticFixture = (source: string) => + analyzeParsedJavaScriptSemantics(parseCommonJsFixture(source));