import { runInNewContext } from "node:vm"; import { expect, it } from "vitest"; import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; const cases = [ [ "native source spread", "Object.assign(exports,...[{change:app.setAppPath,app}]);", true, ], [ "native target spread", "Object.assign(...[exports,{change:app.setAppPath,app}]);", true, ], [ "native call", "Object.assign.call(null,exports,{change:app.setAppPath,app});", true, ], [ "native apply", "Object.assign.apply(null,[exports,{change:app.setAppPath,app}]);", true, ], [ "captured intrinsic before replacement", "const publish=Object.assign;Object.assign=()=>{};publish(exports,{change:app.setAppPath,app});", true, ], [ "replaced intrinsic", "Object.assign=()=>{};Object.assign(exports,{change:app.setAppPath,app});", false, ], [ "replaced call", "Object.assign.call=()=>{};Object.assign.call(null,exports,{change:app.setAppPath,app});", false, ], [ "replaced apply", "Object.assign.apply=()=>{};Object.assign.apply(null,[exports,{change:app.setAppPath,app}]);", false, ], [ "replaced Function call", "Function.prototype.call=()=>{};Object.assign.call(null,exports,{change:app.setAppPath,app});", false, ], [ "replaced Function apply", "Function.prototype.apply=()=>{};Object.assign.apply(null,[exports,{change:app.setAppPath,app}]);", false, ], [ "native exports", "Object.assign(exports,{change:app.setAppPath,app});", true, ], [ "native module exports", "Object.assign(module.exports,{change:app.setAppPath,app});", true, ], [ "detached before", "module.exports={};Object.assign(exports,{change:app.setAppPath,app});", false, ], [ "detached after", "Object.assign(exports,{change:app.setAppPath,app});module.exports={};", false, ], [ "reconnected", "exports=module.exports={};Object.assign(exports,{change:app.setAppPath,app});", true, ], [ "local exports", "const exports={};Object.assign(exports,{change:app.setAppPath,app});", false, ], [ "local Object", "const Object={assign(){}};Object.assign(exports,{change:app.setAppPath,app});", false, ], [ "local setter", "app.setAppPath=()=>{};Object.assign(exports,{change:app.setAppPath,app});", false, ], ] as const; it.each(cases)( "retains an Object.assign-published setter for %s", (_name, body, possible) => { const defining = `const{app}=require('electron');${body}`; let changed = 0; const module = { exports: {} }; runInNewContext(defining, { module, exports: module.exports, require: () => ({ app: { setAppPath: () => changed++ } }), }); expect(changed).toBe(0); runInNewContext( "if(typeof change==='function')change.call(app,'./lib');", module.exports, ); expect(changed).toBe(possible ? 1 : 0); const analysis = analyzeJavaScriptStaticSource(defining); expect(analysis.parse_error_count).toBe(0); expect((analysis.electron.app_path_changes ?? []).length > 0).toBe( possible, ); }, ); it("retains native source uncertainty when an array iterator is overwritten", () => { const defining = "const{app}=require('electron');const sources=[{change:app.setAppPath,app}];sources[Symbol.iterator]=function*(){yield{change(){},app}};Object.assign(exports,...sources);"; let changed = 0; const module = { exports: {} }; runInNewContext(defining, { module, exports: module.exports, require: () => ({ app: { setAppPath: () => changed++ } }), }); runInNewContext("change.call(app,'./lib');", module.exports); expect(changed).toBe(0); // The static owner does not execute this custom iterator; it must retain the // native origin still present in the source rather than silently drop it. const analysis = analyzeJavaScriptStaticSource(defining); expect((analysis.electron.app_path_changes ?? []).length).toBeGreaterThan(0); });