From 6578d95e22cc4c1f3245f0175a6d73f1191477a1 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:10:25 -0700 Subject: [PATCH] callbacks: a function is handed over only where it is written into the argument A function reached the callee of every call whose argument's abstract value might hold it: a Map key, a string, a promise, a loop variable destructured from a Map of handler sets, an object whose member held a parameter. Each config default arrow and each bus handler collected edges from unrelated get/set/has/emit/assert sites. - javascript: a project callee is registered only for a function written at the argument (literal, declaration, import, const, method read, call result); an options object only for a literal or its variable with the function written in. - javascript, typescript: Map/Set/WeakMap/WeakSet methods other than forEach store and never call; TS recognises them by declared reference or `new` initialiser. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../engine/call-edge-generation/callbacks.dl | 71 +++++++++++++------ graph/javascript/souffle/decls_all.dl | 9 +-- .../34-options-object-callbacks/src/main.js | 24 ++++++- .../expected/34-options-object-callbacks.diag | 21 ++++++ .../34-options-object-callbacks.edges | 48 +++++++++++-- .../34-options-object-callbacks.lib.diag | 20 ++++++ .../34-options-object-callbacks.lib.edges | 48 +++++++++++-- .../34-options-object-callbacks.oracle | 36 ++++++++-- .../expected/50-enumerated-copy-keys.edges | 2 - .../src/store.ts | 25 +++++++ .../78-hof-callback-at-library-boundary.edges | 7 ++ ...8-hof-callback-at-library-boundary.entries | 6 +- ...78-hof-callback-at-library-boundary.oracle | 2 +- ...-hof-callback-at-library-boundary.type-use | 2 + ...-callback-at-library-boundary.types-oracle | 8 +-- .../engine/resolution/value-flow.dl | 35 +++++++-- graph/typescript/souffle/decls_all.dl | 4 ++ 17 files changed, 310 insertions(+), 58 deletions(-) create mode 100644 graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/store.ts diff --git a/graph/javascript/engine/call-edge-generation/callbacks.dl b/graph/javascript/engine/call-edge-generation/callbacks.dl index 107842dbd..962d5fe9e 100644 --- a/graph/javascript/engine/call-edge-generation/callbacks.dl +++ b/graph/javascript/engine/call-edge-generation/callbacks.dl @@ -28,18 +28,59 @@ // ============================================================================ // ── callback_registered(CallExpr, CallbackMethod) ─────────────────────────── -callback_registered(ce, m) :- invocation_site(ce, _), call_arg(ce, _, arg), expr_value(arg, "func", m). +callback_registered(ce, m) :- invocation_site(ce, _), !call_has_client_target(ce), !collection_store_site(ce), + call_arg(ce, _, arg), expr_value(arg, "func", m). +// A PROJECT callee's own body calls what it is handed, so the edge from outside restates +// it — and only for a function WRITTEN at the argument. A parameter or loop variable passed +// on holds whatever the value flow says it may, and `for (const [pattern, handlers] of map)` +// gives the key every value too: `matches(pattern)` would register every handler. +callback_registered(ce, m) :- invocation_site(ce, _), call_has_client_target(ce), call_arg(ce, _, arg), + written_function(arg, m). +// written_function(Expr, Method): the expression names the function where it is written — +// a function or arrow literal, a function declaration or an import of one, a variable a +// literal initialises, a method read off a value (`this.onData`), or a call that returns +// one (`once(memoize(f))`, `fn.bind(this)`). +written_function(e, m) :- expr_introduces(_, m, e). +written_function(e, m) :- expr_kind(_, "IDENTIFIER", _, e), expr_binding(_, v, e), function_binding_method(v, m). +written_function(e, m) :- expr_kind(_, "IDENTIFIER", _, e), expr_binding(_, v, e), var_import(_, imp, v), + import_value(imp, "func", m). +written_function(e, m) :- expr_kind(_, "IDENTIFIER", _, e), expr_binding(_, v, e), var_init(_, _, i, v), + written_function(i, m). +written_function(e, m) :- expr_kind(_, "PROPERTY_ACCESS", _, e), expr_value(e, "func", m). +written_function(e, m) :- expr_kind(_, "CALL", _, e), expr_value(e, "func", m). +// A Map or Set stores, finds and drops its argument and never calls it (`subs.add(h)`, +// `cache.get(key)`, `seen.has(entry)`); only `forEach` runs what it is handed. The +// function is reached where it is taken back out and called, not where it is stored. +collection_store_site(ce) :- call_site(_, ck, mn, "SYNTACTIC", _, _, ce, _, _), call_kind_is_member_form(ck), mn != "forEach", + expr_child(_, ce, "RECEIVER", _, r), expr_value(r, "coll", _). // A function handed over as a PROPERTY of an options object (`lib({ filter: f })`, // `opts.resolve = f; lib(opts)`, `https.request({ createConnection: f })`, // `new Transform({ transform })`) is a callback too (#643): the callee reads the property -// and invokes it, and nothing else in the project points into the function. Followed -// two levels (`{ hooks: { visit } }`), and only across the boundary: a callee that is -// NOT a project function (a staged library, the platform, an unknown, a dynamic call), -// because a project callee that reads the property has the ordinary edge from inside -// its body. Only object values carry it (below); an array, a collection, a string, a -// module, an instance and the platform are left out. +// and invokes it, and nothing else in the project points into the function. Only across +// the boundary: a callee that is NOT a project function (a staged library, the platform, +// an unknown, a dynamic call), because a project callee that reads the property has the +// ordinary edge from inside its body. +// Read off the SOURCE, not the value flow: the argument is an object literal or a variable +// one initialises, and the function is WRITTEN into it — a property value, a method of the +// literal, an assignment to a property of that variable, or the same one literal deeper +// (`{ hooks: { visit } }`). An argument whose abstract value merely MAY be such an object +// (a parameter, a property read, a call's result, a string key the flow over-approximated) +// hands nothing over, nor does a member holding a parameter or loop variable: +// `assert.equal(cfg.port, 1)`, `cache.get(key)` and `emit('error', { pattern })` would +// otherwise register every function of every object that value might be. callback_registered(ce, m) :- invocation_site(ce, _), !call_has_client_target(ce), !reflective_site(ce), - call_arg(ce, _, arg), expr_value(arg, k, i), options_value_kind(k), options_member_func(k, i, m). + !collection_store_site(ce), call_arg(ce, _, arg), options_argument(arg, l), options_written_member(l, m). +options_argument(l, l) :- expr_kind(_, "OBJECT_LITERAL", _, l). +options_argument(arg, l) :- expr_kind(_, "IDENTIFIER", _, arg), expr_binding(_, v, arg), var_init(_, _, l, v), + expr_kind(_, "OBJECT_LITERAL", _, l). +options_written_member(l, m) :- expr_kind(_, "OBJECT_LITERAL", _, l), literal_owns_method(l, m), + method_decl(_, _, k, _, _, _, _, m), !method_kind_is_accessor(k). +options_written_member(l, m) :- expr_child(_, l, "PROPERTY_VALUE", _, v), options_written_value(v, m). +options_written_member(l, m) :- expr_kind(_, "ASSIGNMENT", _, a), expr_child(_, a, "ASSIGNMENT_TARGET", _, t), + expr_kind(_, "PROPERTY_ACCESS", _, t), expr_child(_, t, "ACCESS_TARGET", _, r), expr_kind(_, "IDENTIFIER", _, r), + options_argument(r, l), expr_child(_, a, "ASSIGNMENT_VALUE", _, v), options_written_value(v, m). +options_written_value(v, m) :- written_function(v, m). +options_written_value(v, m) :- options_argument(v, l), options_written_member(l, m). // `Object.assign(dst, src)`, `Object.entries(o)`, `Object.setPrototypeOf(a, b)`, // `Reflect.ownKeys(o)`, `JSON.stringify(o)`: reflection over an object reads its // properties and never invokes them; an options-object edge there would be invented. @@ -49,20 +90,6 @@ reflective_ambient("Object"). reflective_ambient("Reflect"). reflective_ambient("JSON"). reflective_ambient("Array"). -// Two levels, each side materialised WITHOUT the name wildcard (#671): joining -// prop_value with itself through `_` in the name column left neither side a -// prefix index for the other's lookup, and on lodash (26M prop_value tuples after -// its mixin copies every function onto every object) that one join took 43 of a -// 51-minute solve to produce 16.8K rows. The projections below are deduplicated -// per (object, member value), and each join is a prefix lookup. -options_member_func(k, i, m) :- options_member_func_own(k, i, m). -options_member_func(k, i, m) :- options_object_member(k, i, k1, i1), options_member_func_own(k1, i1, m). -options_member_func_own(k, i, m) :- prop_value(k, i, _, "func", m). -options_object_member(k, i, k1, i1) :- prop_value(k, i, _, k1, i1), options_value_kind(k1). -// Object values only: an INSTANCE passed to the platform is data (`arr.push(this)`, -// `Promise.resolve(w)`, `items.map(fn, this)`), and registering every method it has -// would invent an edge per method at every such site. -options_value_kind("obj"). call_has_client_target(ce) :- expr_resolves_to_method(ce, m), method_prov(m, "client"). call_has_client_target(ce) :- new_constructs(ce, t), type_decl("client", _, _, _, _, t). diff --git a/graph/javascript/souffle/decls_all.dl b/graph/javascript/souffle/decls_all.dl index 1f91eb17b..c90cda0e5 100644 --- a/graph/javascript/souffle/decls_all.dl +++ b/graph/javascript/souffle/decls_all.dl @@ -446,10 +446,11 @@ // ── call-edge-generation/callbacks.dl ── .decl reflective_site(c0:symbol) .decl reflective_ambient(c0:symbol) -.decl options_member_func(c0:symbol, c1:symbol, c2:symbol) -.decl options_member_func_own(c0:symbol, c1:symbol, c2:symbol) -.decl options_object_member(c0:symbol, c1:symbol, c2:symbol, c3:symbol) -.decl options_value_kind(c0:symbol) +.decl options_argument(c0:symbol, c1:symbol) +.decl options_written_member(c0:symbol, c1:symbol) +.decl options_written_value(c0:symbol, c1:symbol) +.decl written_function(c0:symbol, c1:symbol) +.decl collection_store_site(c0:symbol) .decl call_has_client_target(c0:symbol) .decl callback_registered(c0:symbol, c1:symbol) .decl event_handler(c0:symbol, c1:symbol, c2:symbol, c3:symbol) diff --git a/graph/test/javascript/cases/34-options-object-callbacks/src/main.js b/graph/test/javascript/cases/34-options-object-callbacks/src/main.js index 0a29fdf73..a956bdfe3 100644 --- a/graph/test/javascript/cases/34-options-object-callbacks/src/main.js +++ b/graph/test/javascript/cases/34-options-object-callbacks/src/main.js @@ -12,5 +12,27 @@ function viaVar() { const opts = {}; opts.filter = keep; opts.hooks = { visit }; function viaPlatform() { const o = { host: 'localhost', createConnection: connect }; https.request(o).on('error', () => {}).destroy(); } function viaCtor() { return new Transform({ transform }); } function viaProject() { return localWalk([1], { filter: keep }); } -function main() { direct(); viaVar(); viaPlatform(); viaCtor(); viaProject(); } +// Near misses: the object that holds a function reaches these arguments only through a +// parameter, a property read or a keyed collection — none hands the function over. +const assert = require('assert'); +const cache = new Map(); +const subs = new Set(); +function defineConfig(schema) { const out = {}; for (const k of Object.keys(schema)) out[k] = schema[k].default(); return out; } +const spec = { port: { default: () => 3000 } }; +const cfg = defineConfig(spec); +function lookup(key) { return cache.get(key); } +function known(entry) { return subs.has(entry); } +function subscribe(pattern, handler) { const sub = { pattern, handler }; subs.add(sub); cache.set(pattern, sub); return sub; } +function onUpdated() { return 1; } +function check(entry) { assert.equal(spec.port, cfg.port); lookup(spec); known(entry); return walk([entry], {}); } +function viaTimer() { setTimeout(() => keep(1)); [1].forEach(visit); } +// Near miss: iterating a Map of handler sets gives the KEY the handlers too, so the key +// passed to a project matcher or written into an event payload is not a hand-off. The +// handler is reached where it is called. +const byPattern = new Map(); +function on(p, h) { let s = byPattern.get(p); if (!s) { s = new Set(); byPattern.set(p, s); } s.add(h); } +function matchesKey(p, topic) { return p === topic; } +function deliver(bus, topic) { for (const [pattern, handlers] of byPattern) { if (!matchesKey(pattern, topic)) continue; bus.emit('seen', { pattern }); for (const h of handlers) h(topic); } } +function bus(ev) { on('a', onUpdated); deliver(ev, 'a'); } +function main() { direct(); viaVar(); viaPlatform(); viaCtor(); viaProject(); check(subscribe('a.*', onUpdated)); viaTimer(); bus(new (require('events'))()); } main(); diff --git a/graph/test/javascript/expected/34-options-object-callbacks.diag b/graph/test/javascript/expected/34-options-object-callbacks.diag index f261bb398..b4c9dca3b 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.diag +++ b/graph/test/javascript/expected/34-options-object-callbacks.diag @@ -1,4 +1,6 @@ +import_cause main.js:17:16 assert builtin import_cause main.js:2:14 walker not_staged +import_cause main.js:37:136 events builtin import_cause main.js:3:15 https builtin import_cause main.js:4:9 stream builtin package_entry options-callbacks . [] DEFAULT_INDEX index.js MISSING_FILE -> - @@ -8,6 +10,25 @@ unresolved main.js:12:86 METHOD_CALL destroy no_target unresolved main.js:12:86 METHOD_CALL on no_target unresolved main.js:12:86 METHOD_CALL request no_target unresolved main.js:13:29 CONSTRUCTOR_CALL Transform no_target +unresolved main.js:18:15 CONSTRUCTOR_CALL Map no_target +unresolved main.js:19:14 CONSTRUCTOR_CALL Set no_target +unresolved main.js:20:65 METHOD_CALL keys no_target +unresolved main.js:20:95 METHOD_CALL default receiver_untyped +unresolved main.js:23:31 METHOD_CALL get no_target +unresolved main.js:24:32 METHOD_CALL has no_target +unresolved main.js:25:74 METHOD_CALL add no_target +unresolved main.js:25:89 METHOD_CALL set no_target +unresolved main.js:27:25 METHOD_CALL equal no_target +unresolved main.js:27:95 FUNCTION_CALL walk callee_untyped +unresolved main.js:28:23 FUNCTION_CALL setTimeout no_target +unresolved main.js:28:50 METHOD_CALL forEach no_target +unresolved main.js:32:19 CONSTRUCTOR_CALL Map no_target +unresolved main.js:33:29 METHOD_CALL get no_target +unresolved main.js:33:61 CONSTRUCTOR_CALL Set no_target +unresolved main.js:33:72 METHOD_CALL set no_target +unresolved main.js:33:95 METHOD_CALL add no_target +unresolved main.js:35:122 METHOD_CALL emit no_target +unresolved main.js:37:131 CONSTRUCTOR_CALL no_target unresolved main.js:8:38 FUNCTION_CALL cb callee_untyped unresolved main.js:9:42 METHOD_CALL map no_target value_callee main.js:8:38 cb parameter diff --git a/graph/test/javascript/expected/34-options-object-callbacks.edges b/graph/test/javascript/expected/34-options-object-callbacks.edges index 04390e7a1..fa33e2b06 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.edges +++ b/graph/test/javascript/expected/34-options-object-callbacks.edges @@ -12,12 +12,48 @@ main.js:12:86 METHOD_CALL https.request(o).on('error', () => {}).destroy -> am main.js:13:29 CONSTRUCTOR_CALL Transform -> ambient_terminal - main.js:13:29 CONSTRUCTOR_CALL Transform -> callback_registered main.js:8:1 transform main.js:14:32 FUNCTION_CALL localWalk -> known_edge main.js:9:1 localWalk -main.js:15:19 FUNCTION_CALL direct -> known_edge main.js:10:1 direct -main.js:15:29 FUNCTION_CALL viaVar -> known_edge main.js:11:1 viaVar -main.js:15:39 FUNCTION_CALL viaPlatform -> known_edge main.js:12:1 viaPlatform -main.js:15:54 FUNCTION_CALL viaCtor -> known_edge main.js:13:1 viaCtor -main.js:15:65 FUNCTION_CALL viaProject -> known_edge main.js:14:1 viaProject -main.js:16:1 FUNCTION_CALL main -> known_edge main.js:15:1 main +main.js:18:15 CONSTRUCTOR_CALL Map -> ambient_terminal - +main.js:19:14 CONSTRUCTOR_CALL Set -> ambient_terminal - +main.js:20:65 METHOD_CALL Object.keys -> ambient_terminal - +main.js:20:95 METHOD_CALL schema[k].default -> ambiguous_unknown - +main.js:22:13 FUNCTION_CALL defineConfig -> known_edge main.js:20:1 defineConfig +main.js:23:31 METHOD_CALL cache.get -> ambient_terminal - +main.js:24:32 METHOD_CALL subs.has -> ambient_terminal - +main.js:25:74 METHOD_CALL subs.add -> ambient_terminal - +main.js:25:89 METHOD_CALL cache.set -> ambient_terminal - +main.js:27:25 METHOD_CALL assert.equal -> ambient_terminal - +main.js:27:60 FUNCTION_CALL lookup -> known_edge main.js:23:1 lookup +main.js:27:74 FUNCTION_CALL known -> known_edge main.js:24:1 known +main.js:27:95 FUNCTION_CALL walk -> ambiguous_unknown - +main.js:28:23 FUNCTION_CALL setTimeout -> ambient_terminal - +main.js:28:23 FUNCTION_CALL setTimeout -> callback_registered main.js:28:34 +main.js:28:40 FUNCTION_CALL keep -> known_edge main.js:5:1 keep +main.js:28:50 METHOD_CALL [1].forEach -> ambient_terminal - +main.js:28:50 METHOD_CALL [1].forEach -> callback_registered main.js:6:1 visit +main.js:32:19 CONSTRUCTOR_CALL Map -> ambient_terminal - +main.js:33:29 METHOD_CALL byPattern.get -> ambient_terminal - +main.js:33:61 CONSTRUCTOR_CALL Set -> ambient_terminal - +main.js:33:72 METHOD_CALL byPattern.set -> ambient_terminal - +main.js:33:95 METHOD_CALL s.add -> ambient_terminal - +main.js:35:122 METHOD_CALL bus.emit -> ambient_terminal - +main.js:35:179 FUNCTION_CALL h -> ambient_terminal - +main.js:35:179 FUNCTION_CALL h -> multi_inferred main.js:26:1 onUpdated +main.js:35:84 FUNCTION_CALL matchesKey -> known_edge main.js:34:1 matchesKey +main.js:36:20 FUNCTION_CALL on -> callback_registered main.js:26:1 onUpdated +main.js:36:20 FUNCTION_CALL on -> known_edge main.js:33:1 on +main.js:36:40 FUNCTION_CALL deliver -> known_edge main.js:35:1 deliver +main.js:37:115 FUNCTION_CALL viaTimer -> known_edge main.js:28:1 viaTimer +main.js:37:127 FUNCTION_CALL bus -> known_edge main.js:36:1 bus +main.js:37:131 CONSTRUCTOR_CALL (require('events')) -> ambient_terminal - +main.js:37:19 FUNCTION_CALL direct -> known_edge main.js:10:1 direct +main.js:37:29 FUNCTION_CALL viaVar -> known_edge main.js:11:1 viaVar +main.js:37:39 FUNCTION_CALL viaPlatform -> known_edge main.js:12:1 viaPlatform +main.js:37:54 FUNCTION_CALL viaCtor -> known_edge main.js:13:1 viaCtor +main.js:37:65 FUNCTION_CALL viaProject -> known_edge main.js:14:1 viaProject +main.js:37:79 FUNCTION_CALL check -> known_edge main.js:27:1 check +main.js:37:85 FUNCTION_CALL subscribe -> callback_registered main.js:26:1 onUpdated +main.js:37:85 FUNCTION_CALL subscribe -> known_edge main.js:25:1 subscribe +main.js:38:1 FUNCTION_CALL main -> known_edge main.js:37:1 main main.js:8:38 FUNCTION_CALL cb -> ambiguous_unknown - main.js:9:42 METHOD_CALL items.map -> ambient_terminal - main.js:9:42 METHOD_CALL items.map -> callback_registered main.js:9:52 diff --git a/graph/test/javascript/expected/34-options-object-callbacks.lib.diag b/graph/test/javascript/expected/34-options-object-callbacks.lib.diag index 99d2f7ba0..dbe0282b5 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.lib.diag +++ b/graph/test/javascript/expected/34-options-object-callbacks.lib.diag @@ -1,3 +1,5 @@ +import_cause main.js:17:16 assert builtin +import_cause main.js:37:136 events builtin import_cause main.js:3:15 https builtin import_cause main.js:4:9 stream builtin package_entry options-callbacks . [] DEFAULT_INDEX index.js MISSING_FILE -> - @@ -6,6 +8,24 @@ unresolved main.js:12:86 METHOD_CALL destroy no_target unresolved main.js:12:86 METHOD_CALL on no_target unresolved main.js:12:86 METHOD_CALL request no_target unresolved main.js:13:29 CONSTRUCTOR_CALL Transform no_target +unresolved main.js:18:15 CONSTRUCTOR_CALL Map no_target +unresolved main.js:19:14 CONSTRUCTOR_CALL Set no_target +unresolved main.js:20:65 METHOD_CALL keys no_target +unresolved main.js:20:95 METHOD_CALL default receiver_untyped +unresolved main.js:23:31 METHOD_CALL get no_target +unresolved main.js:24:32 METHOD_CALL has no_target +unresolved main.js:25:74 METHOD_CALL add no_target +unresolved main.js:25:89 METHOD_CALL set no_target +unresolved main.js:27:25 METHOD_CALL equal no_target +unresolved main.js:28:23 FUNCTION_CALL setTimeout no_target +unresolved main.js:28:50 METHOD_CALL forEach no_target +unresolved main.js:32:19 CONSTRUCTOR_CALL Map no_target +unresolved main.js:33:29 METHOD_CALL get no_target +unresolved main.js:33:61 CONSTRUCTOR_CALL Set no_target +unresolved main.js:33:72 METHOD_CALL set no_target +unresolved main.js:33:95 METHOD_CALL add no_target +unresolved main.js:35:122 METHOD_CALL emit no_target +unresolved main.js:37:131 CONSTRUCTOR_CALL no_target unresolved main.js:8:38 FUNCTION_CALL cb callee_untyped unresolved main.js:9:42 METHOD_CALL map no_target value_callee main.js:8:38 cb parameter diff --git a/graph/test/javascript/expected/34-options-object-callbacks.lib.edges b/graph/test/javascript/expected/34-options-object-callbacks.lib.edges index f5582b137..7c0ae679f 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.lib.edges +++ b/graph/test/javascript/expected/34-options-object-callbacks.lib.edges @@ -12,12 +12,48 @@ main.js:12:86 METHOD_CALL https.request(o).on('error', () => {}).destroy -> am main.js:13:29 CONSTRUCTOR_CALL Transform -> ambient_terminal - main.js:13:29 CONSTRUCTOR_CALL Transform -> callback_registered main.js:8:1 transform main.js:14:32 FUNCTION_CALL localWalk -> known_edge main.js:9:1 localWalk -main.js:15:19 FUNCTION_CALL direct -> known_edge main.js:10:1 direct -main.js:15:29 FUNCTION_CALL viaVar -> known_edge main.js:11:1 viaVar -main.js:15:39 FUNCTION_CALL viaPlatform -> known_edge main.js:12:1 viaPlatform -main.js:15:54 FUNCTION_CALL viaCtor -> known_edge main.js:13:1 viaCtor -main.js:15:65 FUNCTION_CALL viaProject -> known_edge main.js:14:1 viaProject -main.js:16:1 FUNCTION_CALL main -> known_edge main.js:15:1 main +main.js:18:15 CONSTRUCTOR_CALL Map -> ambient_terminal - +main.js:19:14 CONSTRUCTOR_CALL Set -> ambient_terminal - +main.js:20:65 METHOD_CALL Object.keys -> ambient_terminal - +main.js:20:95 METHOD_CALL schema[k].default -> ambiguous_unknown - +main.js:22:13 FUNCTION_CALL defineConfig -> known_edge main.js:20:1 defineConfig +main.js:23:31 METHOD_CALL cache.get -> ambient_terminal - +main.js:24:32 METHOD_CALL subs.has -> ambient_terminal - +main.js:25:74 METHOD_CALL subs.add -> ambient_terminal - +main.js:25:89 METHOD_CALL cache.set -> ambient_terminal - +main.js:27:25 METHOD_CALL assert.equal -> ambient_terminal - +main.js:27:60 FUNCTION_CALL lookup -> known_edge main.js:23:1 lookup +main.js:27:74 FUNCTION_CALL known -> known_edge main.js:24:1 known +main.js:27:95 FUNCTION_CALL walk -> boundary_lib lib:index.js:2:1 walk +main.js:28:23 FUNCTION_CALL setTimeout -> ambient_terminal - +main.js:28:23 FUNCTION_CALL setTimeout -> callback_registered main.js:28:34 +main.js:28:40 FUNCTION_CALL keep -> known_edge main.js:5:1 keep +main.js:28:50 METHOD_CALL [1].forEach -> ambient_terminal - +main.js:28:50 METHOD_CALL [1].forEach -> callback_registered main.js:6:1 visit +main.js:32:19 CONSTRUCTOR_CALL Map -> ambient_terminal - +main.js:33:29 METHOD_CALL byPattern.get -> ambient_terminal - +main.js:33:61 CONSTRUCTOR_CALL Set -> ambient_terminal - +main.js:33:72 METHOD_CALL byPattern.set -> ambient_terminal - +main.js:33:95 METHOD_CALL s.add -> ambient_terminal - +main.js:35:122 METHOD_CALL bus.emit -> ambient_terminal - +main.js:35:179 FUNCTION_CALL h -> ambient_terminal - +main.js:35:179 FUNCTION_CALL h -> multi_inferred main.js:26:1 onUpdated +main.js:35:84 FUNCTION_CALL matchesKey -> known_edge main.js:34:1 matchesKey +main.js:36:20 FUNCTION_CALL on -> callback_registered main.js:26:1 onUpdated +main.js:36:20 FUNCTION_CALL on -> known_edge main.js:33:1 on +main.js:36:40 FUNCTION_CALL deliver -> known_edge main.js:35:1 deliver +main.js:37:115 FUNCTION_CALL viaTimer -> known_edge main.js:28:1 viaTimer +main.js:37:127 FUNCTION_CALL bus -> known_edge main.js:36:1 bus +main.js:37:131 CONSTRUCTOR_CALL (require('events')) -> ambient_terminal - +main.js:37:19 FUNCTION_CALL direct -> known_edge main.js:10:1 direct +main.js:37:29 FUNCTION_CALL viaVar -> known_edge main.js:11:1 viaVar +main.js:37:39 FUNCTION_CALL viaPlatform -> known_edge main.js:12:1 viaPlatform +main.js:37:54 FUNCTION_CALL viaCtor -> known_edge main.js:13:1 viaCtor +main.js:37:65 FUNCTION_CALL viaProject -> known_edge main.js:14:1 viaProject +main.js:37:79 FUNCTION_CALL check -> known_edge main.js:27:1 check +main.js:37:85 FUNCTION_CALL subscribe -> callback_registered main.js:26:1 onUpdated +main.js:37:85 FUNCTION_CALL subscribe -> known_edge main.js:25:1 subscribe +main.js:38:1 FUNCTION_CALL main -> known_edge main.js:37:1 main main.js:8:38 FUNCTION_CALL cb -> ambiguous_unknown - main.js:9:42 METHOD_CALL items.map -> ambient_terminal - main.js:9:42 METHOD_CALL items.map -> callback_registered main.js:9:52 diff --git a/graph/test/javascript/expected/34-options-object-callbacks.oracle b/graph/test/javascript/expected/34-options-object-callbacks.oracle index be68d1524..f4ec8215b 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.oracle +++ b/graph/test/javascript/expected/34-options-object-callbacks.oracle @@ -1,8 +1,32 @@ main.js:14:32 FUNCTION_CALL localWalk EXACT main.js:9:1 -main.js:15:19 FUNCTION_CALL direct EXACT main.js:10:1 -main.js:15:29 FUNCTION_CALL viaVar EXACT main.js:11:1 -main.js:15:39 FUNCTION_CALL viaPlatform EXACT main.js:12:1 -main.js:15:54 FUNCTION_CALL viaCtor EXACT main.js:13:1 -main.js:15:65 FUNCTION_CALL viaProject EXACT main.js:14:1 -main.js:16:1 FUNCTION_CALL main EXACT main.js:15:1 +main.js:18:15 CONSTRUCTOR_CALL Map LIB_AMBIENT_OK +main.js:19:14 CONSTRUCTOR_CALL Set LIB_AMBIENT_OK +main.js:20:65 METHOD_CALL keys LIB_AMBIENT_OK +main.js:22:13 FUNCTION_CALL defineConfig EXACT main.js:20:1 +main.js:23:31 METHOD_CALL get LIB_AMBIENT_OK +main.js:24:32 METHOD_CALL has LIB_AMBIENT_OK +main.js:25:74 METHOD_CALL add LIB_AMBIENT_OK +main.js:25:89 METHOD_CALL set LIB_AMBIENT_OK +main.js:27:60 FUNCTION_CALL lookup EXACT main.js:23:1 +main.js:27:74 FUNCTION_CALL known EXACT main.js:24:1 +main.js:28:23 FUNCTION_CALL setTimeout LIB_AMBIENT_OK +main.js:28:40 FUNCTION_CALL keep EXACT main.js:5:1 +main.js:28:50 METHOD_CALL forEach LIB_AMBIENT_OK +main.js:32:19 CONSTRUCTOR_CALL Map LIB_AMBIENT_OK +main.js:33:29 METHOD_CALL get LIB_AMBIENT_OK +main.js:33:61 CONSTRUCTOR_CALL Set LIB_AMBIENT_OK +main.js:33:72 METHOD_CALL set LIB_AMBIENT_OK +main.js:35:84 FUNCTION_CALL matchesKey EXACT main.js:34:1 +main.js:36:20 FUNCTION_CALL on EXACT main.js:33:1 +main.js:36:40 FUNCTION_CALL deliver EXACT main.js:35:1 +main.js:37:115 FUNCTION_CALL viaTimer EXACT main.js:28:1 +main.js:37:127 FUNCTION_CALL bus EXACT main.js:36:1 +main.js:37:19 FUNCTION_CALL direct EXACT main.js:10:1 +main.js:37:29 FUNCTION_CALL viaVar EXACT main.js:11:1 +main.js:37:39 FUNCTION_CALL viaPlatform EXACT main.js:12:1 +main.js:37:54 FUNCTION_CALL viaCtor EXACT main.js:13:1 +main.js:37:65 FUNCTION_CALL viaProject EXACT main.js:14:1 +main.js:37:79 FUNCTION_CALL check EXACT main.js:27:1 +main.js:37:85 FUNCTION_CALL subscribe EXACT main.js:25:1 +main.js:38:1 FUNCTION_CALL main EXACT main.js:37:1 # defects: 0 diff --git a/graph/test/javascript/expected/50-enumerated-copy-keys.edges b/graph/test/javascript/expected/50-enumerated-copy-keys.edges index 3b463f443..6918da3e4 100644 --- a/graph/test/javascript/expected/50-enumerated-copy-keys.edges +++ b/graph/test/javascript/expected/50-enumerated-copy-keys.edges @@ -12,8 +12,6 @@ main.js:19:31 METHOD_CALL keys(obj2).forEach -> callback_registered main.js:1 main.js:20:12 FUNCTION_CALL extend -> known_edge main.js:19:1 extend main.js:21:24 METHOD_CALL d4.alpha -> known_edge main.js:7:15 alpha main.js:25:55 FUNCTION_CALL ownKeys -> ambient_terminal - -main.js:25:55 FUNCTION_CALL ownKeys -> callback_registered main.js:7:15 alpha -main.js:25:55 FUNCTION_CALL ownKeys -> callback_registered main.js:7:42 beta main.js:26:12 FUNCTION_CALL assign -> known_edge main.js:25:1 assign main.js:27:24 METHOD_CALL d5.beta -> known_edge main.js:7:42 beta main.js:28:16 METHOD_CALL Object.getOwnPropertyNames -> ambient_terminal - diff --git a/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/store.ts b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/store.ts new file mode 100644 index 000000000..6ad0b7b8e --- /dev/null +++ b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/store.ts @@ -0,0 +1,25 @@ +// NEAR MISS: a Map or Set stores, finds and drops a function and never calls it, so storing one is not +// handing it over: `set`, `add` and `has` reach nothing. Neither is declared in this case, so each is +// recognised by the reference it was declared with or by the `new` that initialises it. +function onUpdated(): number { + return 1 +} + +const handlers = new Map() +const subs: Set<() => number> = new Set() + +export function store(): void { + handlers.set('a', onUpdated) + subs.add(onUpdated) +} + +export function known(): boolean { + return subs.has(onUpdated) +} + +// CONTROL: a host API with no body that is handed the same function still reaches it. +declare function later(cb: () => number): void + +export function deferred(): void { + later(onUpdated) +} diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.edges b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.edges index 4c3712657..f770ff8f4 100644 --- a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.edges +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.edges @@ -1,5 +1,12 @@ ambient_terminal FUNCTION_CALL app#atStartup() @L38 -> app#onReady(() =) +ambient_terminal FUNCTION_CALL store#deferred() @L24 -> store#later(() =) +ambiguous_unknown CONSTRUCTOR_CALL store#() @L8 -> - +ambiguous_unknown CONSTRUCTOR_CALL store#() @L9 -> - +ambiguous_unknown METHOD_CALL store#known() @L17 -> - +ambiguous_unknown METHOD_CALL store#store() @L12 -> - +ambiguous_unknown METHOD_CALL store#store() @L13 -> - callback_registered FUNCTION_CALL app#atStartup() @L38 -> app#() +callback_registered FUNCTION_CALL store#deferred() @L24 -> store#onUpdated() callback_registered METHOD_CALL app#doubled(number[]) @L16 -> app#(?) callback_registered METHOD_CALL app#named(number[]) @L25 -> app#double(number) callback_registered METHOD_CALL app#scaled(number[]) @L12 -> app#(?) diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.entries b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.entries index 5c3d7421a..08e825e3a 100644 --- a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.entries +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.entries @@ -1,4 +1,4 @@ -── entry_point (9) ── +── entry_point (13) ── exported_from_entry_module app#atStartup app.ts:37 exported_from_entry_module app#doubled app.ts:15 exported_from_entry_module app#each app.ts:43 @@ -6,5 +6,9 @@ exported_from_entry_module app#scaled app.ts:11 exported_from_entry_module app#useEach app.ts:47 exported_from_entry_module app#viaLocal app.ts:29 + exported_from_entry_module store#deferred store.ts:23 + exported_from_entry_module store#known store.ts:16 + exported_from_entry_module store#store store.ts:11 unimported_module app# app.ts:1 unimported_module globals# globals.d.ts:1 + unimported_module store# store.ts:1 diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.oracle b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.oracle index f31a2b293..373cdb593 100644 --- a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.oracle +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.oracle @@ -1 +1 @@ -oracle=10 engine=10 agree=10 missing=0 (known 0, NEW 0) extra=0 +oracle=11 engine=11 agree=11 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.type-use b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.type-use index 960a82a1b..93f4487af 100644 --- a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.type-use +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.type-use @@ -1,2 +1,4 @@ +ambiguous_unknown OBJECT_CREATION_TYPE 0 store [EXPRESSION] -> - +ambiguous_unknown VARIABLE_TYPE 0 store [VARIABLE] -> - known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.types-oracle b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.types-oracle index e7a4988d6..543823f0f 100644 --- a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.types-oracle +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.types-oracle @@ -1,7 +1,7 @@ 78-hof-callback-at-library-boundary [types] 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 - contexts SUPER_TYPE=2 - not scored: 0 rows whose target is not a client declaration + sites 5 resolved 2 (40.0%) + tiers ambiguous_unknown=3 known_edge=2 + contexts OBJECT_CREATION_TYPE=2 SUPER_TYPE=2 VARIABLE_TYPE=1 + not scored: 3 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 b8c6aba61..734debe1c 100644 --- a/graph/typescript/engine/resolution/value-flow.dl +++ b/graph/typescript/engine/resolution/value-flow.dl @@ -212,27 +212,52 @@ holder_value(p, a) :- expr_child("client", ce, "ARGUMENT", pos, a), // ============================================================================ call_runs_client_body(ce) :- call_runs_method(ce, m), method_prov(m, "client"). hof_boundary_site(ce) :- call_site("client", _, _, _, _, ce, _), !call_runs_client_body(ce). +// A Map or Set stores, finds and drops its argument and never calls it (`subs.add(h)`, +// `handlers.set(k, h)`, `seen.has(h)`); only `forEach` runs what it is handed. The function +// is reached where it is taken back out and called, not where it is stored. +collection_store_site(ce) :- expr_resolves_to_method(ce, m), method_decl("lib", mn, _, _, m), mn != "forEach", + method_owner("lib", t, m), type_decl("lib", tn, _, _, _, _, t), keyed_collection_name(tn). +collection_store_site(ce) :- call_site("client", _, mn, _, recv, ce, _), recv != "", mn != "forEach", + expr_type(recv, _, t), type_decl(_, tn, _, _, _, _, t), keyed_collection_name(tn). +collection_store_site(ce) :- call_site("client", _, mn, _, recv, ce, _), recv != "", mn != "forEach", + collection_receiver(recv). +// Without the standard library staged there is no Map declaration to resolve to, so the +// receiver is recognised by the reference it was declared with, or by the `new Map()` an +// un-annotated variable is initialised with. Recognised by NAME only where the program +// does not declare that name itself (builtin_container_ref). +collection_receiver(e) :- expr_decl_ref(e, r), keyed_collection_ref(r). +collection_receiver(e) :- new_expression_ref(e, r), keyed_collection_ref(r). +collection_receiver(e) :- expr_referenced("client", "VARIABLE", v, e), !var_has_declared_type(v), + var_initializer("client", _, init, v), init != "", collection_receiver(init). +keyed_collection_ref(r) :- type_ref(_, "TYPE_REFERENCE", _, tn, _, _, _, r), keyed_collection_name(tn), + builtin_container_ref(r). +keyed_collection_name("Map"). +keyed_collection_name("ReadonlyMap"). +keyed_collection_name("WeakMap"). +keyed_collection_name("Set"). +keyed_collection_name("ReadonlySet"). +keyed_collection_name("WeakSet"). value_branch(a, a) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a). -handed_function(ce, m) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a), +handed_function(ce, m) :- hof_boundary_site(ce), !collection_store_site(ce), expr_child("client", ce, "ARGUMENT", _, a), value_branch(a, x), !expr_kind("client", "CALL_EXPRESSION", _, x), # `xs.map(make())` hands over what make RETURNS expr_callable(x, m). -handed_function(ce, m) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a), +handed_function(ce, m) :- hof_boundary_site(ce), !collection_store_site(ce), expr_child("client", ce, "ARGUMENT", _, a), value_branch(a, x), expr_referenced("client", k, h, x), holder_ref_kind(k), holder_holds_function(h, m). -handed_function(ce, m) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a), +handed_function(ce, m) :- hof_boundary_site(ce), !collection_store_site(ce), expr_child("client", ce, "ARGUMENT", _, a), value_branch(a, x), ts_field_access_target(x, f), holder_holds_function(f, m). -handed_function(ce, m) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a), +handed_function(ce, m) :- hof_boundary_site(ce), !collection_store_site(ce), expr_child("client", ce, "ARGUMENT", _, a), value_branch(a, x), 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), +handed_function(ce, m) :- hof_boundary_site(ce), !collection_store_site(ce), expr_child("client", ce, "ARGUMENT", _, a), value_branch(a, x), method_value(x, m). diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl index 36d89cb37..428096337 100644 --- a/graph/typescript/souffle/decls_all.dl +++ b/graph/typescript/souffle/decls_all.dl @@ -215,6 +215,10 @@ .decl holder_holds_function(c0:symbol,c1:symbol) .decl call_runs_client_body(c0:symbol) .decl hof_boundary_site(c0:symbol) +.decl collection_store_site(c0:symbol) +.decl keyed_collection_name(c0:symbol) +.decl collection_receiver(c0:symbol) +.decl keyed_collection_ref(c0:symbol) .decl handed_function(c0:symbol,c1:symbol) .decl method_value(c0:symbol,c1:symbol) .decl method_is_accessor(c0:symbol)