diff --git a/graph/javascript/engine/call-edge-generation/calls.dl b/graph/javascript/engine/call-edge-generation/calls.dl index ec0a7b5ea..55697c0d0 100644 --- a/graph/javascript/engine/call-edge-generation/calls.dl +++ b/graph/javascript/engine/call-edge-generation/calls.dl @@ -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). diff --git a/graph/javascript/souffle/decls_all.dl b/graph/javascript/souffle/decls_all.dl index 184041a18..32ddecf4c 100644 --- a/graph/javascript/souffle/decls_all.dl +++ b/graph/javascript/souffle/decls_all.dl @@ -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) diff --git a/graph/javascript/souffle/export_manifest.tsv b/graph/javascript/souffle/export_manifest.tsv index b90de230f..1e8b5aac9 100644 --- a/graph/javascript/souffle/export_manifest.tsv +++ b/graph/javascript/souffle/export_manifest.tsv @@ -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 diff --git a/graph/test/javascript/cases/64-value-callee/src/handlers.js b/graph/test/javascript/cases/64-value-callee/src/handlers.js new file mode 100644 index 000000000..9fa522427 --- /dev/null +++ b/graph/test/javascript/cases/64-value-callee/src/handlers.js @@ -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 }; diff --git a/graph/test/javascript/expected/11-class-members.diag b/graph/test/javascript/expected/11-class-members.diag index 3938fcda9..e80c1f58e 100644 --- a/graph/test/javascript/expected/11-class-members.diag +++ b/graph/test/javascript/expected/11-class-members.diag @@ -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 diff --git a/graph/test/javascript/expected/25-stream-hooks.diag b/graph/test/javascript/expected/25-stream-hooks.diag index 5cf6dc6a8..ca85b8d0c 100644 --- a/graph/test/javascript/expected/25-stream-hooks.diag +++ b/graph/test/javascript/expected/25-stream-hooks.diag @@ -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 diff --git a/graph/test/javascript/expected/27-parse-gaps.diag b/graph/test/javascript/expected/27-parse-gaps.diag index 39eff716c..18aec1be1 100644 --- a/graph/test/javascript/expected/27-parse-gaps.diag +++ b/graph/test/javascript/expected/27-parse-gaps.diag @@ -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 diff --git a/graph/test/javascript/expected/32-symbol-keyed-members.diag b/graph/test/javascript/expected/32-symbol-keyed-members.diag index 604fb0bca..af10249bc 100644 --- a/graph/test/javascript/expected/32-symbol-keyed-members.diag +++ b/graph/test/javascript/expected/32-symbol-keyed-members.diag @@ -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 diff --git a/graph/test/javascript/expected/34-options-object-callbacks.diag b/graph/test/javascript/expected/34-options-object-callbacks.diag index 87834728c..f261bb398 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.diag +++ b/graph/test/javascript/expected/34-options-object-callbacks.diag @@ -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 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 785706d9d..99d2f7ba0 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.lib.diag +++ b/graph/test/javascript/expected/34-options-object-callbacks.lib.diag @@ -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 diff --git a/graph/test/javascript/expected/40-object-keys-strings.diag b/graph/test/javascript/expected/40-object-keys-strings.diag index 114e99542..e3e1b7004 100644 --- a/graph/test/javascript/expected/40-object-keys-strings.diag +++ b/graph/test/javascript/expected/40-object-keys-strings.diag @@ -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 diff --git a/graph/test/javascript/expected/58-well-known-symbol.diag b/graph/test/javascript/expected/58-well-known-symbol.diag index 63a5cbad6..3588515bf 100644 --- a/graph/test/javascript/expected/58-well-known-symbol.diag +++ b/graph/test/javascript/expected/58-well-known-symbol.diag @@ -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 diff --git a/graph/test/javascript/expected/61-dispatch-table-computed-key.diag b/graph/test/javascript/expected/61-dispatch-table-computed-key.diag index b42e3ce0d..cabfa5949 100644 --- a/graph/test/javascript/expected/61-dispatch-table-computed-key.diag +++ b/graph/test/javascript/expected/61-dispatch-table-computed-key.diag @@ -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 diff --git a/graph/test/javascript/expected/64-memoize-wrapper-result.diag b/graph/test/javascript/expected/64-memoize-wrapper-result.diag index 73bd4eb57..f9fd9b4b2 100644 --- a/graph/test/javascript/expected/64-memoize-wrapper-result.diag +++ b/graph/test/javascript/expected/64-memoize-wrapper-result.diag @@ -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 diff --git a/graph/test/javascript/expected/64-value-callee.diag b/graph/test/javascript/expected/64-value-callee.diag new file mode 100644 index 000000000..d392b7a25 --- /dev/null +++ b/graph/test/javascript/expected/64-value-callee.diag @@ -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 diff --git a/graph/test/javascript/expected/64-value-callee.edges b/graph/test/javascript/expected/64-value-callee.edges new file mode 100644 index 000000000..2044afa30 --- /dev/null +++ b/graph/test/javascript/expected/64-value-callee.edges @@ -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 +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 - diff --git a/graph/test/javascript/expected/64-value-callee.oracle b/graph/test/javascript/expected/64-value-callee.oracle new file mode 100644 index 000000000..cc6bbd7a9 --- /dev/null +++ b/graph/test/javascript/expected/64-value-callee.oracle @@ -0,0 +1,2 @@ +handlers.js:12:28 FUNCTION_CALL alpha EXACT handlers.js:3:1 +# defects: 0 diff --git a/graph/test/javascript/tools/normalize_diagnostics.py b/graph/test/javascript/tools/normalize_diagnostics.py index 4114a29d7..242a8bfd5 100644 --- a/graph/test/javascript/tools/normalize_diagnostics.py +++ b/graph/test/javascript/tools/normalize_diagnostics.py @@ -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' % ( diff --git a/plugins/axiomcode/skills/axiomcode/scripts/ax_pages.py b/plugins/axiomcode/skills/axiomcode/scripts/ax_pages.py index a0d932d02..a809781fa 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/ax_pages.py +++ b/plugins/axiomcode/skills/axiomcode/scripts/ax_pages.py @@ -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 ` 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 diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path index 404481d61..9c7659b82 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path @@ -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. @@ -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] @@ -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 diff --git a/tests/cases/javascript/value-callee-is-unknown/case.json b/tests/cases/javascript/value-callee-is-unknown/case.json new file mode 100644 index 000000000..723d51064 --- /dev/null +++ b/tests/cases/javascript/value-callee-is-unknown/case.json @@ -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 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" + ] + } + ] +} \ No newline at end of file diff --git a/tests/cases/javascript/value-callee-is-unknown/src/handlers.js b/tests/cases/javascript/value-callee-is-unknown/src/handlers.js new file mode 100644 index 000000000..9fa522427 --- /dev/null +++ b/tests/cases/javascript/value-callee-is-unknown/src/handlers.js @@ -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 };