Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 49 additions & 22 deletions graph/javascript/engine/call-edge-generation/callbacks.dl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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).

Expand Down
9 changes: 5 additions & 4 deletions graph/javascript/souffle/decls_all.dl
Original file line number Diff line number Diff line change
Expand Up @@ -455,10 +455,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 lib_rooted(c0:symbol)
.decl import_outcome_is_package(c0:symbol)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,29 @@ 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();
// a function wrapped by a package call and kept in a const, then handed to a package registration: registered
function migrate() { return 1; }
Expand Down
29 changes: 25 additions & 4 deletions graph/test/javascript/expected/34-options-object-callbacks.diag
Original file line number Diff line number Diff line change
@@ -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 -> -
Expand All @@ -8,10 +10,29 @@ 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:19:16 FUNCTION_CALL walk callee_untyped
unresolved main.js:20:18 FUNCTION_CALL walk callee_untyped
unresolved main.js:21:25 METHOD_CALL register receiver_untyped
unresolved main.js:21:48 METHOD_CALL register receiver_untyped
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:41:16 FUNCTION_CALL walk callee_untyped
unresolved main.js:42:18 FUNCTION_CALL walk callee_untyped
unresolved main.js:43:25 METHOD_CALL register receiver_untyped
unresolved main.js:43:48 METHOD_CALL register receiver_untyped
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
62 changes: 49 additions & 13 deletions graph/test/javascript/expected/34-options-object-callbacks.edges
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,55 @@ 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:19:16 FUNCTION_CALL walk -> ambiguous_unknown -
main.js:19:16 FUNCTION_CALL walk -> callback_registered main.js:19:21 <arrow>
main.js:19:33 FUNCTION_CALL migrate -> known_edge main.js:18:1 migrate
main.js:20:18 FUNCTION_CALL walk -> ambiguous_unknown -
main.js:21:25 METHOD_CALL walk.register -> ambiguous_unknown -
main.js:21:25 METHOD_CALL walk.register -> callback_registered main.js:19:21 <arrow>
main.js:21:48 METHOD_CALL walk.register -> ambiguous_unknown -
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 <arrow>
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:41:16 FUNCTION_CALL walk -> ambiguous_unknown -
main.js:41:16 FUNCTION_CALL walk -> callback_registered main.js:41:21 <arrow>
main.js:41:33 FUNCTION_CALL migrate -> known_edge main.js:40:1 migrate
main.js:42:18 FUNCTION_CALL walk -> ambiguous_unknown -
main.js:43:25 METHOD_CALL walk.register -> ambiguous_unknown -
main.js:43:25 METHOD_CALL walk.register -> callback_registered main.js:41:21 <arrow>
main.js:43:48 METHOD_CALL walk.register -> ambiguous_unknown -
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 <arrow>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 -> -
Expand All @@ -6,8 +8,26 @@ 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:21:25 METHOD_CALL register member_absent
unresolved main.js:21:48 METHOD_CALL register member_absent
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:43:25 METHOD_CALL register member_absent
unresolved main.js:43:48 METHOD_CALL register member_absent
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
Loading
Loading