diff --git a/graph/javascript/engine/call-edge-generation/partial-dispatch.dl b/graph/javascript/engine/call-edge-generation/partial-dispatch.dl index 5af7f6c12..9d2b24190 100644 --- a/graph/javascript/engine/call-edge-generation/partial-dispatch.dl +++ b/graph/javascript/engine/call-edge-generation/partial-dispatch.dl @@ -58,12 +58,27 @@ swallowed_read(e, i) :- expr_kind(_, "ELEMENT_ACCESS", _, e), expr_kind(_, "BINARY", _, key), expr_operator(_, op, key), default_op(op), expr_child(_, e, "ACCESS_TARGET", _, r), expr_value(r, "obj", i). +// ── the variable a site calls through, and every value written into it ───── +// Its initializer and each assignment (`h = d`, `h ??= d`, `h ||= d`). +var_source(v, e) :- var_init(_, _, e, v). +var_source(v, e) :- expr_kind(_, "ASSIGNMENT", _, a), expr_child(_, a, "ASSIGNMENT_TARGET", _, tgt), + expr_binding(_, v, tgt), expr_child(_, a, "ASSIGNMENT_VALUE", _, e). +// A default BY REASSIGNMENT: `let h = t[k]; if (!h) h = d;`, `h ??= d`, or the other +// way round. The variable holds the open read and something else, so the value layer +// sees only the something else and asserts it; the read is swallowed all the same. +swallowed_var(v, i) :- var_source(v, e), open_read(e, i), var_source(v, o), o != e. +// A default written INTO the variable: `const h = t[k] || d`, or `h = t[k] || d`. +swallowed_var(v, i) :- var_source(v, e), expr_child(_, e, _, _, o), swallowed_read(o, i). + // ── partial_dispatch(CallExpr) ───────────────────────────────────────────── -// The site invokes such a read: written at the site, or one hop through the const -// it was assigned to, which is how both real shapes are written. +// The site invokes such a read: written at the site (`t[k]()`, or the default itself +// as the callee, `(t[k] || d)()`, whose parentheses the parser drops), or through the +// variable it was stored in. partial_dispatch(ce) :- invocation_site(ce, _), expr_child(_, ce, "CALLEE", _, c), swallowed_read(c, _). partial_dispatch(ce) :- invocation_site(ce, _), expr_child(_, ce, "CALLEE", _, c), - expr_binding(_, v, c), var_init(_, _, ie, v), expr_child(_, ie, _, _, o), swallowed_read(o, _). + expr_child(_, c, _, _, o), swallowed_read(o, _). +partial_dispatch(ce) :- invocation_site(ce, _), expr_child(_, ce, "CALLEE", _, c), + expr_binding(_, v, c), swallowed_var(v, _). // `resolvers[](...)`: the parser puts the object under RECEIVER and the key // under COMPUTED_KEY on the call itself, with no ELEMENT_ACCESS node of its own. partial_dispatch(ce) :- call_site(_, "COMPUTED_CALL", _, _, _, _, ce, _, _), @@ -75,8 +90,9 @@ partial_dispatch(ce) :- call_site(_, "COMPUTED_CALL", _, _, _, _, ce, _, _), partial_dispatch_target(ce, m) :- invocation_site(ce, _), expr_child(_, ce, "CALLEE", _, c), swallowed_read(c, i), prop_value("obj", i, _, "func", m). partial_dispatch_target(ce, m) :- invocation_site(ce, _), expr_child(_, ce, "CALLEE", _, c), - expr_binding(_, v, c), var_init(_, _, ie, v), expr_child(_, ie, _, _, o), - swallowed_read(o, i), prop_value("obj", i, _, "func", m). + expr_child(_, c, _, _, o), swallowed_read(o, i), prop_value("obj", i, _, "func", m). +partial_dispatch_target(ce, m) :- invocation_site(ce, _), expr_child(_, ce, "CALLEE", _, c), + expr_binding(_, v, c), swallowed_var(v, i), prop_value("obj", i, _, "func", m). partial_dispatch_target(ce, m) :- call_site(_, "COMPUTED_CALL", _, _, _, _, ce, _, _), expr_child(_, ce, "COMPUTED_KEY", _, key), expr_kind(_, "BINARY", _, key), expr_operator(_, op, key), default_op(op), diff --git a/graph/javascript/souffle/decls_all.dl b/graph/javascript/souffle/decls_all.dl index 6d84c6a91..e876f7b24 100644 --- a/graph/javascript/souffle/decls_all.dl +++ b/graph/javascript/souffle/decls_all.dl @@ -295,6 +295,8 @@ .decl swallowed_read(c0:symbol, c1:symbol) .decl partial_dispatch(c0:symbol) .decl partial_dispatch_target(c0:symbol, c1:symbol) +.decl var_source(c0:symbol, c1:symbol) +.decl swallowed_var(c0:symbol, c1:symbol) // the same read with no default beside it, called: one of the literal's functions .decl open_read(c0:symbol, c1:symbol) .decl open_dispatch_target(c0:symbol, c1:symbol) diff --git a/graph/test/javascript/cases/70-dispatch-table-default-inline-or-reassigned/src/main.js b/graph/test/javascript/cases/70-dispatch-table-default-inline-or-reassigned/src/main.js new file mode 100644 index 000000000..c9aa2edda --- /dev/null +++ b/graph/test/javascript/cases/70-dispatch-table-default-inline-or-reassigned/src/main.js @@ -0,0 +1,79 @@ +// A DISPATCH TABLE read with a computed key and defaulted, written without a const +// between the default and the call (#1727). Case 61 pins `const h = t[k] || d; h()`; +// these are the same default written inline, or by assigning the variable again. The +// value layer sees only the default, so each used to be a known_edge to it alone. +import { remote } from './table.js'; + +function fromA(a) { return a; } +function fromB(a) { return a; } +function deep(a) { return [a]; } + +const table = { a: fromA, b: fromB }; + +// THE CONSTRUCT, inline: the default is the callee, `(t[k] || d)()`. +function inlineOr(k, a) { return (table[k] || deep)(a); } + +// THE CONSTRUCT, inline with a conditional. +function inlineTernary(k, a) { return (k in table ? table[k] : deep)(a); } + +// THE CONSTRUCT, by reassignment: `if (!h) h = d`. +function reassigned(k, a) { + let h = table[k]; + if (!h) h = deep; + return h(a); +} + +// THE CONSTRUCT, by a logical assignment. +function nullishAssigned(k, a) { + let h = table[k]; + h ??= deep; + return h(a); +} + +// THE CONSTRUCT, the other way round: the default first, the read assigned over it. +function readAssigned(k, a) { + let h = deep; + if (k) h = table[k]; + return h(a); +} + +// THE CONSTRUCT, the table imported from another module. +function importedInline(k, a) { return (remote[k] || deep)(a); } +function importedReassigned(k, a) { + let h = remote[k]; + if (!h) h = deep; + return h(a); +} + +// CONTROL 1: an inline default whose key is WRITTEN AS A LITERAL: fromA and deep only. +function inlineLiteralKey(a) { return (table['a'] || deep)(a); } + +// CONTROL 2: a dotted read reassigned: fromB and deep only. +function reassignedDotted(a) { + let h = table.b; + if (!h) h = deep; + return h(a); +} + +// CONTROL 3: a variable reassigned with no table read at all: fromA and deep only. +function reassignedPlain(k, a) { + let h = fromA; + if (k) h = deep; + return h(a); +} + +// CONTROL 4: the default alone, inline. The one known target stays a known_edge. +function inlineDefaultOnly(k, a) { return (k || deep)(a); } + +const key = process.argv[2]; +inlineOr(key, 1); +inlineTernary(key, 1); +reassigned(key, 1); +nullishAssigned(key, 1); +readAssigned(key, 1); +importedInline(key, 1); +importedReassigned(key, 1); +inlineLiteralKey(1); +reassignedDotted(1); +reassignedPlain(key, 1); +inlineDefaultOnly(key, 1); diff --git a/graph/test/javascript/cases/70-dispatch-table-default-inline-or-reassigned/src/table.js b/graph/test/javascript/cases/70-dispatch-table-default-inline-or-reassigned/src/table.js new file mode 100644 index 000000000..85898f954 --- /dev/null +++ b/graph/test/javascript/cases/70-dispatch-table-default-inline-or-reassigned/src/table.js @@ -0,0 +1,3 @@ +function fromC(a) { return a; } +function fromD(a) { return a; } +export const remote = { c: fromC, d: fromD }; diff --git a/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.diag b/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.diag new file mode 100644 index 000000000..3ef2afad0 --- /dev/null +++ b/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.diag @@ -0,0 +1 @@ +package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> - diff --git a/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.edges b/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.edges new file mode 100644 index 000000000..f7aad7a0f --- /dev/null +++ b/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.edges @@ -0,0 +1,40 @@ +main.js:14:34 FUNCTION_CALL (table[k] || deep) -> multi_inferred main.js:7:1 fromA +main.js:14:34 FUNCTION_CALL (table[k] || deep) -> multi_inferred main.js:8:1 fromB +main.js:14:34 FUNCTION_CALL (table[k] || deep) -> multi_inferred main.js:9:1 deep +main.js:17:39 FUNCTION_CALL (k in table ? table[k] : deep) -> multi_inferred main.js:7:1 fromA +main.js:17:39 FUNCTION_CALL (k in table ? table[k] : deep) -> multi_inferred main.js:8:1 fromB +main.js:17:39 FUNCTION_CALL (k in table ? table[k] : deep) -> multi_inferred main.js:9:1 deep +main.js:23:10 FUNCTION_CALL h -> multi_inferred main.js:7:1 fromA +main.js:23:10 FUNCTION_CALL h -> multi_inferred main.js:8:1 fromB +main.js:23:10 FUNCTION_CALL h -> multi_inferred main.js:9:1 deep +main.js:30:10 FUNCTION_CALL h -> multi_inferred main.js:7:1 fromA +main.js:30:10 FUNCTION_CALL h -> multi_inferred main.js:8:1 fromB +main.js:30:10 FUNCTION_CALL h -> multi_inferred main.js:9:1 deep +main.js:37:10 FUNCTION_CALL h -> multi_inferred main.js:7:1 fromA +main.js:37:10 FUNCTION_CALL h -> multi_inferred main.js:8:1 fromB +main.js:37:10 FUNCTION_CALL h -> multi_inferred main.js:9:1 deep +main.js:41:40 FUNCTION_CALL (remote[k] || deep) -> multi_inferred main.js:9:1 deep +main.js:41:40 FUNCTION_CALL (remote[k] || deep) -> multi_inferred table.js:1:1 fromC +main.js:41:40 FUNCTION_CALL (remote[k] || deep) -> multi_inferred table.js:2:1 fromD +main.js:45:10 FUNCTION_CALL h -> multi_inferred main.js:9:1 deep +main.js:45:10 FUNCTION_CALL h -> multi_inferred table.js:1:1 fromC +main.js:45:10 FUNCTION_CALL h -> multi_inferred table.js:2:1 fromD +main.js:49:39 FUNCTION_CALL (table['a'] || deep) -> multi_inferred main.js:7:1 fromA +main.js:49:39 FUNCTION_CALL (table['a'] || deep) -> multi_inferred main.js:9:1 deep +main.js:55:10 FUNCTION_CALL h -> multi_inferred main.js:8:1 fromB +main.js:55:10 FUNCTION_CALL h -> multi_inferred main.js:9:1 deep +main.js:62:10 FUNCTION_CALL h -> multi_inferred main.js:7:1 fromA +main.js:62:10 FUNCTION_CALL h -> multi_inferred main.js:9:1 deep +main.js:66:43 FUNCTION_CALL (k || deep) -> ambient_terminal - +main.js:66:43 FUNCTION_CALL (k || deep) -> multi_inferred main.js:9:1 deep +main.js:69:1 FUNCTION_CALL inlineOr -> known_edge main.js:14:1 inlineOr +main.js:70:1 FUNCTION_CALL inlineTernary -> known_edge main.js:17:1 inlineTernary +main.js:71:1 FUNCTION_CALL reassigned -> known_edge main.js:20:1 reassigned +main.js:72:1 FUNCTION_CALL nullishAssigned -> known_edge main.js:27:1 nullishAssigned +main.js:73:1 FUNCTION_CALL readAssigned -> known_edge main.js:34:1 readAssigned +main.js:74:1 FUNCTION_CALL importedInline -> known_edge main.js:41:1 importedInline +main.js:75:1 FUNCTION_CALL importedReassigned -> known_edge main.js:42:1 importedReassigned +main.js:76:1 FUNCTION_CALL inlineLiteralKey -> known_edge main.js:49:1 inlineLiteralKey +main.js:77:1 FUNCTION_CALL reassignedDotted -> known_edge main.js:52:1 reassignedDotted +main.js:78:1 FUNCTION_CALL reassignedPlain -> known_edge main.js:59:1 reassignedPlain +main.js:79:1 FUNCTION_CALL inlineDefaultOnly -> known_edge main.js:66:1 inlineDefaultOnly diff --git a/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.oracle b/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.oracle new file mode 100644 index 000000000..927704eb5 --- /dev/null +++ b/graph/test/javascript/expected/70-dispatch-table-default-inline-or-reassigned.oracle @@ -0,0 +1,15 @@ +main.js:37:10 FUNCTION_CALL h SOUND_SUPERSET main.js:7:1;main.js:8:1;main.js:9:1 +main.js:55:10 FUNCTION_CALL h SOUND_SUPERSET main.js:8:1;main.js:9:1 +main.js:62:10 FUNCTION_CALL h SOUND_SUPERSET main.js:7:1;main.js:9:1 +main.js:69:1 FUNCTION_CALL inlineOr EXACT main.js:14:1 +main.js:70:1 FUNCTION_CALL inlineTernary EXACT main.js:17:1 +main.js:71:1 FUNCTION_CALL reassigned EXACT main.js:20:1 +main.js:72:1 FUNCTION_CALL nullishAssigned EXACT main.js:27:1 +main.js:73:1 FUNCTION_CALL readAssigned EXACT main.js:34:1 +main.js:74:1 FUNCTION_CALL importedInline EXACT main.js:41:1 +main.js:75:1 FUNCTION_CALL importedReassigned EXACT main.js:42:1 +main.js:76:1 FUNCTION_CALL inlineLiteralKey EXACT main.js:49:1 +main.js:77:1 FUNCTION_CALL reassignedDotted EXACT main.js:52:1 +main.js:78:1 FUNCTION_CALL reassignedPlain EXACT main.js:59:1 +main.js:79:1 FUNCTION_CALL inlineDefaultOnly EXACT main.js:66:1 +# defects: 0