From 445691dc872144f16c3aefaa002cd2bc50d94b62 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:48:16 -0700 Subject: [PATCH 1/2] typescript: a method passed by reference registers with the function that passes it `xs.forEach(this.handle, this)`, `el.addEventListener('click', this.onClick)` and `setTimeout(this.onHover.bind(this))` hand an instance method over without calling it, and the passer had no row for it, where a free function passed at the same position had one. method_value reads the member off the receiver's type (or shape) the way an instance call does, sees through `.bind`, and leaves getters, setters, data fields and `.call`/`.apply` alone. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../src/globals.d.ts | 19 ++++++++ .../src/tsconfig.json | 9 ++++ .../src/widget.ts | 46 +++++++++++++++++++ .../08-function-values-and-callbacks.envelope | 2 + ...function-values-and-callbacks.lib.envelope | 2 + .../21-callable-function-members.envelope | 2 + .../22-loose-bind-call-apply.envelope | 2 + .../62-field-access-and-type-use.envelope | 1 + .../73-assigned-interface-members.envelope | 3 ++ .../80-method-passed-by-reference.edges | 20 ++++++++ .../80-method-passed-by-reference.entries | 4 ++ .../80-method-passed-by-reference.envelope | 2 + .../80-method-passed-by-reference.fields | 2 + ...0-method-passed-by-reference.fields-oracle | 7 +++ .../80-method-passed-by-reference.oracle | 3 ++ .../80-method-passed-by-reference.type-use | 4 ++ ...80-method-passed-by-reference.types-oracle | 7 +++ .../engine/resolution/value-flow.dl | 40 ++++++++++++++++ graph/typescript/souffle/decls_all.dl | 2 + 19 files changed, 177 insertions(+) create mode 100644 graph/test/typescript/cases/80-method-passed-by-reference/src/globals.d.ts create mode 100644 graph/test/typescript/cases/80-method-passed-by-reference/src/tsconfig.json create mode 100644 graph/test/typescript/cases/80-method-passed-by-reference/src/widget.ts create mode 100644 graph/test/typescript/expected/62-field-access-and-type-use.envelope create mode 100644 graph/test/typescript/expected/80-method-passed-by-reference.edges create mode 100644 graph/test/typescript/expected/80-method-passed-by-reference.entries create mode 100644 graph/test/typescript/expected/80-method-passed-by-reference.envelope create mode 100644 graph/test/typescript/expected/80-method-passed-by-reference.fields create mode 100644 graph/test/typescript/expected/80-method-passed-by-reference.fields-oracle create mode 100644 graph/test/typescript/expected/80-method-passed-by-reference.oracle create mode 100644 graph/test/typescript/expected/80-method-passed-by-reference.type-use create mode 100644 graph/test/typescript/expected/80-method-passed-by-reference.types-oracle diff --git a/graph/test/typescript/cases/80-method-passed-by-reference/src/globals.d.ts b/graph/test/typescript/cases/80-method-passed-by-reference/src/globals.d.ts new file mode 100644 index 000000000..b76b4fc98 --- /dev/null +++ b/graph/test/typescript/cases/80-method-passed-by-reference/src/globals.d.ts @@ -0,0 +1,19 @@ +// The slice of the standard library this case hands methods to. Declared here, as client code, so the +// client-only pass sees `forEach`, `map` and `bind` as what they are to the project: signatures with no body. +interface Array { + map(fn: (v: T) => U, thisArg?: unknown): U[]; + forEach(fn: (v: T) => void, thisArg?: unknown): void; +} +interface Object {} +interface Boolean {} +interface Number {} +interface String {} +interface Function { + bind(thisArg: unknown, ...args: unknown[]): any; + call(thisArg: unknown, ...args: unknown[]): any; +} +interface CallableFunction extends Function {} +interface NewableFunction extends Function {} +interface IArguments {} +interface RegExp {} +declare function setTimeout(cb: () => void, ms?: number): number; diff --git a/graph/test/typescript/cases/80-method-passed-by-reference/src/tsconfig.json b/graph/test/typescript/cases/80-method-passed-by-reference/src/tsconfig.json new file mode 100644 index 000000000..904cf7a11 --- /dev/null +++ b/graph/test/typescript/cases/80-method-passed-by-reference/src/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true + } +} diff --git a/graph/test/typescript/cases/80-method-passed-by-reference/src/widget.ts b/graph/test/typescript/cases/80-method-passed-by-reference/src/widget.ts new file mode 100644 index 000000000..acb892a6f --- /dev/null +++ b/graph/test/typescript/cases/80-method-passed-by-reference/src/widget.ts @@ -0,0 +1,46 @@ +// A METHOD PASSED BY REFERENCE. `this.m`, `this.m.bind(this)`, `xs.forEach(this.m, this)` and a listener +// registered on a typed receiver hand the method over without calling it, exactly as `xs.map(double)` hands a +// free function over, so the method that writes the hand-off reaches the method it hands. +export interface Target { + addEventListener(type: string, fn: () => void): void; +} + +export class Widget { + private scale = 2; + handle(n: number): number { return n * this.scale; } + onClick(): number { return 1; } + onHover(): number { return 2; } + onTap(): number { return 4; } + later(): () => void { return () => {}; } + label = 'w'; + + start(xs: number[]): number[] { + xs.forEach(this.handle.bind(this)); + return xs.map(this.handle, this); + } + + wire(el: Target): void { + el.addEventListener('click', this.onClick); + setTimeout(this.onHover.bind(this)); + } + + // a method of ANOTHER instance, handed over through a typed receiver + relay(other: Widget, xs: number[]): void { + xs.forEach(other.handle, other); + } + + // CONTROL: an arrow that CALLS the method is the arrow's call, as before — the passer registers the arrow only + tap(el: Target): void { + el.addEventListener('tap', () => this.onTap()); + } + + // CONTROL: `call` RUNS the method here and hands over what it returns (the arrow `later` returns, not + // `later`), a data field is not a function, and a getter is READ where it is written: its value is handed over + now(xs: number[]): void { + setTimeout(this.later.call(this)); + xs.forEach(this.label as any); + xs.forEach(this.current); + } + + get current(): (n: number) => void { return () => {}; } +} diff --git a/graph/test/typescript/expected/08-function-values-and-callbacks.envelope b/graph/test/typescript/expected/08-function-values-and-callbacks.envelope index 35412fef8..b629f2f6b 100644 --- a/graph/test/typescript/expected/08-function-values-and-callbacks.envelope +++ b/graph/test/typescript/expected/08-function-values-and-callbacks.envelope @@ -1,5 +1,7 @@ value values#@4:25 -> values#@6 value values#@4:25 -> values#@7 +value values#@4:25 -> values#Pipeline.tag value_call values#Pipeline.run -> values#@6 value_call values#apply -> values#@6 value_call values#apply -> values#@7 +value_call values#drive -> values#Pipeline.tag diff --git a/graph/test/typescript/expected/08-function-values-and-callbacks.lib.envelope b/graph/test/typescript/expected/08-function-values-and-callbacks.lib.envelope index b3f60e377..c3b500e6a 100644 --- a/graph/test/typescript/expected/08-function-values-and-callbacks.lib.envelope +++ b/graph/test/typescript/expected/08-function-values-and-callbacks.lib.envelope @@ -1,6 +1,8 @@ value lib:hooks#@1:20 -> values#@37 value values#@4:25 -> values#@6 value values#@4:25 -> values#@7 +value values#@4:25 -> values#Pipeline.tag value_call values#Pipeline.run -> values#@6 value_call values#apply -> values#@6 value_call values#apply -> values#@7 +value_call values#drive -> values#Pipeline.tag diff --git a/graph/test/typescript/expected/21-callable-function-members.envelope b/graph/test/typescript/expected/21-callable-function-members.envelope index e914caf7d..00ae5de16 100644 --- a/graph/test/typescript/expected/21-callable-function-members.envelope +++ b/graph/test/typescript/expected/21-callable-function-members.envelope @@ -1,3 +1,5 @@ nominal globals#Function.apply -> globals#CallableFunction.apply nominal globals#Function.bind -> globals#CallableFunction.bind nominal globals#Function.call -> globals#CallableFunction.call +value_call strict-bind#viaArrow -> strict-bind# +value_call strict-bind#viaDeclaredFunction -> strict-bind#localFormat diff --git a/graph/test/typescript/expected/22-loose-bind-call-apply.envelope b/graph/test/typescript/expected/22-loose-bind-call-apply.envelope index e914caf7d..858625e64 100644 --- a/graph/test/typescript/expected/22-loose-bind-call-apply.envelope +++ b/graph/test/typescript/expected/22-loose-bind-call-apply.envelope @@ -1,3 +1,5 @@ nominal globals#Function.apply -> globals#CallableFunction.apply nominal globals#Function.bind -> globals#CallableFunction.bind nominal globals#Function.call -> globals#CallableFunction.call +value_call loose-bind#viaArrow -> loose-bind# +value_call loose-bind#viaDeclaredFunction -> loose-bind#localFormat diff --git a/graph/test/typescript/expected/62-field-access-and-type-use.envelope b/graph/test/typescript/expected/62-field-access-and-type-use.envelope new file mode 100644 index 000000000..cf52cd4f3 --- /dev/null +++ b/graph/test/typescript/expected/62-field-access-and-type-use.envelope @@ -0,0 +1 @@ +value members#@46:20 -> members#Box.run diff --git a/graph/test/typescript/expected/73-assigned-interface-members.envelope b/graph/test/typescript/expected/73-assigned-interface-members.envelope index 0c3335bd3..f71eac45d 100644 --- a/graph/test/typescript/expected/73-assigned-interface-members.envelope +++ b/graph/test/typescript/expected/73-assigned-interface-members.envelope @@ -2,8 +2,11 @@ value schema#@11:21 -> schema#@24 value schema#Internals.parse -> schema#@15 value schema#Internals.run -> schema#@15 value schema#Internals.run -> schema#@19 +value schema#Internals.run -> schema#Internals.parse value_call schema#@19 -> schema#@15 value_call schema#@24 -> schema#@15 value_call schema#@24 -> schema#@19 +value_call schema#@24 -> schema#Internals.parse value_call schema#parseTyped -> schema#@15 value_call schema#parseTyped -> schema#@19 +value_call schema#parseTyped -> schema#Internals.parse diff --git a/graph/test/typescript/expected/80-method-passed-by-reference.edges b/graph/test/typescript/expected/80-method-passed-by-reference.edges new file mode 100644 index 000000000..3d1be41b8 --- /dev/null +++ b/graph/test/typescript/expected/80-method-passed-by-reference.edges @@ -0,0 +1,20 @@ +ambient_terminal FUNCTION_CALL Widget#now(number[]) @L40 -> globals#setTimeout(() =,number) +ambient_terminal FUNCTION_CALL Widget#wire(Target) @L24 -> globals#setTimeout(() =,number) +ambiguous_unknown METHOD_CALL Widget#now(number[]) @L40 -> - +ambiguous_unknown METHOD_CALL Widget#start(number[]) @L18 -> - +ambiguous_unknown METHOD_CALL Widget#wire(Target) @L24 -> - +callback_registered FUNCTION_CALL Widget#wire(Target) @L24 -> Widget#onHover() +callback_registered METHOD_CALL Widget#relay(Widget,number[]) @L29 -> Widget#handle(number) +callback_registered METHOD_CALL Widget#start(number[]) @L18 -> Widget#handle(number) +callback_registered METHOD_CALL Widget#start(number[]) @L19 -> Widget#handle(number) +callback_registered METHOD_CALL Widget#tap(Target) @L34 -> Widget#() +callback_registered METHOD_CALL Widget#wire(Target) @L23 -> Widget#onClick() +known_edge METHOD_CALL Widget#() @L34 -> Widget#onTap() +known_edge METHOD_CALL Widget#now(number[]) @L41 -> Array#forEach((v: T) =,unknown) +known_edge METHOD_CALL Widget#now(number[]) @L42 -> Array#forEach((v: T) =,unknown) +known_edge METHOD_CALL Widget#relay(Widget,number[]) @L29 -> Array#forEach((v: T) =,unknown) +known_edge METHOD_CALL Widget#start(number[]) @L18 -> Array#forEach((v: T) =,unknown) +known_edge METHOD_CALL Widget#start(number[]) @L19 -> Array#map((v: T) =,unknown) +known_edge METHOD_CALL Widget#tap(Target) @L34 -> Target#addEventListener(string,() =) +known_edge METHOD_CALL Widget#wire(Target) @L23 -> Target#addEventListener(string,() =) +known_edge PROPERTY_READ Widget#now(number[]) @L42 -> Widget#current() diff --git a/graph/test/typescript/expected/80-method-passed-by-reference.entries b/graph/test/typescript/expected/80-method-passed-by-reference.entries new file mode 100644 index 000000000..306627ee7 --- /dev/null +++ b/graph/test/typescript/expected/80-method-passed-by-reference.entries @@ -0,0 +1,4 @@ +── entry_point (3) ── + exported_from_entry_module globals#setTimeout globals.d.ts:19 + unimported_module globals# globals.d.ts:1 + unimported_module widget# widget.ts:1 diff --git a/graph/test/typescript/expected/80-method-passed-by-reference.envelope b/graph/test/typescript/expected/80-method-passed-by-reference.envelope new file mode 100644 index 000000000..8432d19e7 --- /dev/null +++ b/graph/test/typescript/expected/80-method-passed-by-reference.envelope @@ -0,0 +1,2 @@ +value widget#@14:12 -> widget#Widget.@14 +value widget#@45:18 -> widget#Widget.@45 diff --git a/graph/test/typescript/expected/80-method-passed-by-reference.fields b/graph/test/typescript/expected/80-method-passed-by-reference.fields new file mode 100644 index 000000000..1f90c5744 --- /dev/null +++ b/graph/test/typescript/expected/80-method-passed-by-reference.fields @@ -0,0 +1,2 @@ +known_edge read Widget#handle(number) -> Widget#scale +known_edge read Widget#now(number[]) -> Widget#label diff --git a/graph/test/typescript/expected/80-method-passed-by-reference.fields-oracle b/graph/test/typescript/expected/80-method-passed-by-reference.fields-oracle new file mode 100644 index 000000000..0cf12b3fc --- /dev/null +++ b/graph/test/typescript/expected/80-method-passed-by-reference.fields-oracle @@ -0,0 +1,7 @@ +80-method-passed-by-reference [fields] + precision 1.0000 (2 correct, 0 wrong) + recall 1.0000 (2 of 2 the compiler resolved) + sites 2 resolved 2 (100.0%) + tiers known_edge=2 + access read=2 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/80-method-passed-by-reference.oracle b/graph/test/typescript/expected/80-method-passed-by-reference.oracle new file mode 100644 index 000000000..6f224890f --- /dev/null +++ b/graph/test/typescript/expected/80-method-passed-by-reference.oracle @@ -0,0 +1,3 @@ +oracle=7 engine=9 agree=7 missing=0 (known 0, NEW 0) extra=2 + extra Widget#now(number[]) -> globals#setTimeout(() =,number) + extra Widget#wire(Target) -> globals#setTimeout(() =,number) diff --git a/graph/test/typescript/expected/80-method-passed-by-reference.type-use b/graph/test/typescript/expected/80-method-passed-by-reference.type-use new file mode 100644 index 000000000..624c1ffb8 --- /dev/null +++ b/graph/test/typescript/expected/80-method-passed-by-reference.type-use @@ -0,0 +1,4 @@ +known_edge METHOD_PARAM 0 Widget [METHOD_PARAM] -> Target +known_edge METHOD_PARAM 0 Widget [METHOD_PARAM] -> Widget +known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function +known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function diff --git a/graph/test/typescript/expected/80-method-passed-by-reference.types-oracle b/graph/test/typescript/expected/80-method-passed-by-reference.types-oracle new file mode 100644 index 000000000..a08ef86e2 --- /dev/null +++ b/graph/test/typescript/expected/80-method-passed-by-reference.types-oracle @@ -0,0 +1,7 @@ +80-method-passed-by-reference [types] + precision 1.0000 (4 correct, 0 wrong) + recall 1.0000 (4 of 4 the compiler resolved) + sites 5 resolved 5 (100.0%) + tiers known_edge=5 + contexts METHOD_PARAM=3 SUPER_TYPE=2 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/typescript/engine/resolution/value-flow.dl b/graph/typescript/engine/resolution/value-flow.dl index 1989b9460..b8c6aba61 100644 --- a/graph/typescript/engine/resolution/value-flow.dl +++ b/graph/typescript/engine/resolution/value-flow.dl @@ -232,6 +232,43 @@ handed_function(ce, m) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUM expr_kind("client", "CALL_EXPRESSION", _, x), call_runs(x, f), holder_holds_function(f, m). +handed_function(ce, m) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a), + value_branch(a, x), + method_value(x, m). + +// ── method_value(Expr, Method): an INSTANCE method read as a value, not called ── +// `xs.forEach(this.handle, this)`, `el.addEventListener('click', this.onClick)`, +// `setTimeout(this.onHover.bind(this))`. expr_callable names a free function, an import and +// a static member (`Util.f`), and stops there on purpose: it feeds the callee resolver's own +// negation, so reading the receiver's type from it would put that negation in a cycle. +// Down here every resolver rule has already run, so the member is looked up exactly as an +// instance call `recv.name()` looks it up (callee-resolution rules 4 and 6). Without it a +// method passed by reference had no edge from the function that passes it, where a free +// function passed at the same position had one. +method_value(x, m) :- expr_kind("client", "PROPERTY_ACCESS", _, x), + property_access_name(x, n), + expr_child("client", x, "RECEIVER", _, q), + expr_type(q, _, qt), + method_in_scope(qt, n, "false", m), + !method_is_accessor(m). +method_value(x, m) :- expr_kind("client", "PROPERTY_ACCESS", _, x), + property_access_name(x, n), + expr_child("client", x, "RECEIVER", _, q), + expr_shape(q, s), + method_in_scope(s, n, _, m), + !method_is_accessor(m). +// `String(c.current)` with `get current()` READS the property: the getter runs there and its +// value is what is handed over, and a setter is never read at all. +method_is_accessor(m) :- method_kind(_, "GETTER", _, m). +method_is_accessor(m) :- method_kind(_, "SETTER", _, m). +// `f.bind(thisArg, …)` is `f` with its receiver fixed: what it hands over is `f`. Only `bind`: +// `f.call(…)` and `f.apply(…)` run `f` there and hand over what it returns. +method_value(x, m) :- call_site("client", _, "bind", _, recv, x, _), + recv != "", + method_value(recv, m). +method_value(x, m) :- call_site("client", _, "bind", _, recv, x, _), + recv != "", + expr_callable(recv, m). // ── value_branch(Root, Expr): Expr is a value Root can evaluate to ── value_branch(e, e) :- holder_value(_, e). @@ -259,6 +296,9 @@ holder_holds_function(h, m) :- holder_value(h, e), value_branch(e, x), !expr_kind("client", "CALL_EXPRESSION", _, x), # `return make()` holds what make RETURNS, not make: expr_callable(x, m). # a call node carries its callee as its referenced entity +holder_holds_function(h, m) :- holder_value(h, e), + value_branch(e, x), + method_value(x, m). // …AND WHAT ANOTHER HOLDER HANDS OVER. `run(fn, promise, resolve)` passes the caller's own // parameter on, and a wrapper that forwards a callback is the ordinary shape of one: the // function reaches the inner parameter only through the outer one. diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl index f48cb98bb..c312c0720 100644 --- a/graph/typescript/souffle/decls_all.dl +++ b/graph/typescript/souffle/decls_all.dl @@ -202,6 +202,8 @@ .decl call_runs_client_body(c0:symbol) .decl hof_boundary_site(c0:symbol) .decl handed_function(c0:symbol,c1:symbol) +.decl method_value(c0:symbol,c1:symbol) +.decl method_is_accessor(c0:symbol) .decl called_through(c0:symbol,c1:symbol) .decl method_enclosing_member(c0:symbol,c1:symbol) .decl call_runs(c0:symbol,c1:symbol) From 989852cab701f0c695d93f0188ac5f0fb9d481d7 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:10:52 -0700 Subject: [PATCH 2/2] tests: case 80's oracle summary counts the call CI's TypeScript compiler reports Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../typescript/expected/80-method-passed-by-reference.oracle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph/test/typescript/expected/80-method-passed-by-reference.oracle b/graph/test/typescript/expected/80-method-passed-by-reference.oracle index 6f224890f..196060769 100644 --- a/graph/test/typescript/expected/80-method-passed-by-reference.oracle +++ b/graph/test/typescript/expected/80-method-passed-by-reference.oracle @@ -1,3 +1,3 @@ -oracle=7 engine=9 agree=7 missing=0 (known 0, NEW 0) extra=2 +oracle=8 engine=10 agree=8 missing=0 (known 0, NEW 0) extra=2 extra Widget#now(number[]) -> globals#setTimeout(() =,number) extra Widget#wire(Target) -> globals#setTimeout(() =,number)