From 0a6c391b772e4874076414f179c737856071aa6a Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:48:30 -0700 Subject: [PATCH 1/2] javascript: path says unknown, not independent, when one side calls a function stored on this, in a module variable, in a tested options member, or through an expression (#1675) unresolved_value_callee gains five reasons, each an unresolved call whose callee is a value the graph does not hold: - module_variable: `let handler = null; ... handler()` - field: `this.cb()` where cb is a field, not a method - parameter_member: `opts.onReady()` when the code treats the member as an optional function (typeof test, `&&`/`||`/`??` guard, if condition, `?.()`) - expression: `(c ? a : b)()`, `(0, cb)()`, `make()()` - dynamic_code: `new Function(s)()` A bare member call on an untyped parameter (`res.send()`, `s.trim()`) is not counted: without a type it reads the same as a library method, and counting it turned 10 of 12 independent pairs on a real Express app into unknown. No call edge changes. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../engine/call-edge-generation/calls.dl | 48 +++- graph/javascript/souffle/decls_all.dl | 6 + .../65-stored-value-callee/src/stored.js | 28 ++ .../expected/04-parameters-and-fields.diag | 1 + .../expected/63-jsx-wrapper-lazy.diag | 1 + .../expected/65-stored-value-callee.diag | 26 ++ .../expected/65-stored-value-callee.edges | 18 ++ .../expected/65-stored-value-callee.oracle | 6 + .../skills/axiomcode/scripts/axiomcode-path | 4 +- .../stored-value-callee-is-unknown/case.json | 269 ++++++++++++++++++ .../src/stored.js | 28 ++ 11 files changed, 432 insertions(+), 3 deletions(-) create mode 100644 graph/test/javascript/cases/65-stored-value-callee/src/stored.js create mode 100644 graph/test/javascript/expected/65-stored-value-callee.diag create mode 100644 graph/test/javascript/expected/65-stored-value-callee.edges create mode 100644 graph/test/javascript/expected/65-stored-value-callee.oracle create mode 100644 tests/cases/javascript/stored-value-callee-is-unknown/case.json create mode 100644 tests/cases/javascript/stored-value-callee-is-unknown/src/stored.js diff --git a/graph/javascript/engine/call-edge-generation/calls.dl b/graph/javascript/engine/call-edge-generation/calls.dl index 55697c0d0..1ac25b8df 100644 --- a/graph/javascript/engine/call-edge-generation/calls.dl +++ b/graph/javascript/engine/call-edge-generation/calls.dl @@ -78,13 +78,57 @@ 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). +callee_platform_function(ce) :- callee_value(ce, "ambient", n), n != "LITERAL". +// `(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. +unresolved_value_callee(ce, "field") :- call_site("client", ck, n, "SYNTACTIC", _, em, ce, _, _), + call_kind_is_member_form(ck), !call_kind_is_dynamic(ck), !call_resolved(ce), !callee_value_known(ce), + expr_child(_, ce, "RECEIVER", _, r), expr_kind(_, "THIS", _, r), this_value(em, _, t), this_field_named(t, n). +this_field_named(t, n) :- field_decl("client", n, t, _, _, _, _, _). +this_field_named(t, n) :- type_ancestor(t, a), field_decl("client", n, a, _, _, _, _, _). +// 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). diff --git a/graph/javascript/souffle/decls_all.dl b/graph/javascript/souffle/decls_all.dl index 32ddecf4c..beba22868 100644 --- a/graph/javascript/souffle/decls_all.dl +++ b/graph/javascript/souffle/decls_all.dl @@ -72,6 +72,12 @@ .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) // ── containment/ownership.dl ── .decl call_caller_method(c0:symbol, c1:symbol) diff --git a/graph/test/javascript/cases/65-stored-value-callee/src/stored.js b/graph/test/javascript/cases/65-stored-value-callee/src/stored.js new file mode 100644 index 000000000..8bcc315c4 --- /dev/null +++ b/graph/test/javascript/cases/65-stored-value-callee/src/stored.js @@ -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 }; diff --git a/graph/test/javascript/expected/04-parameters-and-fields.diag b/graph/test/javascript/expected/04-parameters-and-fields.diag index 575e18922..91cbbcbe7 100644 --- a/graph/test/javascript/expected/04-parameters-and-fields.diag +++ b/graph/test/javascript/expected/04-parameters-and-fields.diag @@ -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 diff --git a/graph/test/javascript/expected/63-jsx-wrapper-lazy.diag b/graph/test/javascript/expected/63-jsx-wrapper-lazy.diag index edf63700d..ce31a3fec 100644 --- a/graph/test/javascript/expected/63-jsx-wrapper-lazy.diag +++ b/graph/test/javascript/expected/63-jsx-wrapper-lazy.diag @@ -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 diff --git a/graph/test/javascript/expected/65-stored-value-callee.diag b/graph/test/javascript/expected/65-stored-value-callee.diag new file mode 100644 index 000000000..bf13ca669 --- /dev/null +++ b/graph/test/javascript/expected/65-stored-value-callee.diag @@ -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 diff --git a/graph/test/javascript/expected/65-stored-value-callee.edges b/graph/test/javascript/expected/65-stored-value-callee.edges new file mode 100644 index 000000000..de30a1ecc --- /dev/null +++ b/graph/test/javascript/expected/65-stored-value-callee.edges @@ -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 +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 - diff --git a/graph/test/javascript/expected/65-stored-value-callee.oracle b/graph/test/javascript/expected/65-stored-value-callee.oracle new file mode 100644 index 000000000..5d93afb30 --- /dev/null +++ b/graph/test/javascript/expected/65-stored-value-callee.oracle @@ -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 diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path index 585a7fa03..46ceb732c 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path @@ -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 on this', '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]: diff --git a/tests/cases/javascript/stored-value-callee-is-unknown/case.json b/tests/cases/javascript/stored-value-callee-is-unknown/case.json new file mode 100644 index 000000000..7e32fdfc6 --- /dev/null +++ b/tests/cases/javascript/stored-value-callee-is-unknown/case.json @@ -0,0 +1,269 @@ +{ + "lang": "javascript", + "src": "src", + "checks": [ + { + "why": "fire calls a module-level variable a setter assigns: the callee can be any function stored or handed to it, so path says unknown and names the site (#1675)", + "run": [ + "path", + "fire", + "alpha" + ], + "expect_error": true, + "want": [ + "connection is UNKNOWN, not absent", + "at src/stored.js:6 — the callee is a module-level variable", + "not shown to be independent" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "Emitter.emit calls a function stored on this in the constructor: the callee can be any function stored or handed to it, so path says unknown and names the site (#1675)", + "run": [ + "path", + "Emitter.emit", + "alpha" + ], + "expect_error": true, + "want": [ + "connection is UNKNOWN, not absent", + "at src/stored.js:9 — the callee is a function stored on this", + "not shown to be independent" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "optsTypeof calls an options-object member it tests with typeof: the callee can be any function stored or handed to it, so path says unknown and names the site (#1675)", + "run": [ + "path", + "optsTypeof", + "alpha" + ], + "expect_error": true, + "want": [ + "connection is UNKNOWN, not absent", + "at src/stored.js:13 — the callee is a member of a parameter", + "not shown to be independent" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "optsAnd calls an options-object member guarded by &&: the callee can be any function stored or handed to it, so path says unknown and names the site (#1675)", + "run": [ + "path", + "optsAnd", + "alpha" + ], + "expect_error": true, + "want": [ + "connection is UNKNOWN, not absent", + "at src/stored.js:14 — the callee is a member of a parameter", + "not shown to be independent" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "optsIf calls an options-object member guarded by if: the callee can be any function stored or handed to it, so path says unknown and names the site (#1675)", + "run": [ + "path", + "optsIf", + "alpha" + ], + "expect_error": true, + "want": [ + "connection is UNKNOWN, not absent", + "at src/stored.js:15 — the callee is a member of a parameter", + "not shown to be independent" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "optsOptional calls an options-object member with ?.(): the callee can be any function stored or handed to it, so path says unknown and names the site (#1675)", + "run": [ + "path", + "optsOptional", + "alpha" + ], + "expect_error": true, + "want": [ + "connection is UNKNOWN, not absent", + "at src/stored.js:16 — the callee is a member of a parameter", + "not shown to be independent" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "condInvoke calls a conditional expression: the callee can be any function stored or handed to it, so path says unknown and names the site (#1675)", + "run": [ + "path", + "condInvoke", + "alpha" + ], + "expect_error": true, + "want": [ + "connection is UNKNOWN, not absent", + "at src/stored.js:17 — the callee is computed by an expression", + "not shown to be independent" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "seqInvoke calls a sequence expression: the callee can be any function stored or handed to it, so path says unknown and names the site (#1675)", + "run": [ + "path", + "seqInvoke", + "alpha" + ], + "expect_error": true, + "want": [ + "connection is UNKNOWN, not absent", + "at src/stored.js:18 — the callee is computed by an expression", + "not shown to be independent" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "newFn calls a function built from a string: the callee can be any function stored or handed to it, so path says unknown and names the site (#1675)", + "run": [ + "path", + "newFn", + "alpha" + ], + "expect_error": true, + "want": [ + "connection is UNKNOWN, not absent", + "at src/stored.js:19 — the callee is code built at run time", + "not shown to be independent" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "control: this.own() resolves to a method of the class, not a stored function", + "run": [ + "path", + "Emitter.callsOwn", + "alpha" + ], + "expect_error": true, + "want": [ + "the two are independent in this graph" + ], + "avoid": [ + "connection is UNKNOWN, not absent", + "through a value" + ] + }, + { + "why": "control: a member call on a core-module import is a platform call", + "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 member call on a string literal is a platform call", + "run": [ + "path", + "literalTrim", + "alpha" + ], + "expect_error": true, + "want": [ + "the two are independent in this graph" + ], + "avoid": [ + "connection is UNKNOWN, not absent", + "through a value" + ] + }, + { + "why": "control: a function that calls nothing", + "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 bare member call on an untyped parameter reads the same as a library method (res.send, s.trim) and is not counted", + "run": [ + "path", + "optsPlain", + "alpha" + ], + "expect_error": true, + "want": [ + "the two are independent in this graph" + ], + "avoid": [ + "connection is UNKNOWN, not absent", + "through a value" + ] + }, + { + "why": "control: res.status().send() on a handler's parameters is a library call, not a stored function", + "run": [ + "path", + "respond", + "alpha" + ], + "expect_error": true, + "want": [ + "the two are independent in this graph" + ], + "avoid": [ + "connection is UNKNOWN, not absent", + "through a value" + ] + }, + { + "why": "control: a module-level variable holding a function resolves, so the chain is found", + "run": [ + "path", + "viaHelper", + "alpha" + ], + "want": [ + "viaHelper → 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/stored-value-callee-is-unknown/src/stored.js b/tests/cases/javascript/stored-value-callee-is-unknown/src/stored.js new file mode 100644 index 000000000..8bcc315c4 --- /dev/null +++ b/tests/cases/javascript/stored-value-callee-is-unknown/src/stored.js @@ -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 }; From 61bb4ad7d1f9df0650942164cc876372a2181c63 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Mon, 28 Sep 2026 12:59:46 -0700 Subject: [PATCH 2/2] javascript: path says unknown for a field set to null, a static or aliased this field, a fallback field and new this.Ctor() (#1675) The field reason of unresolved_value_callee now covers: - a field holding only null until a setter runs (`cb = null`, `this.cb = null`, `static cb = null`): the null literal no longer counts as the known value - a static `this.cb = f` / `this.cb()` pair, known by the assignment, not a field declaration - `self.cb()` with `const self = this`: any receiver valued as the instance or class - `new this.Ctor()` - `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 A module variable initialised from a call and never reassigned (`const log = require('debug')('app')`) is no longer a value callee: the library made it. One handed a function (`memo(Inner)`) stays open. `Model.tag2()` (a known-missing member of case 49) now names the field. No call edge changes. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../engine/call-edge-generation/calls.dl | 40 +++- graph/javascript/souffle/decls_all.dl | 10 +- .../66-stored-callee-shapes/src/shapes.js | 47 ++++ .../49-chained-member-assignment.diag | 1 + .../expected/66-stored-callee-shapes.diag | 24 ++ .../expected/66-stored-callee-shapes.edges | 18 ++ .../expected/66-stored-callee-shapes.oracle | 5 + .../skills/axiomcode/scripts/axiomcode-path | 2 +- .../stored-field-callee-shapes/case.json | 220 ++++++++++++++++++ .../stored-field-callee-shapes/src/shapes.js | 47 ++++ .../stored-value-callee-is-unknown/case.json | 2 +- 11 files changed, 406 insertions(+), 10 deletions(-) create mode 100644 graph/test/javascript/cases/66-stored-callee-shapes/src/shapes.js create mode 100644 graph/test/javascript/expected/66-stored-callee-shapes.diag create mode 100644 graph/test/javascript/expected/66-stored-callee-shapes.edges create mode 100644 graph/test/javascript/expected/66-stored-callee-shapes.oracle create mode 100644 tests/cases/javascript/stored-field-callee-shapes/case.json create mode 100644 tests/cases/javascript/stored-field-callee-shapes/src/shapes.js diff --git a/graph/javascript/engine/call-edge-generation/calls.dl b/graph/javascript/engine/call-edge-generation/calls.dl index 1ac25b8df..593693d1e 100644 --- a/graph/javascript/engine/call-edge-generation/calls.dl +++ b/graph/javascript/engine/call-edge-generation/calls.dl @@ -96,8 +96,17 @@ value_callee_unresolved(ce, c) :- call_site("client", ck, _, _, _, _, ce, _, _), // (`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). + !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). @@ -106,12 +115,29 @@ unresolved_value_callee(ce, "expression") :- value_callee_open(ce, c), expr_kind 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. -unresolved_value_callee(ce, "field") :- call_site("client", ck, n, "SYNTACTIC", _, em, ce, _, _), - call_kind_is_member_form(ck), !call_kind_is_dynamic(ck), !call_resolved(ce), !callee_value_known(ce), - expr_child(_, ce, "RECEIVER", _, r), expr_kind(_, "THIS", _, r), this_value(em, _, t), this_field_named(t, n). -this_field_named(t, n) :- field_decl("client", n, t, _, _, _, _, _). -this_field_named(t, n) :- type_ancestor(t, a), field_decl("client", n, a, _, _, _, _, _). +// 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 diff --git a/graph/javascript/souffle/decls_all.dl b/graph/javascript/souffle/decls_all.dl index beba22868..1b29b551e 100644 --- a/graph/javascript/souffle/decls_all.dl +++ b/graph/javascript/souffle/decls_all.dl @@ -77,7 +77,15 @@ .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) +.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) diff --git a/graph/test/javascript/cases/66-stored-callee-shapes/src/shapes.js b/graph/test/javascript/cases/66-stored-callee-shapes/src/shapes.js new file mode 100644 index 000000000..753496438 --- /dev/null +++ b/graph/test/javascript/cases/66-stored-callee-shapes/src/shapes.js @@ -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 }; diff --git a/graph/test/javascript/expected/49-chained-member-assignment.diag b/graph/test/javascript/expected/49-chained-member-assignment.diag index d6277ea1a..351ec34a0 100644 --- a/graph/test/javascript/expected/49-chained-member-assignment.diag +++ b/graph/test/javascript/expected/49-chained-member-assignment.diag @@ -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 diff --git a/graph/test/javascript/expected/66-stored-callee-shapes.diag b/graph/test/javascript/expected/66-stored-callee-shapes.diag new file mode 100644 index 000000000..b86fee8ab --- /dev/null +++ b/graph/test/javascript/expected/66-stored-callee-shapes.diag @@ -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 diff --git a/graph/test/javascript/expected/66-stored-callee-shapes.edges b/graph/test/javascript/expected/66-stored-callee-shapes.edges new file mode 100644 index 000000000..d3c72d58f --- /dev/null +++ b/graph/test/javascript/expected/66-stored-callee-shapes.edges @@ -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 +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 - diff --git a/graph/test/javascript/expected/66-stored-callee-shapes.oracle b/graph/test/javascript/expected/66-stored-callee-shapes.oracle new file mode 100644 index 000000000..fb75cc273 --- /dev/null +++ b/graph/test/javascript/expected/66-stored-callee-shapes.oracle @@ -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 diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path index 46ceb732c..4f7747d12 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path @@ -1492,7 +1492,7 @@ def path(g, a, b, show_all=False, limit=10, every=False, max_paths=20): 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', 'module_variable': 'a module-level variable', - 'field': 'a function stored on this', 'parameter_member': 'a member of a parameter', + '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:") diff --git a/tests/cases/javascript/stored-field-callee-shapes/case.json b/tests/cases/javascript/stored-field-callee-shapes/case.json new file mode 100644 index 000000000..b17c9107f --- /dev/null +++ b/tests/cases/javascript/stored-field-callee-shapes/case.json @@ -0,0 +1,220 @@ +{ + "lang": "javascript", + "src": "src", + "checks": [ + { + "why": "FieldNull.run calls a class field initialised to null and filled by a setter: the callee can be any function stored or handed to it, so path says unknown and names the site (#1675)", + "run": [ + "path", + "FieldNull.run", + "alpha" + ], + "expect_error": true, + "want": [ + "connection is UNKNOWN, not absent", + "at src/shapes.js:7 — the callee is a function stored in a field", + "not shown to be independent" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "CtorNull.run calls a field set to null in the constructor and filled by a setter: the callee can be any function stored or handed to it, so path says unknown and names the site (#1675)", + "run": [ + "path", + "CtorNull.run", + "alpha" + ], + "expect_error": true, + "want": [ + "connection is UNKNOWN, not absent", + "at src/shapes.js:12 — the callee is a function stored in a field", + "not shown to be independent" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "Fallback.run calls a field holding a parameter or a fallback function: the callee can be any function stored or handed to it, so path says unknown and names the site (#1675)", + "run": [ + "path", + "Fallback.run", + "alpha" + ], + "expect_error": true, + "want": [ + "connection is UNKNOWN, not absent", + "at src/shapes.js:16 — the callee is a function stored in a field", + "not shown to be independent" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "Static.run calls a static field a static setter fills: the callee can be any function stored or handed to it, so path says unknown and names the site (#1675)", + "run": [ + "path", + "Static.run", + "alpha" + ], + "expect_error": true, + "want": [ + "connection is UNKNOWN, not absent", + "at src/shapes.js:20 — the callee is a function stored in a field", + "not shown to be independent" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "StaticField.run calls a static class field initialised to null: the callee can be any function stored or handed to it, so path says unknown and names the site (#1675)", + "run": [ + "path", + "StaticField.run", + "alpha" + ], + "expect_error": true, + "want": [ + "connection is UNKNOWN, not absent", + "at src/shapes.js:24 — the callee is a function stored in a field", + "not shown to be independent" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "Alias.run calls a field read through an alias of this: the callee can be any function stored or handed to it, so path says unknown and names the site (#1675)", + "run": [ + "path", + "Alias.run", + "alpha" + ], + "expect_error": true, + "want": [ + "connection is UNKNOWN, not absent", + "at src/shapes.js:28 — the callee is a function stored in a field", + "not shown to be independent" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "Maker.make calls a constructor stored in a field: the callee can be any function stored or handed to it, so path says unknown and names the site (#1675)", + "run": [ + "path", + "Maker.make", + "alpha" + ], + "expect_error": true, + "want": [ + "connection is UNKNOWN, not absent", + "at src/shapes.js:32 — the callee is a function stored in a field", + "not shown to be independent" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "callsWrapped calls a module variable holding what a library returned for a project function: the callee can be any function stored or handed to it, so path says unknown and names the site (#1675)", + "run": [ + "path", + "callsWrapped", + "alpha" + ], + "expect_error": true, + "want": [ + "connection is UNKNOWN, not absent", + "at src/shapes.js:39 — the callee is a module-level variable", + "not shown to be independent" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "control: a module variable holding what a library call returned for a string is a library function", + "run": [ + "path", + "logs", + "alpha" + ], + "expect_error": true, + "want": [ + "the two are independent in this graph" + ], + "avoid": [ + "connection is UNKNOWN, not absent", + "through a value" + ] + }, + { + "why": "control: a module variable holding a platform function is a platform call", + "run": [ + "path", + "tags", + "alpha" + ], + "expect_error": true, + "want": [ + "the two are independent in this graph" + ], + "avoid": [ + "connection is UNKNOWN, not absent", + "through a value" + ] + }, + { + "why": "control: this.emit() on a class extending a library class is an inherited library method, not a field", + "run": [ + "path", + "Bus.go", + "alpha" + ], + "expect_error": true, + "want": [ + "the two are independent in this graph" + ], + "avoid": [ + "connection is UNKNOWN, not absent", + "through a value" + ] + }, + { + "why": "control: a method call on a project instance resolves", + "run": [ + "path", + "useOwn", + "alpha" + ], + "expect_error": true, + "want": [ + "the two are independent in this graph" + ], + "avoid": [ + "connection is UNKNOWN, not absent", + "through a value" + ] + }, + { + "why": "control: a field holding a project function resolves, so the chain is found", + "run": [ + "path", + "Known.run", + "alpha" + ], + "want": [ + "Known.run → alpha: 1 of 1 target(s) reached" + ], + "avoid": [ + "connection is UNKNOWN, not absent" + ] + } + ] +} diff --git a/tests/cases/javascript/stored-field-callee-shapes/src/shapes.js b/tests/cases/javascript/stored-field-callee-shapes/src/shapes.js new file mode 100644 index 000000000..753496438 --- /dev/null +++ b/tests/cases/javascript/stored-field-callee-shapes/src/shapes.js @@ -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 }; diff --git a/tests/cases/javascript/stored-value-callee-is-unknown/case.json b/tests/cases/javascript/stored-value-callee-is-unknown/case.json index 7e32fdfc6..f22bd8699 100644 --- a/tests/cases/javascript/stored-value-callee-is-unknown/case.json +++ b/tests/cases/javascript/stored-value-callee-is-unknown/case.json @@ -29,7 +29,7 @@ "expect_error": true, "want": [ "connection is UNKNOWN, not absent", - "at src/stored.js:9 — the callee is a function stored on this", + "at src/stored.js:9 — the callee is a function stored in a field", "not shown to be independent" ], "avoid": [