Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
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
28 changes: 28 additions & 0 deletions graph/javascript/engine/call-edge-generation/callbacks.dl
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,34 @@ 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).

// A function WRAPPED BY A PACKAGE CALL and kept in a binding: `export const plugin = fp(async (app) => …)`,
// `const ext = Prisma.defineExtension((client) => …)`, then `app.register(plugin)`, `client.$extends(ext)`.
// The package returns the function it was handed, or one that runs it, and nothing in the project says
// which, so the binding had no value and the registration reached nothing, although the same literal
// handed to it bare is registered. The wrapping site is a value of its own, ("libwrap", site), carried
// wherever a value goes (a const, an import, an export), and a registration handed it registers what the
// site was handed. The site is recognised syntactically, by a callee rooted at a binding imported from a
// package, so the value stays below the resolver's negations; a project wrapper is followed through its
// body instead (value-flow.dl, "wrap").
lib_rooted(e) :- expr_kind(_, "IDENTIFIER", _, e), expr_binding(_, v, e), var_import(_, imp, v),
import_decl(_, _, _, _, _, _, out, _, imp), import_outcome_is_package(out).
lib_rooted(e) :- expr_kind(_, k, _, e), access_kind_reads_member(k), expr_child(_, e, "ACCESS_TARGET", _, r), lib_rooted(r).
import_outcome_is_package("RESOLVED_EXTERNAL").
import_outcome_is_package("UNRESOLVED_MISSING").
access_kind_reads_member("PROPERTY_ACCESS").
access_kind_reads_member("OPTIONAL_ACCESS").
lib_wrap_site(s) :- call_site(_, ck, _, _, _, _, s, _, _), call_kind_is_callee_form(ck),
expr_child(_, s, "CALLEE", _, c), lib_rooted(c).
lib_wrap_site(s) :- call_site(_, ck, _, "SYNTACTIC", _, _, s, _, _), call_kind_is_member_form(ck),
expr_child(_, s, "RECEIVER", _, r), lib_rooted(r).
// Handed over by NAME: `register(wrap(f))` needs nothing new, since the inner site already registers f
// from the same caller. The value is not a callee (callee-resolution.dl): what a call of it runs is still
// unknown, and says so.
expr_value(s, "libwrap", s) :- lib_wrap_site(s), call_arg(s, _, a), expr_value(a, "func", _).
callback_registered(ce, m) :- invocation_site(ce, _), !call_has_client_target(ce), !reflective_site(ce),
call_arg(ce, _, arg), !expr_kind(_, "CALL", _, arg),
expr_value(arg, "libwrap", s), call_arg(s, _, a), expr_value(a, "func", m).

// A listener runs with the EMITTER as `this` (`e.on('x', function () { this.other(); })`).
this_value(m, k, i) :- event_handler(k, i, _, m), method_this_binding(_, "DYNAMIC", m), !method_owner_type(m, _), k != "module".

Expand Down
31 changes: 31 additions & 0 deletions graph/javascript/engine/call-edge-generation/calls.dl
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,37 @@ module_variable_from_call(v) :- var_init("client", _, e, v), expr_kind(_, k, _,
!variable_reassigned(v), !call_passes_function(e).
call_passes_function(e) :- call_arg(e, _, a), expr_value(a, "func", _).
call_passes_function(e) :- call_arg(e, _, a), expr_introduces(_, _, a).
// `promisify(store.find.bind(store))`: a `.bind` always evaluates to a function, typed
// receiver or not. A bound platform function (`Math.max.bind(Math)`) is the platform's.
call_passes_function(e) :- call_arg(e, _, a), call_site(_, "FUNCTION_CALL_BIND", _, _, _, _, a, _, _),
expr_child(_, a, "CALLEE", _, f), !bound_platform_function(f).
bound_platform_function(f) :- expr_kind(_, "PROPERTY_ACCESS", _, f), expr_child(_, f, "ACCESS_TARGET", _, r),
expr_value(r, "ambient", _).
// A holder whose value is what a PLATFORM call returned when handed a project function
// (`this.find = promisify(s.find.bind(s))`, `const f = util.callbackify(g)`): the value
// is a wrapper the platform made around that function, and calling it runs the
// function. The platform value on the holder made the call an ambient terminal, a
// claimed correct end, and path said "independent" of the very method the wrapper
// runs. A platform value made from no project function (`promisify(setTimeout)`)
// keeps its platform reading.
// When the graph knows the function the wrapper was made from (a function value, or
// the method a `.bind` site resolves to), the call runs it: one of a set, beside the
// platform row the call keeps. Only when it knows none is the callee an open value.
unresolved_value_callee(ce, "field") :- wrapper_holder_call(ce, val), !wrapper_call_target(ce, _), made_from_function(val),
field_call(ce, _, _, _).
unresolved_value_callee(ce, "module_variable") :- wrapper_holder_call(ce, e), !wrapper_call_target(ce, _), made_from_function(e),
!field_call(ce, _, _, _).
wrapper_holder_call(ce, val) :- field_call(ce, k, t, n), !call_resolved(ce), field_assignment(k, t, n, val).
wrapper_holder_call(ce, e) :- value_callee_unresolved(ce, c), expr_binding(_, v, c),
var_decl("client", _, _, _, _, _, v), !var_owner_method("client", _, v), !var_import(_, _, v), !expr_param(_, _, c),
var_init("client", _, e, v), !variable_reassigned(v).
made_from_function(e) :- expr_kind(_, "CALL", _, e), expr_value(e, "ambient", _), call_passes_function(e).
wrapper_runs(e, m) :- made_from_function(e), call_arg(e, _, a), expr_value(a, "func", m).
wrapper_runs(e, m) :- made_from_function(e), call_arg(e, _, a), call_site(_, "FUNCTION_CALL_BIND", _, _, _, _, a, _, _),
expr_resolves_to_method(a, m).
wrapper_call_target(ce, m) :- wrapper_holder_call(ce, e), wrapper_runs(e, m), call_target_count(ce, 0).
call_chain_edge(ce, caller, "-", m, "client", "multi_inferred", kind) :-
wrapper_call_target(ce, m), !call_over_cap(ce), call_from(ce, caller), invocation_site(ce, kind).
variable_reassigned(v) :- expr_kind(_, "ASSIGNMENT", _, a), expr_child(_, a, "ASSIGNMENT_TARGET", _, tgt),
expr_binding(_, v, tgt).
// `(c ? a : b)()`, `(0, cb)()`, `make()()`: the callee is computed by an expression.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

// ── callee_value(CallExpr, K, I) — the value being invoked ─────────────────
callee_value(ce, k, i) :- call_site(_, ck, _, _, _, _, ce, _, _), call_kind_is_callee_form(ck),
expr_child(_, ce, "CALLEE", _, c), expr_value(c, k, i).
expr_child(_, ce, "CALLEE", _, c), expr_value(c, k, i), k != "libwrap". # what a package wrapper returns runs nothing known (callbacks.dl)
// `f.call(o)`: f runs. But the parser classifies by NAME, so `selector.apply(node)`
// on an object with its own `apply` method is here too — and for that reading the
// CALLEE child (the object) is the receiver and its member is the target. Both
Expand Down
20 changes: 20 additions & 0 deletions graph/javascript/engine/resolution/ambient.dl
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,26 @@ modelled_platform_call(ce) :- expr_kind(_, "CALL", _, ce), call_site(_, "METHOD_
expr_kind(_, "IDENTIFIER", _, recv), expr_text(_, "Object", recv).
modelled_platform_call(ce) :- expr_kind(_, "CALL", _, ce), call_site(_, "METHOD_CALL", "_extend", _, _, _, ce, _, _),
expr_child(_, ce, "RECEIVER", _, recv), expr_kind(_, "IDENTIFIER", _, recv), expr_text(_, "util", recv).
// `promisify(f)` is f behind a transparent wrapper (value-flow.dl): its value is what f holds.
modelled_platform_call(ce) :- promisify_call(ce).
// promisify_call(Call): the core `util` module's `promisify`, as a name imported from it
// (`const { promisify } = require('util')`, `import { promisify } from 'node:util'`) or as
// a member of it (`util.promisify(f)` with `util` bound to the module, `require('util').promisify(f)`).
// Recognised on the import rows and the binder, never by the name alone: a project's own
// `promisify` is an ordinary function and resolves as one.
promisify_call(ce) :- call_site(_, "FUNCTION_CALL", "promisify", _, _, _, ce, _, _),
expr_child(_, ce, "CALLEE", _, c), expr_binding(_, v, c), var_import(_, imp, v),
import_decl(_, spec, _, _, "promisify", _, "RESOLVED_BUILTIN", _, imp), util_module(spec).
promisify_call(ce) :- call_site(_, "METHOD_CALL", "promisify", _, _, _, ce, _, _),
expr_child(_, ce, "RECEIVER", _, r), expr_binding(_, v, r), var_import(_, imp, v),
import_decl(_, spec, _, bf, _, _, "RESOLVED_BUILTIN", _, imp), import_binds_whole_module(bf), util_module(spec).
promisify_call(ce) :- call_site(_, "METHOD_CALL", "promisify", _, _, _, ce, _, _),
expr_child(_, ce, "RECEIVER", _, r), expr_kind(_, "MODULE_EDGE_CALL", _, r), expr_module_edge(_, imp, r),
import_decl(_, spec, _, _, _, _, "RESOLVED_BUILTIN", _, imp), util_module(spec).
import_binds_whole_module("DEFAULT").
import_binds_whole_module("NAMESPACE").
util_module("util").
util_module("node:util").
// The Object statics whose result frameworks.dl gives a value of its own.
modelled_object_method("assign").
modelled_object_method("create").
Expand Down
17 changes: 17 additions & 0 deletions graph/javascript/engine/resolution/value-flow.dl
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ expr_value(e, k, i) :- expr_kind(_, "NEW", _, e), new_callee_value(e, "func", m)
// (callee-resolution.dl); the VALUE of the expression is f as well.
expr_value(e, "func", m) :- expr_kind(_, "CALL", _, e), call_site(_, "FUNCTION_CALL_BIND", _, _, _, _, e, _, _),
expr_child(_, e, "CALLEE", _, c), expr_value(c, "func", m).
// `promisify(f)` evaluates to a function that runs f (with a callback appended), so a call
// through it reaches f: the wrapper is transparent. What the argument holds is what the
// result holds, so `promisify(fs.readFile)` stays the platform's and an unknown argument
// stays unknown. ambient.dl keeps the platform's own value off it (modelled_platform_call).
expr_value(ce, k, i) :- promisify_call(ce), call_arg(ce, 0, a), expr_value(a, k, i).
// Transparent wrappers: `await x`, `(c ? a : b)`, `a || b`, `a && b`, `a ?? b`,
// `x = v` (an assignment expression evaluates to v), `(a, b)`.
expr_value(e, k, i) :- expr_kind(_, "AWAIT", _, e), expr_child(_, e, _, _, c), expr_value(c, k, i).
Expand Down Expand Up @@ -330,6 +335,18 @@ this_value(m, k, i) :- method_this_binding(_, "LEXICAL", m), method_enclosing(m,
this_value(m, "obj", l) :- expr_child(_, l, "PROPERTY_VALUE", _, v), expr_kind(_, "OBJECT_LITERAL", _, l),
expr_introduces(_, m, v), method_this_binding(_, "DYNAMIC", m), !method_owner_type(m, _).
this_value(m, "obj", l) :- literal_owns_method(l, m), method_this_binding(_, "DYNAMIC", m), !method_owner_type(m, _).
// `f.bind(o)` fixes f's `this` to o for every call through what it returns, wherever that
// value travels: `onEvent.bind({ repo })` then `this.repo.append()` inside onEvent. The
// thisArg is the site's RECEIVER child. Only objects and instances, and only a function no
// class owns (a member's `this` is its instance already): `bind(null)` and a primitive
// bind nothing, and a class member re-bound to its own instance adds nothing new.
// `f.call(o)` / `f.apply(o)` are not read here: they run f once at that site.
this_value(m, k, i) :- call_site(_, "FUNCTION_CALL_BIND", _, _, _, _, ce, _, _),
expr_child(_, ce, "CALLEE", _, c), expr_value(c, "func", m),
method_this_binding(_, "DYNAMIC", m), !method_owner_type(m, _),
expr_child(_, ce, "RECEIVER", _, o), expr_value(o, k, i), bound_this_kind(k).
bound_this_kind("obj").
bound_this_kind("inst").
// `T.prototype.constructor = T` is a BACK-REFERENCE, not an installation: nothing
// calls it with the prototype as `this`, and reading it as one gave a constructor
// the `Object.create(Base.prototype)` object as `this` (#708).
Expand Down
13 changes: 13 additions & 0 deletions graph/javascript/souffle/decls_all.dl
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@
.decl module_variable_from_call(c0:symbol)
.decl variable_reassigned(c0:symbol)
.decl call_passes_function(c0:symbol)
.decl bound_platform_function(c0:symbol)
.decl made_from_function(c0:symbol)
.decl wrapper_holder_call(c0:symbol, c1:symbol)
.decl wrapper_runs(c0:symbol, c1:symbol)
.decl wrapper_call_target(c0:symbol, c1:symbol)
.decl live_export_variable(c0:symbol, c1:symbol)
.decl this_type_open(c0:symbol)

Expand Down Expand Up @@ -300,6 +305,10 @@
.decl well_known_symbol_read(c0:symbol)
.decl modelled_platform_call(c0:symbol)
.decl modelled_object_method(c0:symbol)
.decl promisify_call(c0:symbol)
.decl util_module(c0:symbol)
.decl import_binds_whole_module(c0:symbol)
.decl bound_this_kind(c0:symbol)
.decl free_namespace(c0:symbol, c1:symbol)
.decl ts_export_star(c0:symbol, c1:symbol)
.decl ts_export_star_helper(c0:symbol)
Expand Down Expand Up @@ -451,6 +460,10 @@
.decl options_object_member(c0:symbol, c1:symbol, c2:symbol, c3:symbol)
.decl options_value_kind(c0:symbol)
.decl call_has_client_target(c0:symbol)
.decl lib_rooted(c0:symbol)
.decl import_outcome_is_package(c0:symbol)
.decl access_kind_reads_member(c0:symbol)
.decl lib_wrap_site(c0:symbol)
.decl callback_registered(c0:symbol, c1:symbol)
.decl event_handler(c0:symbol, c1:symbol, c2:symbol, c3:symbol)
.decl event_register_method(c0:symbol)
Expand Down
15 changes: 7 additions & 8 deletions graph/python/engine/resolution/value-flow.dl
Original file line number Diff line number Diff line change
Expand Up @@ -329,14 +329,13 @@ partial_target(site, m) :-
call_arg(site, "0", a),
expr_denotes_method("client", a, m).

// ── a name holding a partial denotes the partial's target ────────────────────
binding_value_method(p, b, m) :-
binding_rebinding(p, "1", _, _, b),
expr_binding(p, b, "STORE", tgt),
assign_pair(p, tgt, val),
call_of_expr(val, site),
partial_target(site, m),
p = "client".
// ── a partial denotes the partial's target ───────────────────────────────────
// The call EXPRESSION, so wherever the partial goes the target goes with it: a name
// (`add_ten = partial(add, 10)`, through (d)'s alias rule), an attribute
// (`self.op = partial(repo.append)`, through field_holds_method), an argument.
expr_denotes_method("client", e, m) :-
call_of_expr(e, site),
partial_target(site, m).

// ── param_arg_class(ParamHash, TypeHash) — a CLASS OBJECT reaching a parameter ─
// Distinct from param_arg_type on purpose: param_arg_type means "an INSTANCE of this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ function viaCtor() { return new Transform({ transform }); }
function viaProject() { return localWalk([1], { filter: keep }); }
function main() { direct(); viaVar(); viaPlatform(); viaCtor(); viaProject(); }
main();
// a function wrapped by a package call and kept in a const, then handed to a package registration: registered
function migrate() { return 1; }
const plugin = walk(async () => migrate());
const settings = walk(42);
function viaWrapped() { walk.register(plugin); walk.register(settings); }
module.exports = { viaWrapped };
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';
const { promisify } = require('util');

class Repo {
append(x) { return x; }
find(k) { return k; }
}
class Other {
append(x) { return x; }
}

// (a) a field or variable assigned from x.m.bind(x) is x.m
class Service {
constructor(r) {
this.repo = r;
this.add = this.repo.append.bind(this.repo);
this.lookup = promisify(r.find.bind(r));
}
go() { return this.add(1); }
get(k) { return this.lookup(k); }
}
function makeService() { return new Service(new Repo()); }
const repo = new Repo();
const bound = repo.append.bind(repo);
function viaVariable() { return bound(2); }
const wrapped = promisify(repo.find.bind(repo));
function viaPromisified() { return wrapped('k'); }
const util = require('util');
const viaNamespace = util.promisify(repo.find.bind(repo));
function viaNamespaceCall() { return viaNamespace('n'); }

// (b) a function bound to an object literal sees its keys through `this`
function onEvent(e) { return this.repo.append(e); }
const handler = onEvent.bind({ repo: new Repo() });
function fire() { return handler(1); }
const handlers = {
created: function onCreated(e) { return this.store.append(e); },
};
const onCreatedBound = handlers.created.bind({ store: new Other() });

// controls: none of these changes
function unbound(e) { return this.repo.append(e); }
function callsUnbound() { return unbound.call({ repo: new Repo() }, 1); }
const snapshot = repo.find.bind(null);
function openBind(fn) { const g = fn.bind(repo); return g(); }
const own = { promisify(f) { return () => f; } };
const notUtil = own.promisify(repo.find);
function viaOwnPromisify() { return notUtil(); }

module.exports = { makeService, viaVariable, viaPromisified, fire, onCreatedBound, callsUnbound, snapshot, openBind, viaNamespaceCall, viaOwnPromisify };
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ 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: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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ main.js:15:39 FUNCTION_CALL viaPlatform -> known_edge main.js:12:1 viaPlatfor
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: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
Expand Up @@ -6,6 +6,8 @@ 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: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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ main.js:15:39 FUNCTION_CALL viaPlatform -> known_edge main.js:12:1 viaPlatfor
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 -> boundary_lib lib:index.js:2:1 walk
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 -> boundary_lib lib:index.js:2:1 walk
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: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
Expand Up @@ -5,4 +5,5 @@ 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:19:33 FUNCTION_CALL migrate EXACT main.js:18:1
# defects: 0
Loading
Loading