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
17 changes: 17 additions & 0 deletions graph/javascript/engine/call-edge-generation/calls.dl
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,20 @@ unresolved_covered(ce) :- call_site("client", ck, _, rp, _, _, ce, _, _), rp !=
unresolved_covered(ce) :- call_site("client", ck, _, _, _, _, ce, _, _), call_kind_is_dynamic(ck).
unresolved_receiver(ce, cn, ck, "other_form") :- call_site("client", ck, cn, _, _, _, ce, _, _),
!call_resolved(ce), !unresolved_covered(ce).

// ── unresolved_value_callee(CallExpr, Binding) ──────────────────────────────
// An unresolved call whose callee is a VALUE, not a name: `cb()` on a parameter,
// `f()` on a loop variable or a local, `table[k]()`, `eval(s)`. Nothing about its
// 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.
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).
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).
2 changes: 2 additions & 0 deletions graph/javascript/souffle/decls_all.dl
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
.decl call_resolved(c0:symbol)
.decl unresolved_receiver(c0:symbol, c1:symbol, c2:symbol, c3:symbol)
.decl unresolved_covered(c0:symbol)
.decl value_callee_open(c0:symbol, c1:symbol)
.decl unresolved_value_callee(c0:symbol, c1:symbol)

// ── containment/ownership.dl ──
.decl call_caller_method(c0:symbol, c1:symbol)
Expand Down
1 change: 1 addition & 0 deletions graph/javascript/souffle/export_manifest.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ client_calls_lib client-to-lib-calls.csv
client_calls_lib_detail client-to-lib-navigation.csv
unresolved_receiver unresolved-receiver.csv
method_has_unknown_call method-has-unknown-call.csv
unresolved_value_callee unresolved-value-callee.csv
entry_point entry-point.csv
entry_reachable entry-reachable.csv
method_call_edge method-call-edge.csv
Expand Down
13 changes: 13 additions & 0 deletions graph/test/javascript/cases/64-value-callee/src/handlers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use strict';
const path = require('path');
function alpha() { return 'a'; }
function runAll(handlers) { handlers.forEach(h => h()); }
function dispatch(table, k) { return table[k](); }
function invoke(cb) { return cb(); }
function runList(ctx) { for (const f of ctx.list) f(); }
function viaLocal(o) { const fn = o.pick; return fn(); }
function viaCall(cb) { return cb.call(null); }
function pure() { return 1 + 2; }
function joins() { return path.join('a', 'b'); }
function direct() { return alpha(); }
module.exports = { alpha, runAll, dispatch, invoke, runList, viaLocal, viaCall, pure, joins, direct };
1 change: 1 addition & 0 deletions graph/test/javascript/expected/11-class-members.diag
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ unresolved main.js:26:17 FUNCTION_CALL_BIND Holder no_target
unresolved main.js:26:55 CONSTRUCTOR_CALL Bound callee_untyped
unresolved main.js:26:55 METHOD_CALL go receiver_untyped
unresolved main.js:7:24 FUNCTION_CALL String no_target
value_callee main.js:26:55 Bound local
6 changes: 6 additions & 0 deletions graph/test/javascript/expected/25-stream-hooks.diag
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,9 @@ unresolved streams.js:7:67 FUNCTION_CALL cb callee_untyped
unresolved streams.js:7:99 FUNCTION_CALL cb callee_untyped
unresolved streams.js:8:55 FUNCTION_CALL cb callee_untyped
unresolved streams.js:8:73 METHOD_CALL push no_target
value_callee streams.js:5:120 cb parameter
value_callee streams.js:5:63 cb parameter
value_callee streams.js:5:92 cb parameter
value_callee streams.js:7:67 cb parameter
value_callee streams.js:7:99 cb parameter
value_callee streams.js:8:55 cb parameter
1 change: 1 addition & 0 deletions graph/test/javascript/expected/27-parse-gaps.diag
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ unresolved jsdoc.js:5:33 METHOD_CALL render receiver_type_unreadable
unresolved jsdoc.js:5:33 METHOD_CALL render receiver_untyped
unresolved jsdoc.js:8:30 METHOD_CALL render receiver_untyped
unresolved partial.js:5:42 FUNCTION_CALL unknownThing callee_untyped
value_callee dynamic.js:3:29 eval dynamic_code
2 changes: 2 additions & 0 deletions graph/test/javascript/expected/32-symbol-keyed-members.diag
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ unresolved queue.js:4:15 FUNCTION_CALL Symbol no_target
unresolved queue.js:5:17 METHOD_CALL for no_target
unresolved symbols.js:2:28 FUNCTION_CALL Symbol no_target
unresolved symbols.js:2:53 FUNCTION_CALL Symbol no_target
value_callee queue.js:33:54 q[Symbol] computed
value_callee queue.js:33:76 q[anySymbol] computed
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ unresolved main.js:12:86 METHOD_CALL request no_target
unresolved main.js:13:29 CONSTRUCTOR_CALL Transform 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
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ unresolved main.js:12:86 METHOD_CALL request no_target
unresolved main.js:13:29 CONSTRUCTOR_CALL Transform 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
1 change: 1 addition & 0 deletions graph/test/javascript/expected/40-object-keys-strings.diag
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ unresolved main.js:26:42 METHOD_CALL toLowerCase no_target
unresolved main.js:34:26 METHOD_CALL padStart no_target
unresolved main.js:35:34 COMPUTED_CALL dynamic
unresolved main.js:9:21 METHOD_CALL keys no_target
value_callee main.js:35:34 handlers[name] computed
2 changes: 2 additions & 0 deletions graph/test/javascript/expected/58-well-known-symbol.diag
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ package_entry 58-well-known-symbol . [] DEFAULT_INDEX index.js MISSING_F
unresolved main.js:15:14 FUNCTION_CALL Symbol no_target
unresolved main.js:21:37 COMPUTED_CALL dynamic
unresolved main.js:9:40 COMPUTED_CALL dynamic
value_callee main.js:21:37 b[Symbol[x]] computed
value_callee main.js:9:40 this.items[Symbol.iterator] computed
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED
unresolved main.js:10:23 FUNCTION_CALL_CALL hasOwnProperty no_target
unresolved main.js:30:10 COMPUTED_CALL dynamic
unresolved main.js:69:15 FUNCTION_CALL Number no_target
value_callee main.js:30:10 table[prop] computed
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> -
unresolved memoize.js:7:14 FUNCTION_CALL fn callee_untyped
unresolved memoize.js:7:14 FUNCTION_CALL fn param_fan_capped
value_callee memoize.js:7:14 fn parameter
16 changes: 16 additions & 0 deletions graph/test/javascript/expected/64-value-callee.diag
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import_cause handlers.js:2:14 path builtin
package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> -
unresolved handlers.js:11:27 METHOD_CALL join no_target
unresolved handlers.js:4:29 METHOD_CALL forEach receiver_untyped
unresolved handlers.js:4:51 FUNCTION_CALL h callee_untyped
unresolved handlers.js:5:38 COMPUTED_CALL dynamic
unresolved handlers.js:6:30 FUNCTION_CALL cb callee_untyped
unresolved handlers.js:7:51 FUNCTION_CALL f callee_untyped
unresolved handlers.js:8:50 FUNCTION_CALL fn callee_untyped
unresolved handlers.js:9:31 FUNCTION_CALL_CALL cb callee_untyped
value_callee handlers.js:4:51 h parameter
value_callee handlers.js:5:38 table[k] computed
value_callee handlers.js:6:30 cb parameter
value_callee handlers.js:7:51 f loop_variable
value_callee handlers.js:8:50 fn local
value_callee handlers.js:9:31 cb.call parameter
10 changes: 10 additions & 0 deletions graph/test/javascript/expected/64-value-callee.edges
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
handlers.js:11:27 METHOD_CALL path.join -> ambient_terminal -
handlers.js:12:28 FUNCTION_CALL alpha -> known_edge handlers.js:3:1 alpha
handlers.js:4:29 METHOD_CALL handlers.forEach -> ambiguous_unknown -
handlers.js:4:29 METHOD_CALL handlers.forEach -> callback_registered handlers.js:4:46 <arrow>
handlers.js:4:51 FUNCTION_CALL h -> ambiguous_unknown -
handlers.js:5:38 COMPUTED_CALL table[k] -> dynamic_terminal -
handlers.js:6:30 FUNCTION_CALL cb -> ambiguous_unknown -
handlers.js:7:51 FUNCTION_CALL f -> ambiguous_unknown -
handlers.js:8:50 FUNCTION_CALL fn -> ambiguous_unknown -
handlers.js:9:31 FUNCTION_CALL_CALL cb.call -> ambient_terminal -
2 changes: 2 additions & 0 deletions graph/test/javascript/expected/64-value-callee.oracle
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
handlers.js:12:28 FUNCTION_CALL alpha EXACT handlers.js:3:1
# defects: 0
4 changes: 4 additions & 0 deletions graph/test/javascript/tools/normalize_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ def raw(path, width):
for f in raw(os.path.join(out, 'unresolved-receiver.csv'), 4):
pos, kind, _ = site.get(f[0], ('?', f[2], '?'))
lines.add('unresolved %s %s %s %s' % (pos, kind, f[1], f[3]))
# unresolved_value_callee(CallExpr, Binding): the callee is a value, so the site may call anything (#1649)
for f in raw(os.path.join(out, 'unresolved-value-callee.csv'), 2):
pos, _, text = site.get(f[0], ('?', '?', '?'))
lines.add('value_callee %s %s %s' % (pos, text, f[1]))
# package_entry(Prov, Package, Subpath, Condition, Source, Target, Outcome, ModuleHash)
for f in raw(os.path.join(out, 'package-entry.csv'), 8):
lines.add('package_entry %s %s [%s] %s %s %s -> %s' % (
Expand Down
5 changes: 5 additions & 0 deletions plugins/axiomcode/skills/axiomcode/scripts/ax_pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ def next_path(text):
return ("next: no chain of calls; the library calls named above are where one could continue: read the body "
"that makes them — one that publishes, schedules or registers what the entered method handles connects "
"the two at run time")
if 'connection is UNKNOWN, not absent' in text:
m = re.search(r'^\s+`[^`]*` in \S+ at (\S+:\d+)', text, re.M)
return ("next: not shown to be independent — " + (f"read {m.group(1)} and " if m else "read the calls through a value named above and ")
+ "find what its callee is given (the arguments its callers pass, what the loop runs over); a function handed "
"there is the connection")
if 'the hop above is the only connection' in text:
return "next: the cross-process hop above is the only connection; `impact <its target>` lists it as a [remote] dependent"
# an endpoint a framework enters in a way the graph does not model: the check is the grep the verdict printed
Expand Down
30 changes: 29 additions & 1 deletion plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,24 @@ def path(g, a, b, show_all=False, limit=10, every=False, max_paths=20):
print(f" {ly} is entered from outside the graph in the same process, as such a call can do: "
+ ', '.join(f"{g.disp(m)} ({why}) {g.loc(m)}" for m, why in entered[:3])
+ (f" … +{len(entered) - 3}" if len(entered) > 3 else ''))
# A CALL WHOSE CALLEE IS A VALUE CAN LAND ANYWHERE (#1649). `cb()` on a parameter, `f()` on a loop variable,
# `table[k]()`: nothing about the name narrows the target, so the by-name search above finds no lead and
# "independent" was printed for a start that hands control to whatever it was given. The engine marks such a
# site (`unresolved_value_callee`); one in either side's closure makes the connection unknown, not absent.
opaque = []
if not (remote_found or lib_side) and g.has('ext_unresolved_value_callee'):
for xs, lx in ((A_, la), (B_, lb)):
seen = {i for i in xs if i in g.sym and g.sym[i]['kind'] not in ('library', 'written')}; fr = list(seen)
while fr:
fr = [y for y in dict.fromkeys(y for x in fr for y in adj.get(x, ())) if y not in seen and y in g.sym
and g.sym[y]['kind'] not in ('library', 'written')]
seen.update(fr)
ids = sorted(seen); rows = []
for i in range(0, len(ids), 900):
part = ids[i:i + 900]
rows += g.q("SELECT s.caller_id c, s.callee_name n, s.file_path f, s.start_line ln, v.c1 why FROM ext_unresolved_value_callee v"
" JOIN call_sites s ON s.id = v.c0 WHERE s.caller_id IN (%s)" % ','.join('?' * len(part)), *part)
if rows: opaque.append((lx, sorted(rows, key=lambda r: (g.site_file(r['f']), r['ln'] or 0))))
# …but a framework may still connect them, and the graph holds the evidence for it: the target is registered
# under a key, and the source writes that key. That is not a chain of calls, so it is reported and not walked.
# Computed BEFORE the verdict line, which used to say "independent" one line above the note that joined them.
Expand All @@ -1363,7 +1381,7 @@ def path(g, a, b, show_all=False, limit=10, every=False, max_paths=20):
# a claim about the model and not the code. An endpoint the engine already made an entry point is modelled, and
# the library-call rule above decides it.
unmod = []
if not (remote_found or keyed or lib_side):
if not (remote_found or keyed or lib_side or opaque):
called = {y for ys in adj.values() for y in ys}
eps = set()
ends = [i for i in list(B_) + list(A_) if i in g.sym and g.sym[i]['kind'] not in ('library', 'written') and i not in called]
Expand All @@ -1378,10 +1396,20 @@ def path(g, a, b, show_all=False, limit=10, every=False, max_paths=20):
" does: NOT independent" if keyed
else " — but a library called above can reach the other at run time (an event, a callback, a hook) with no"
" call edge to record it: NOT shown to be independent" if lib_side
else f" — but {' and '.join(lx for lx, _ in opaque)} make{'s' if len(opaque) == 1 else ''} a call whose callee is a"
f" value, not a name (below): any function passed, stored or exported may be what it calls, so the"
f" connection is UNKNOWN, not absent" if opaque
else f" — but NOT CHECKED: {'; '.join(s_ for _i, s_ in unmod[:3])} — a framework calls it and the graph does not"
f" model how, so the other may reach it through that framework. Before treating them as independent run: "
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'}
for lx, rows in opaque:
print(f" {lx} reaches {len(rows)} call(s) through a value:")
for r in rows[:4]:
print(f" `{r['n'] or '[…]'}()` in {g.disp(r['c'])} at {g.site_file(r['f'])}:{r['ln']} — the callee is {WHY.get(r['why'], r['why'])}")
if len(rows) > 4: print(f" … +{len(rows) - 4} more")
for line in fw: print(line)
return 1

Expand Down
170 changes: 170 additions & 0 deletions tests/cases/javascript/value-callee-is-unknown/case.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
{
"lang": "javascript",
"src": "src",
"checks": [
{
"why": "invoke calls a parameter: the callee can be any function handed to it, so path says unknown and names the site (#1649)",
"run": [
"path",
"invoke",
"alpha"
],
"expect_error": true,
"want": [
"connection is UNKNOWN, not absent",
"`cb()` in invoke at src/handlers.js:6 — the callee is a parameter",
"not shown to be independent"
],
"avoid": [
"the two are independent in this graph"
]
},
{
"why": "dispatch calls a computed member (table[k]()): the callee can be any function handed to it, so path says unknown and names the site (#1649)",
"run": [
"path",
"dispatch",
"alpha"
],
"expect_error": true,
"want": [
"connection is UNKNOWN, not absent",
"at src/handlers.js:5 — the callee is a computed member",
"not shown to be independent"
],
"avoid": [
"the two are independent in this graph"
]
},
{
"why": "runList calls a for-of loop variable: the callee can be any function handed to it, so path says unknown and names the site (#1649)",
"run": [
"path",
"runList",
"alpha"
],
"expect_error": true,
"want": [
"connection is UNKNOWN, not absent",
"`f()` in runList at src/handlers.js:7 — the callee is a loop variable",
"not shown to be independent"
],
"avoid": [
"the two are independent in this graph"
]
},
{
"why": "runAll calls an arrow's parameter inside forEach: the callee can be any function handed to it, so path says unknown and names the site (#1649)",
"run": [
"path",
"runAll",
"alpha"
],
"expect_error": true,
"want": [
"connection is UNKNOWN, not absent",
"`h()` in <arrow> at src/handlers.js:4 — the callee is a parameter",
"not shown to be independent"
],
"avoid": [
"the two are independent in this graph"
]
},
{
"why": "viaLocal calls a local with no value: the callee can be any function handed to it, so path says unknown and names the site (#1649)",
"run": [
"path",
"viaLocal",
"alpha"
],
"expect_error": true,
"want": [
"connection is UNKNOWN, not absent",
"`fn()` in viaLocal at src/handlers.js:8 — the callee is a local",
"not shown to be independent"
],
"avoid": [
"the two are independent in this graph"
]
},
{
"why": "viaCall calls a parameter through .call: the callee can be any function handed to it, so path says unknown and names the site (#1649)",
"run": [
"path",
"viaCall",
"alpha"
],
"expect_error": true,
"want": [
"connection is UNKNOWN, not absent",
"at src/handlers.js:9 — the callee is a parameter",
"not shown to be independent"
],
"avoid": [
"the two are independent in this graph"
]
},
{
"why": "the reverse question is the same connection: the value call is on the other side (#1649)",
"run": [
"path",
"alpha",
"invoke"
],
"expect_error": true,
"want": [
"connection is UNKNOWN, not absent",
"`cb()` in invoke"
],
"avoid": [
"the two are independent in this graph"
]
},
{
"why": "control: a function that calls nothing stays independent",
"run": [
"path",
"pure",
"alpha"
],
"expect_error": true,
"want": [
"the two are independent in this graph"
],
"avoid": [
"connection is UNKNOWN, not absent",
"through a value"
]
},
{
"why": "control: a call through a required module binding names a module member, not a value",
"run": [
"path",
"joins",
"alpha"
],
"expect_error": true,
"want": [
"the two are independent in this graph"
],
"avoid": [
"connection is UNKNOWN, not absent",
"through a value"
]
},
{
"why": "control: a resolved call is still a chain",
"run": [
"path",
"direct",
"alpha"
],
"want": [
"direct → alpha: 1 of 1 target(s) reached"
],
"avoid": [
"connection is UNKNOWN, not absent"
]
}
]
}
Loading
Loading