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
74 changes: 72 additions & 2 deletions graph/javascript/engine/call-edge-generation/calls.dl
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,83 @@ unresolved_receiver(ce, cn, ck, "other_form") :- call_site("client", ck, cn, _,
// name narrows what it calls, so any function that escapes as a value may be the
// callee, and a question that finds no chain from its caller must answer "unknown",
// never "independent" (#1649). Binding: parameter | loop_variable | local |
// computed | dynamic_code. An imported binding is not one: it names a module
// member, and a module-level binding is left to the by-name leads.
// computed | dynamic_code | module_variable | field | parameter_member | expression.
// An imported binding is not one: it names a module member.
value_callee_open(ce, c) :- call_site("client", ck, _, _, _, _, ce, _, _), !call_kind_is_dynamic(ck),
!call_resolved(ce), !callee_value_known(ce), expr_child(_, ce, "CALLEE", _, c).
unresolved_value_callee(ce, "parameter") :- value_callee_open(ce, c), expr_param(_, _, c).
unresolved_value_callee(ce, "loop_variable") :- value_callee_open(ce, c), expr_binding(_, v, c), loop_head(v, _).
unresolved_value_callee(ce, "local") :- value_callee_open(ce, c), expr_binding(_, v, c), !loop_head(v, _),
var_owner_method("client", _, v), !var_import(_, _, v), !expr_param(_, _, c).
// The two below keep a callee whose value IS known but calls nothing the graph holds
// (`let handler = null`, a constructed `Function`): the value in the graph is not
// the one the call runs.
value_callee_unresolved(ce, c) :- call_site("client", ck, _, _, _, _, ce, _, _), !call_kind_is_dynamic(ck),
!call_resolved(ce), expr_child(_, ce, "CALLEE", _, c).
// `let handler = null; … handler()`: a module-level variable holds whatever a setter
// or an importer stored in it (#1675). One holding a platform function
// (`let f = Math.floor`) is a platform call; `null` is a literal, not a function.
unresolved_value_callee(ce, "module_variable") :- value_callee_unresolved(ce, c), expr_binding(_, v, c),
var_decl("client", _, _, _, _, _, v), !var_owner_method("client", _, v), !var_import(_, _, v), !expr_param(_, _, c),
!callee_platform_function(ce), !module_variable_from_call(v).
callee_platform_function(ce) :- callee_value(ce, "ambient", n), n != "LITERAL".
// `const log = require('debug')('app')`: a variable only ever holding what a call
// returned holds a function the library made, not one a setter stored. One that
// was handed a function (`const Card = memo(Inner)`) may call it, so stays open.
module_variable_from_call(v) :- var_init("client", _, e, v), expr_kind(_, k, _, e), (k = "CALL" ; k = "NEW"),
!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).
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.
// `new Function(s)()` / `Function(s)()` runs code built at run time.
unresolved_value_callee(ce, "dynamic_code") :- value_callee_unresolved(ce, c), callee_builds_code(c).
unresolved_value_callee(ce, "expression") :- value_callee_open(ce, c), expr_kind(_, k, _, c),
k != "IDENTIFIER", k != "THIS", k != "SUPER", !callee_builds_code(c).
callee_builds_code(c) :- expr_kind(_, k, _, c), (k = "NEW" ; k = "CALL"), expr_child(_, c, "CALLEE", _, f),
expr_text(_, "Function", f), expr_binding_resolution(_, "GLOBAL_BUILTIN", f).
// `this.cb()` where `cb` is a field (`this.cb = cb`), not a method: the function
// stored there is whatever the constructor or a setter was handed. The same for
// `self.cb()` (`const self = this`), a static `this.cb()`, `new this.Ctor()`, and a
// field that holds only `null` until a setter runs.
unresolved_value_callee(ce, "field") :- field_call(ce, k, t, n), !call_resolved(ce), !callee_holds_function(ce),
this_field_named(k, t, n).
// `this.cb = cb || function () {}`: the call resolves to the fallback, but the
// parameter it falls back from is a function no caller in the graph supplies.
unresolved_value_callee(ce, "field") :- field_call(ce, k, t, n), field_open_fallback(k, t, n).
field_call(ce, k, t, n) :- call_site("client", ck, n, "SYNTACTIC", _, _, ce, _, _),
(call_kind_is_member_form(ck) ; ck = "CONSTRUCTOR_CALL"), !call_kind_is_dynamic(ck),
expr_child(_, ce, "RECEIVER", _, r), instance_or_class_value(r, k, t).
instance_or_class_value(r, k, t) :- expr_kind(_, rk, _, r), (rk = "THIS" ; rk = "IDENTIFIER"), expr_value(r, k, t),
(k = "inst" ; k = "ctor").
callee_holds_function(ce) :- callee_value(ce, k, n), (k = "func" ; k = "ctor" ; (k = "ambient", n != "LITERAL")).
callee_holds_function(ce) :- new_callee_value(ce, _, _).
this_field_named(k, t, n) :- field_decl("client", n, t, _, _, _, _, _), (k = "inst" ; k = "ctor").
this_field_named(k, t, n) :- type_ancestor(t, a), field_decl("client", n, a, _, _, _, _, _), (k = "inst" ; k = "ctor").
this_field_named(k, t, n) :- field_assignment(k, t, n, _).
field_assignment(k, t, n, val) :- expr_kind(_, "ASSIGNMENT", _, a), expr_child(_, a, "ASSIGNMENT_TARGET", _, tgt),
expr_kind(_, "PROPERTY_ACCESS", _, tgt), expr_name(_, n, tgt), expr_child(_, tgt, "ACCESS_TARGET", _, r),
instance_or_class_value(r, k, t), expr_child(_, a, "ASSIGNMENT_VALUE", _, val).
field_open_fallback(k, t, n) :- field_assignment(k, t, n, val), expr_kind(_, "BINARY", _, val), expr_operator(_, op, val),
(op = "||" ; op = "??"), expr_child(_, val, "OPERAND", _, p), expr_param(_, _, p), !expr_value(p, "func", _).
// A callback in an options object: a member of an untyped parameter that the code
// treats as an optional FUNCTION — `opts.onReady?.()`, `typeof opts.onReady ===
// 'function'`, `opts.onReady && opts.onReady()`. A bare `res.send()` / `s.trim()` on
// an untyped parameter is left out: without a type it reads the same as a library
// or platform method, and counting it made 10 of 12 independent pairs on a real
// Express app unknown.
unresolved_value_callee(ce, "parameter_member") :- call_site("client", ck, n, "SYNTACTIC", _, _, ce, _, _),
call_kind_is_member_form(ck), !call_kind_is_dynamic(ck), !call_resolved(ce), !callee_value_known(ce),
!ambient_site(ce), expr_child(_, ce, "RECEIVER", _, r), expr_param(_, p, r),
(ck = "OPTIONAL_CALL" ; param_member_tested(p, n)).
param_member_tested(p, n) :- expr_kind(_, "UNARY", _, u), expr_operator(_, "typeof", u), expr_child(_, u, _, _, a),
param_member_read(a, p, n).
param_member_tested(p, n) :- expr_kind(_, "BINARY", _, b), expr_operator(_, op, b), (op = "&&" ; op = "||" ; op = "??"),
expr_child(_, b, "OPERAND", _, a), param_member_read(a, p, n).
param_member_tested(p, n) :- expr_child(_, _, "CONDITION", _, a), param_member_read(a, p, n).
param_member_tested(p, n) :- expr_root(_, "CONDITION", a), param_member_read(a, p, n).
param_member_read(a, p, n) :- expr_kind(_, k, _, a), (k = "PROPERTY_ACCESS" ; k = "OPTIONAL_ACCESS"), expr_name(_, n, a),
expr_child(_, a, "ACCESS_TARGET", _, t), expr_param(_, p, t).
unresolved_value_callee(ce, "computed") :- call_site("client", "COMPUTED_CALL", _, _, _, _, ce, _, _), !call_resolved(ce).
unresolved_value_callee(ce, "dynamic_code") :- call_site("client", "DYNAMIC_CODE_CALL", _, _, _, _, ce, _, _), !call_resolved(ce).
14 changes: 14 additions & 0 deletions graph/javascript/souffle/decls_all.dl
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,20 @@
.decl unresolved_covered(c0:symbol)
.decl value_callee_open(c0:symbol, c1:symbol)
.decl unresolved_value_callee(c0:symbol, c1:symbol)
.decl callee_builds_code(c0:symbol)
.decl value_callee_unresolved(c0:symbol, c1:symbol)
.decl callee_platform_function(c0:symbol)
.decl param_member_tested(c0:symbol, c1:symbol)
.decl param_member_read(c0:symbol, c1:symbol, c2:symbol)
.decl this_field_named(c0:symbol, c1:symbol, c2:symbol)
.decl field_call(c0:symbol, c1:symbol, c2:symbol, c3:symbol)
.decl instance_or_class_value(c0:symbol, c1:symbol, c2:symbol)
.decl callee_holds_function(c0:symbol)
.decl field_assignment(c0:symbol, c1:symbol, c2:symbol, c3:symbol)
.decl field_open_fallback(c0:symbol, c1:symbol, c2:symbol)
.decl module_variable_from_call(c0:symbol)
.decl variable_reassigned(c0:symbol)
.decl call_passes_function(c0:symbol)

// ── containment/ownership.dl ──
.decl call_caller_method(c0:symbol, c1:symbol)
Expand Down
28 changes: 28 additions & 0 deletions graph/test/javascript/cases/65-stored-value-callee/src/stored.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';
const path = require('path');
function alpha() { return 'a'; }
let handler = null;
function setHandler(h) { handler = h; }
function fire() { return handler(); }
class Emitter {
constructor(cb) { this.cb = cb; }
emit() { return this.cb(); }
own() { return 1; }
callsOwn() { return this.own(); }
}
function optsTypeof(opts) { if (typeof opts.onReady === 'function') return opts.onReady(); return null; }
function optsAnd(opts) { return opts.onDone && opts.onDone(); }
function optsIf(opts) { if (opts.onEnd) { opts.onEnd(); } }
function optsOptional(opts) { return opts.onStart?.(); }
function condInvoke(a, b, c) { return (c ? a : b)(); }
function seqInvoke(cb) { return (0, cb)(); }
function newFn(s) { return new Function(s)(); }
const helper = function () { return alpha(); };
function viaHelper() { return helper(); }
function joins() { return path.join('a', 'b'); }
function literalTrim() { return 'abc'.trim(); }
function pure() { return 1 + 2; }
function optsPlain(opts) { return opts.onReady(); }
function respond(req, res) { return res.status(200).send(req.query.q); }
module.exports = { alpha, setHandler, fire, Emitter, optsTypeof, optsAnd, optsIf, optsOptional, condInvoke, seqInvoke, newFn,
viaHelper, joins, literalTrim, pure, optsPlain, respond };
47 changes: 47 additions & 0 deletions graph/test/javascript/cases/66-stored-callee-shapes/src/shapes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict';
const debug = require('debug');
function alpha() { return 'a'; }
class FieldNull {
cb = null;
set(f) { this.cb = f; }
run() { return this.cb(); }
}
class CtorNull {
constructor() { this.cb = null; }
set(f) { this.cb = f; }
run() { return this.cb(); }
}
class Fallback {
constructor(cb) { this.cb = cb || function () {}; }
run() { return this.cb(); }
}
class Static {
static set(f) { this.cb = f; }
static run() { return this.cb(); }
}
class StaticField {
static cb = null;
static run() { return this.cb(); }
}
class Alias {
constructor(cb) { this.cb = cb; }
run() { const self = this; return self.cb(); }
}
class Maker {
constructor(C) { this.Ctor = C; }
make() { return new this.Ctor(); }
}
const log = debug('app');
function logs() { log('x'); return 1; }
const tag = require('util').format;
function tags() { return tag('%s', 'x'); }
const wrapped = debug(alpha);
function callsWrapped() { return wrapped(); }
// controls: none of these is a value callee
const EventEmitter = require('events');
class Bus extends EventEmitter { go() { return this.emit('x'); } }
class Own { own() { return 1; } run() { return this.own(); } }
class Known { constructor() { this.fn = alpha; } run() { return this.fn(); } }
class Fixed { constructor(cb) { this.cb = alpha || cb; } }
function useOwn() { const o = new Own(); return o.run(); }
module.exports = { alpha, FieldNull, CtorNull, Fallback, Static, StaticField, Alias, Maker, logs, tags, callsWrapped, Bus, Known, Fixed, useOwn };
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
package_entry probe4 . [] DEFAULT_INDEX index.js MISSING_FILE -> -
unresolved a.js:8:11 METHOD_CALL typed member_absent
value_callee a.js:8:11 this.typed field
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
package_entry 49-chained-member-assignment . [] DEFAULT_INDEX index.js MISSING_FILE -> -
unresolved core.js:14:19 METHOD_CALL create no_target
unresolved main.js:14:3 METHOD_CALL tag2 member_absent
value_callee main.js:14:3 Model.tag2 field
1 change: 1 addition & 0 deletions graph/test/javascript/expected/63-jsx-wrapper-lazy.diag
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ unresolved app.jsx:9:14 FUNCTION_CALL memo callee_untyped
unresolved defmemo.jsx:4:16 FUNCTION_CALL memo callee_untyped
unresolved parts.jsx:6:24 FUNCTION_CALL memo callee_untyped
unresolved parts.jsx:6:29 FUNCTION_CALL forwardRef callee_untyped
value_callee app.jsx:42:3 Card module_variable
26 changes: 26 additions & 0 deletions graph/test/javascript/expected/65-stored-value-callee.diag
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import_cause stored.js:2:14 path builtin
package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> -
unresolved stored.js:13:76 METHOD_CALL onReady receiver_untyped
unresolved stored.js:14:48 METHOD_CALL onDone receiver_untyped
unresolved stored.js:15:43 METHOD_CALL onEnd receiver_untyped
unresolved stored.js:16:38 OPTIONAL_CALL onStart receiver_untyped
unresolved stored.js:17:39 FUNCTION_CALL callee_untyped
unresolved stored.js:18:33 FUNCTION_CALL callee_untyped
unresolved stored.js:19:28 CONSTRUCTOR_CALL Function no_target
unresolved stored.js:19:28 FUNCTION_CALL no_target
unresolved stored.js:22:27 METHOD_CALL join no_target
unresolved stored.js:23:33 METHOD_CALL trim no_target
unresolved stored.js:25:35 METHOD_CALL onReady receiver_untyped
unresolved stored.js:26:37 METHOD_CALL send receiver_untyped
unresolved stored.js:26:37 METHOD_CALL status receiver_untyped
unresolved stored.js:6:26 FUNCTION_CALL handler no_target
unresolved stored.js:9:19 METHOD_CALL cb member_absent
value_callee stored.js:13:76 opts.onReady parameter_member
value_callee stored.js:14:48 opts.onDone parameter_member
value_callee stored.js:15:43 opts.onEnd parameter_member
value_callee stored.js:16:38 opts.onStart parameter_member
value_callee stored.js:17:39 (c ? a : b) expression
value_callee stored.js:18:33 (0, cb) expression
value_callee stored.js:19:28 new Function(s) dynamic_code
value_callee stored.js:6:26 handler module_variable
value_callee stored.js:9:19 this.cb field
18 changes: 18 additions & 0 deletions graph/test/javascript/expected/65-stored-value-callee.edges
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
stored.js:11:23 METHOD_CALL this.own -> known_edge stored.js:10:3 own
stored.js:13:76 METHOD_CALL opts.onReady -> ambiguous_unknown -
stored.js:14:48 METHOD_CALL opts.onDone -> ambiguous_unknown -
stored.js:15:43 METHOD_CALL opts.onEnd -> ambiguous_unknown -
stored.js:16:38 OPTIONAL_CALL opts.onStart -> ambiguous_unknown -
stored.js:17:39 FUNCTION_CALL (c ? a : b) -> ambiguous_unknown -
stored.js:18:33 FUNCTION_CALL (0, cb) -> ambiguous_unknown -
stored.js:19:28 CONSTRUCTOR_CALL Function -> ambient_terminal -
stored.js:19:28 FUNCTION_CALL new Function(s) -> ambient_terminal -
stored.js:20:37 FUNCTION_CALL alpha -> known_edge stored.js:3:1 alpha
stored.js:21:31 FUNCTION_CALL helper -> known_edge stored.js:20:16 <function-expression>
stored.js:22:27 METHOD_CALL path.join -> ambient_terminal -
stored.js:23:33 METHOD_CALL 'abc'.trim -> ambient_terminal -
stored.js:25:35 METHOD_CALL opts.onReady -> ambiguous_unknown -
stored.js:26:37 METHOD_CALL res.status -> ambiguous_unknown -
stored.js:26:37 METHOD_CALL res.status(200).send -> ambiguous_unknown -
stored.js:6:26 FUNCTION_CALL handler -> ambient_terminal -
stored.js:9:19 METHOD_CALL this.cb -> ambiguous_unknown -
6 changes: 6 additions & 0 deletions graph/test/javascript/expected/65-stored-value-callee.oracle
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
stored.js:11:23 METHOD_CALL own EXACT stored.js:10:3
stored.js:19:28 CONSTRUCTOR_CALL Function LIB_AMBIENT_OK
stored.js:20:37 FUNCTION_CALL alpha EXACT stored.js:3:1
stored.js:21:31 FUNCTION_CALL helper EXACT stored.js:20:16
stored.js:23:33 METHOD_CALL trim LIB_AMBIENT_OK
# defects: 0
24 changes: 24 additions & 0 deletions graph/test/javascript/expected/66-stored-callee-shapes.diag
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import_cause shapes.js:2:15 debug not_staged
import_cause shapes.js:36:13 util builtin
import_cause shapes.js:41:22 events builtin
package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> -
unresolved shapes.js:12:18 METHOD_CALL cb no_target
unresolved shapes.js:20:25 METHOD_CALL cb member_absent
unresolved shapes.js:24:25 METHOD_CALL cb no_target
unresolved shapes.js:28:37 METHOD_CALL cb member_absent
unresolved shapes.js:32:19 CONSTRUCTOR_CALL Ctor member_absent
unresolved shapes.js:34:13 FUNCTION_CALL debug callee_untyped
unresolved shapes.js:35:19 FUNCTION_CALL log callee_untyped
unresolved shapes.js:37:26 FUNCTION_CALL tag no_target
unresolved shapes.js:38:17 FUNCTION_CALL debug callee_untyped
unresolved shapes.js:39:34 FUNCTION_CALL wrapped callee_untyped
unresolved shapes.js:42:48 METHOD_CALL emit no_target
unresolved shapes.js:7:18 METHOD_CALL cb no_target
value_callee shapes.js:12:18 this.cb field
value_callee shapes.js:16:18 this.cb field
value_callee shapes.js:20:25 this.cb field
value_callee shapes.js:24:25 this.cb field
value_callee shapes.js:28:37 self.cb field
value_callee shapes.js:32:19 this.Ctor field
value_callee shapes.js:39:34 wrapped module_variable
value_callee shapes.js:7:18 this.cb field
18 changes: 18 additions & 0 deletions graph/test/javascript/expected/66-stored-callee-shapes.edges
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
shapes.js:12:18 METHOD_CALL this.cb -> ambient_terminal -
shapes.js:16:18 METHOD_CALL this.cb -> known_edge shapes.js:15:37 <function-expression>
shapes.js:20:25 METHOD_CALL this.cb -> ambiguous_unknown -
shapes.js:24:25 METHOD_CALL this.cb -> ambient_terminal -
shapes.js:28:37 METHOD_CALL self.cb -> ambiguous_unknown -
shapes.js:32:19 CONSTRUCTOR_CALL this.Ctor -> ambiguous_unknown -
shapes.js:34:13 FUNCTION_CALL debug -> ambiguous_unknown -
shapes.js:35:19 FUNCTION_CALL log -> ambiguous_unknown -
shapes.js:37:26 FUNCTION_CALL tag -> ambient_terminal -
shapes.js:38:17 FUNCTION_CALL debug -> ambiguous_unknown -
shapes.js:38:17 FUNCTION_CALL debug -> callback_registered shapes.js:3:1 alpha
shapes.js:39:34 FUNCTION_CALL wrapped -> ambiguous_unknown -
shapes.js:42:48 METHOD_CALL this.emit -> ambient_terminal -
shapes.js:43:48 METHOD_CALL this.own -> known_edge shapes.js:43:13 own
shapes.js:44:65 METHOD_CALL this.fn -> known_edge shapes.js:3:1 alpha
shapes.js:46:31 CONSTRUCTOR_CALL Own -> implicit_constructor -
shapes.js:46:49 METHOD_CALL o.run -> known_edge shapes.js:43:33 run
shapes.js:7:18 METHOD_CALL this.cb -> ambient_terminal -
5 changes: 5 additions & 0 deletions graph/test/javascript/expected/66-stored-callee-shapes.oracle
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
shapes.js:43:48 METHOD_CALL own EXACT shapes.js:43:13
shapes.js:44:65 METHOD_CALL fn EXACT shapes.js:3:1
shapes.js:46:31 CONSTRUCTOR_CALL Own SYNTHESIZED_OK
shapes.js:46:49 METHOD_CALL run EXACT shapes.js:43:33
# defects: 0
4 changes: 3 additions & 1 deletion plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path
Original file line number Diff line number Diff line change
Expand Up @@ -1491,7 +1491,9 @@ def path(g, a, b, show_all=False, limit=10, every=False, max_paths=20):
f"{grep_for(g, g.sym[unmod[0][0]]['name'])}" if unmod
else ": the two are independent in this graph"))
WHY = {'parameter': 'a parameter', 'loop_variable': 'a loop variable', 'local': 'a local', 'computed': 'a computed member',
'dynamic_code': 'code built at run time'}
'dynamic_code': 'code built at run time', 'module_variable': 'a module-level variable',
'field': 'a function stored in a field', 'parameter_member': 'a member of a parameter',
'expression': 'computed by an expression'}
for lx, rows in opaque:
print(f" {lx} reaches {len(rows)} call(s) through a value:")
for r in rows[:4]:
Expand Down
Loading
Loading