mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 22:36:08 +00:00
fix(javascript): count one IPC observation per site-resolved channel (#5810)
* fix(javascript): count one IPC observation per site-resolved channel A send with a local constant channel was recorded twice: once unresolved by the syntax pass and once resolved by wrapper substitution, because duplicates were keyed on the channel. A channel the call site determines now resolves that observation in place. A channel supplied through a wrapper argument stays a candidate beside the unresolved observation. Invocation identity now follows the call that supplies the wrapper's literal arguments, so one wrapper call reached through several entries is recorded once. Fixes #5710 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(javascript): keep wrapper invocations distinct while removing repeats Taking the nearest literal call as invocation identity collapsed separate outer calls that pass different handlers through one wrapper call. Entry identity is restored, and a finding is a repeat only when an identical finding was reached through the same trailing local calls. A registration replaces its unresolved source observation only when the site also determines its handler, so a parameter handler keeps the generic source registration beside per-invocation candidates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(javascript): compare site-determined IPC handlers by value In-place resolution compared handler representations, so a constant channel with a named local handler (recorded as an identifier by the syntax pass and as its function by substitution) stayed counted twice. The handler is now site-determined when it evaluates to the same function in the site's lexical environment. A replaced observation also removes the syntax pass's unknown-channel contribution, so a fully resolved file is no longer reported as partial. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(javascript): keep closures over invocation values as distinct IPC handlers A handler node created in different wrapper activations may capture different values, so the same function node alone does not prove the call site determines the registration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(javascript): guard optional call paths in closure handler test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(javascript): decide IPC handler identity from activation captures A closure is one site-determined handler only when it reads no binding of a function this trace activated, such as a wrapper parameter or factory local. Unused bindings in the activation no longer split a known listener, and closures over outer-call values stay distinct when a shorter entry traced the same wrapper call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(javascript): treat implicit captures and bundle modules precisely in IPC handler identity An arrow handler reading its enclosing function's arguments or this captures that activation. A bundle module factory runs once per module load, so its bindings are shared by every trace rather than per-invocation captures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(javascript): attribute an arrow handler's inherited context to its owner An arrow inherits this and arguments from the nearest non-arrow function, so an activated arrow inside that owner does not make the handler depend on the traced call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(javascript): skip suffix scans for per-invocation IPC handlers Check activation capture before scanning recorded call chains, keep the chains of capture-dependent handlers out of the index, append chains in place, and treat an arrow's inherited super and new.target as captures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(javascript): supersede nested-entry IPC records and share module-init factories A shorter entry traced alone no longer swallows the outer calls that invoke the same wrapper sequence: the first fuller path takes over its record and separate outer calls stay distinct invocations. A factory activation called at module scope runs once per load, so its closure is shared unless the trace itself makes that call. The resolver now owns bundle module-factory recognition instead of an environment marker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
35d6d5bb0a
commit
705058f3e2
5 files changed
+735
-13
No files matched your search
@@ -384,6 +384,324 @@ it("keeps a directly observed renderer operation singular when tracing its expos
|
||||
expect.objectContaining({ entry: "desktop.ping", channel: "ping" }),
|
||||
);
|
||||
});
|
||||
it("resolves a site-determined constant channel in place of its dynamic observation", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const {contextBridge,ipcRenderer}=require("electron");
|
||||
const PING = "ping";
|
||||
contextBridge.exposeInMainWorld("desktop",{ping:()=>ipcRenderer.send(PING)});
|
||||
`);
|
||||
expect(result.electron.ipc).toEqual([
|
||||
expect.objectContaining({
|
||||
side: "renderer",
|
||||
operation: "send",
|
||||
channel: "ping",
|
||||
resolution_basis: "wrapper-substitution",
|
||||
}),
|
||||
]);
|
||||
expect(result.electron.call_paths).toContainEqual(
|
||||
expect.objectContaining({ entry: "desktop.ping", channel: "ping" }),
|
||||
);
|
||||
// The syntax pass's unknown channel is resolved, not left as partial.
|
||||
expect(result.parse_status).toBe("complete");
|
||||
});
|
||||
it("resolves a site-determined registration whose handler is a named local", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const {contextBridge,ipcRenderer}=require("electron");
|
||||
const CH = "ch";
|
||||
const handler = () => {};
|
||||
contextBridge.exposeInMainWorld("desktop",{on:()=>ipcRenderer.on(CH, handler)});
|
||||
`);
|
||||
expect(result.electron.ipc).toEqual([
|
||||
expect.objectContaining({
|
||||
side: "renderer",
|
||||
mode: "listen",
|
||||
channel: "ch",
|
||||
handler_kind: "inline-function",
|
||||
}),
|
||||
]);
|
||||
expect(result.parse_status).toBe("complete");
|
||||
});
|
||||
it("keeps an argument-supplied channel unresolved beside its resolved candidates", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const {contextBridge,ipcRenderer}=require("electron");
|
||||
const forward = channel => ipcRenderer.send(channel);
|
||||
contextBridge.exposeInMainWorld("desktop",{
|
||||
a:()=>forward("a"),
|
||||
b:()=>forward("b"),
|
||||
any:name=>forward(name),
|
||||
});
|
||||
`);
|
||||
const sends = result.electron.ipc.filter(
|
||||
({ side, operation }) => side === "renderer" && operation === "send",
|
||||
);
|
||||
expect(sends.map(({ channel }) => channel).sort()).toEqual(["a", "b", null]);
|
||||
expect(
|
||||
new Set(sends.map(({ location }) => JSON.stringify(location))).size,
|
||||
).toBe(1);
|
||||
});
|
||||
it.each([
|
||||
[
|
||||
"an exposed member",
|
||||
`const forward = (channel, callback) => ipcRenderer.on(channel, callback);
|
||||
contextBridge.exposeInMainWorld("desktop",{on:()=>forward("pong",()=>{})});`,
|
||||
],
|
||||
[
|
||||
"an enclosing local call",
|
||||
`const forward = (channel, callback) => ipcRenderer.on(channel, callback);
|
||||
const notify = kind => forward("pong", () => {});
|
||||
notify("ready");`,
|
||||
],
|
||||
])(
|
||||
"keeps one invocation of a wrapper call also reached through %s",
|
||||
(_, entry) => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const {contextBridge,ipcRenderer}=require("electron");
|
||||
${entry}
|
||||
`);
|
||||
expect(
|
||||
result.electron.ipc.filter(({ channel }) => channel === "pong"),
|
||||
).toHaveLength(1);
|
||||
},
|
||||
);
|
||||
it("keeps distinct handlers that separate outer calls pass through one wrapper call", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const e = require("electron");
|
||||
function register(channel, handler) { e.ipcMain.on(channel, handler); }
|
||||
function setup(tag, handler) { register("same", handler); }
|
||||
setup("first", () => 1);
|
||||
setup("second", () => 2);
|
||||
`);
|
||||
const registrations = result.electron.ipc.filter(
|
||||
({ channel }) => channel === "same",
|
||||
);
|
||||
// The wrapper call traced alone has an unresolved handler; each outer call
|
||||
// supplies its own concrete handler.
|
||||
expect(registrations.map(({ handler_kind }) => handler_kind).sort()).toEqual([
|
||||
"dynamic-expression",
|
||||
"inline-function",
|
||||
"inline-function",
|
||||
]);
|
||||
expect(
|
||||
new Set(
|
||||
registrations.map(({ handler_location }) =>
|
||||
JSON.stringify(handler_location),
|
||||
),
|
||||
).size,
|
||||
).toBe(3);
|
||||
});
|
||||
it("keeps the source registration when only its channel is site-determined", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const { ipcMain } = require("electron");
|
||||
const CH = "ch";
|
||||
function register(seed, handler) { ipcMain.on(CH, handler); }
|
||||
register("seed", () => 1);
|
||||
`);
|
||||
expect(
|
||||
result.electron.ipc.map(({ channel, handler_kind, resolution_basis }) => [
|
||||
channel,
|
||||
handler_kind,
|
||||
resolution_basis ?? "direct",
|
||||
]),
|
||||
).toEqual([
|
||||
[null, "identifier", "direct"],
|
||||
["ch", "inline-function", "wrapper-substitution"],
|
||||
]);
|
||||
});
|
||||
it.each([
|
||||
[
|
||||
"a closure over a wrapper parameter",
|
||||
`function register(seed) {
|
||||
const handler = () => ipcRenderer.send(seed);
|
||||
ipcRenderer.on(CH, handler);
|
||||
}`,
|
||||
],
|
||||
[
|
||||
"an arrow function reading its wrapper's arguments",
|
||||
`function register(seed) {
|
||||
const handler = () => arguments[0];
|
||||
ipcRenderer.on(CH, handler);
|
||||
}`,
|
||||
],
|
||||
[
|
||||
"an arrow function reading its wrapper's new.target",
|
||||
`function register(seed) {
|
||||
const handler = () => new.target;
|
||||
ipcRenderer.on(CH, handler);
|
||||
}`,
|
||||
],
|
||||
[
|
||||
"a closure returned from a factory of a wrapper parameter",
|
||||
`const make = value => () => value;
|
||||
function register(seed) {
|
||||
const handler = make(seed);
|
||||
ipcRenderer.on(CH, handler);
|
||||
}`,
|
||||
],
|
||||
])("keeps distinct listeners when one handler node is %s", (_, wrapper) => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
const CH = "ch", A = "a", B = "b";
|
||||
${wrapper}
|
||||
contextBridge.exposeInMainWorld("first", { on: () => register(A) });
|
||||
contextBridge.exposeInMainWorld("second", { on: () => register(B) });
|
||||
`);
|
||||
// Each invocation installs its own closure; the direct source observation
|
||||
// stays for callers this pass does not trace.
|
||||
expect(
|
||||
result.electron.ipc
|
||||
.filter(({ mode }) => mode === "listen")
|
||||
.map(({ channel, resolution_basis }) => [
|
||||
channel,
|
||||
resolution_basis ?? "direct",
|
||||
]),
|
||||
).toEqual([
|
||||
[null, "direct"],
|
||||
["ch", "wrapper-substitution"],
|
||||
["ch", "wrapper-substitution"],
|
||||
]);
|
||||
expect(
|
||||
(result.electron.call_paths ?? [])
|
||||
.filter(({ channel }) => channel === "ch")
|
||||
.map(({ entry }) => entry)
|
||||
.sort(),
|
||||
).toEqual(["first.on", "second.on"]);
|
||||
});
|
||||
it("resolves a bundle module's handler in place as one shared listener", () => {
|
||||
const result = analyzeJavaScriptStaticSource(
|
||||
bundle(`const CH = "ch";
|
||||
const handler = () => electron.ipcRenderer.send("reply");
|
||||
electron.contextBridge.exposeInMainWorld("api", {
|
||||
on: () => electron.ipcRenderer.on(CH, handler),
|
||||
});`),
|
||||
);
|
||||
expect(
|
||||
result.electron.ipc
|
||||
.filter(({ mode, module_key }) => mode === "listen" && module_key === "9")
|
||||
.map(({ channel, resolution_basis }) => [
|
||||
channel,
|
||||
resolution_basis ?? "direct",
|
||||
]),
|
||||
).toEqual([["ch", "wrapper-substitution"]]);
|
||||
});
|
||||
it("resolves an arrow handler in place when its inherited this is module scope", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
const CH = "ch";
|
||||
const register = () => {
|
||||
const handler = () => this;
|
||||
ipcRenderer.on(CH, handler);
|
||||
};
|
||||
contextBridge.exposeInMainWorld("api", { on: () => register() });
|
||||
`);
|
||||
expect(
|
||||
result.electron.ipc
|
||||
.filter(({ mode }) => mode === "listen")
|
||||
.map(({ channel, resolution_basis }) => [
|
||||
channel,
|
||||
resolution_basis ?? "direct",
|
||||
]),
|
||||
).toEqual([["ch", "wrapper-substitution"]]);
|
||||
});
|
||||
it("resolves a handler a module-level factory call created once in place", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const { ipcMain } = require("electron");
|
||||
const CH = "ch";
|
||||
const make = () => { const value = Math.random(); return () => value; };
|
||||
const handler = make();
|
||||
function register(seed) { ipcMain.on(CH, handler); }
|
||||
register("x");
|
||||
`);
|
||||
expect(
|
||||
result.electron.ipc.map(({ channel, resolution_basis }) => [
|
||||
channel,
|
||||
resolution_basis ?? "direct",
|
||||
]),
|
||||
).toEqual([["ch", "wrapper-substitution"]]);
|
||||
expect(result.parse_status).toBe("complete");
|
||||
});
|
||||
it("keeps separate outer calls that pass one shared handler through a wrapper", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const { ipcMain } = require("electron");
|
||||
const copy = () => {};
|
||||
function register(ch, handler) { ipcMain.on(ch, handler); }
|
||||
function setup(tag) { register("same", copy); }
|
||||
setup("first");
|
||||
setup("second");
|
||||
`);
|
||||
const registrations = result.electron.ipc.filter(
|
||||
({ channel }) => channel === "same",
|
||||
);
|
||||
// The shorter entry traced the same wrapper call alone; both outer calls
|
||||
// supersede it as distinct invocations.
|
||||
expect(registrations).toHaveLength(2);
|
||||
expect(
|
||||
new Set(
|
||||
registrations.map(({ invocation_location }) =>
|
||||
JSON.stringify(invocation_location),
|
||||
),
|
||||
).size,
|
||||
).toBe(2);
|
||||
});
|
||||
it("keeps a parameterless factory's closure as a per-invocation handler", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
const CH = "ch";
|
||||
const make = () => { const value = Math.random(); return () => value; };
|
||||
function register() {
|
||||
const handler = make();
|
||||
ipcRenderer.on(CH, handler);
|
||||
}
|
||||
contextBridge.exposeInMainWorld("api", { on: () => register() });
|
||||
`);
|
||||
expect(
|
||||
result.electron.ipc
|
||||
.filter(({ mode }) => mode === "listen")
|
||||
.map(({ resolution_basis }) => resolution_basis ?? "direct"),
|
||||
).toEqual(["direct", "wrapper-substitution"]);
|
||||
});
|
||||
it("resolves a module-level handler in place when the entry binds an unused parameter", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
const CH = "ch";
|
||||
const handler = () => {};
|
||||
contextBridge.exposeInMainWorld("api", {
|
||||
on: unused => ipcRenderer.on(CH, handler),
|
||||
});
|
||||
`);
|
||||
expect(result.electron.ipc).toEqual([
|
||||
expect.objectContaining({
|
||||
mode: "listen",
|
||||
channel: "ch",
|
||||
resolution_basis: "wrapper-substitution",
|
||||
}),
|
||||
]);
|
||||
expect(result.parse_status).toBe("complete");
|
||||
});
|
||||
it.each([
|
||||
["a named local", 'const handler = () => tag; register("same", handler);'],
|
||||
["an inline argument", 'register("same", () => tag);'],
|
||||
[
|
||||
"a nested helper",
|
||||
'const inner = () => register("same", () => tag); inner();',
|
||||
],
|
||||
])("keeps closures that separate outer calls create through %s", (_, body) => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const { ipcMain } = require("electron");
|
||||
function register(ch, handler) { ipcMain.on(ch, handler); }
|
||||
function setup(tag) { ${body} }
|
||||
setup("first");
|
||||
setup("second");
|
||||
`);
|
||||
const invocations = result.electron.ipc
|
||||
.filter(
|
||||
({ channel, resolution_basis }) =>
|
||||
channel === "same" && resolution_basis === "wrapper-substitution",
|
||||
)
|
||||
.map(({ invocation_location }) => JSON.stringify(invocation_location));
|
||||
// Both outer calls stay, beside the shorter entry that traced the same
|
||||
// wrapper call without either caller's value.
|
||||
expect(new Set(invocations).size).toBe(3);
|
||||
});
|
||||
it("counts one exposed renderer listener callsite once and retains its bridge path", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
const {contextBridge,ipcRenderer}=require("electron");
|
||||
|
||||
Reference in new issue
Block a user