diff --git a/graph/javascript/engine/resolution/ambient.dl b/graph/javascript/engine/resolution/ambient.dl index 3f0d8ebe2..7d321c896 100644 --- a/graph/javascript/engine/resolution/ambient.dl +++ b/graph/javascript/engine/resolution/ambient.dl @@ -73,6 +73,26 @@ modelled_platform_call(ce) :- expr_kind(_, "CALL", _, ce), call_site(_, "METHOD_ expr_kind(_, "IDENTIFIER", _, recv), expr_text(_, "Object", recv). modelled_platform_call(ce) :- expr_kind(_, "CALL", _, ce), call_site(_, "METHOD_CALL", "_extend", _, _, _, ce, _, _), expr_child(_, ce, "RECEIVER", _, recv), expr_kind(_, "IDENTIFIER", _, recv), expr_text(_, "util", recv). +// `promisify(f)` is f behind a transparent wrapper (value-flow.dl): its value is what f holds. +modelled_platform_call(ce) :- promisify_call(ce). +// promisify_call(Call): the core `util` module's `promisify`, as a name imported from it +// (`const { promisify } = require('util')`, `import { promisify } from 'node:util'`) or as +// a member of it (`util.promisify(f)` with `util` bound to the module, `require('util').promisify(f)`). +// Recognised on the import rows and the binder, never by the name alone: a project's own +// `promisify` is an ordinary function and resolves as one. +promisify_call(ce) :- call_site(_, "FUNCTION_CALL", "promisify", _, _, _, ce, _, _), + expr_child(_, ce, "CALLEE", _, c), expr_binding(_, v, c), var_import(_, imp, v), + import_decl(_, spec, _, _, "promisify", _, "RESOLVED_BUILTIN", _, imp), util_module(spec). +promisify_call(ce) :- call_site(_, "METHOD_CALL", "promisify", _, _, _, ce, _, _), + expr_child(_, ce, "RECEIVER", _, r), expr_binding(_, v, r), var_import(_, imp, v), + import_decl(_, spec, _, bf, _, _, "RESOLVED_BUILTIN", _, imp), import_binds_whole_module(bf), util_module(spec). +promisify_call(ce) :- call_site(_, "METHOD_CALL", "promisify", _, _, _, ce, _, _), + expr_child(_, ce, "RECEIVER", _, r), expr_kind(_, "MODULE_EDGE_CALL", _, r), expr_module_edge(_, imp, r), + import_decl(_, spec, _, _, _, _, "RESOLVED_BUILTIN", _, imp), util_module(spec). +import_binds_whole_module("DEFAULT"). +import_binds_whole_module("NAMESPACE"). +util_module("util"). +util_module("node:util"). // The Object statics whose result frameworks.dl gives a value of its own. modelled_object_method("assign"). modelled_object_method("create"). diff --git a/graph/javascript/engine/resolution/value-flow.dl b/graph/javascript/engine/resolution/value-flow.dl index 15379c699..d6f8c1323 100644 --- a/graph/javascript/engine/resolution/value-flow.dl +++ b/graph/javascript/engine/resolution/value-flow.dl @@ -155,6 +155,11 @@ expr_value(e, k, i) :- expr_kind(_, "NEW", _, e), new_callee_value(e, "func", m) // (callee-resolution.dl); the VALUE of the expression is f as well. expr_value(e, "func", m) :- expr_kind(_, "CALL", _, e), call_site(_, "FUNCTION_CALL_BIND", _, _, _, _, e, _, _), expr_child(_, e, "CALLEE", _, c), expr_value(c, "func", m). +// `promisify(f)` evaluates to a function that runs f (with a callback appended), so a call +// through it reaches f: the wrapper is transparent. What the argument holds is what the +// result holds, so `promisify(fs.readFile)` stays the platform's and an unknown argument +// stays unknown. ambient.dl keeps the platform's own value off it (modelled_platform_call). +expr_value(ce, k, i) :- promisify_call(ce), call_arg(ce, 0, a), expr_value(a, k, i). // Transparent wrappers: `await x`, `(c ? a : b)`, `a || b`, `a && b`, `a ?? b`, // `x = v` (an assignment expression evaluates to v), `(a, b)`. expr_value(e, k, i) :- expr_kind(_, "AWAIT", _, e), expr_child(_, e, _, _, c), expr_value(c, k, i). @@ -330,6 +335,18 @@ this_value(m, k, i) :- method_this_binding(_, "LEXICAL", m), method_enclosing(m, this_value(m, "obj", l) :- expr_child(_, l, "PROPERTY_VALUE", _, v), expr_kind(_, "OBJECT_LITERAL", _, l), expr_introduces(_, m, v), method_this_binding(_, "DYNAMIC", m), !method_owner_type(m, _). this_value(m, "obj", l) :- literal_owns_method(l, m), method_this_binding(_, "DYNAMIC", m), !method_owner_type(m, _). +// `f.bind(o)` fixes f's `this` to o for every call through what it returns, wherever that +// value travels: `onEvent.bind({ repo })` then `this.repo.append()` inside onEvent. The +// thisArg is the site's RECEIVER child. Only objects and instances, and only a function no +// class owns (a member's `this` is its instance already): `bind(null)` and a primitive +// bind nothing, and a class member re-bound to its own instance adds nothing new. +// `f.call(o)` / `f.apply(o)` are not read here: they run f once at that site. +this_value(m, k, i) :- call_site(_, "FUNCTION_CALL_BIND", _, _, _, _, ce, _, _), + expr_child(_, ce, "CALLEE", _, c), expr_value(c, "func", m), + method_this_binding(_, "DYNAMIC", m), !method_owner_type(m, _), + expr_child(_, ce, "RECEIVER", _, o), expr_value(o, k, i), bound_this_kind(k). +bound_this_kind("obj"). +bound_this_kind("inst"). // `T.prototype.constructor = T` is a BACK-REFERENCE, not an installation: nothing // calls it with the prototype as `this`, and reading it as one gave a constructor // the `Object.create(Base.prototype)` object as `this` (#708). diff --git a/graph/javascript/souffle/decls_all.dl b/graph/javascript/souffle/decls_all.dl index 1f91eb17b..ba441ed72 100644 --- a/graph/javascript/souffle/decls_all.dl +++ b/graph/javascript/souffle/decls_all.dl @@ -300,6 +300,10 @@ .decl well_known_symbol_read(c0:symbol) .decl modelled_platform_call(c0:symbol) .decl modelled_object_method(c0:symbol) +.decl promisify_call(c0:symbol) +.decl util_module(c0:symbol) +.decl import_binds_whole_module(c0:symbol) +.decl bound_this_kind(c0:symbol) .decl free_namespace(c0:symbol, c1:symbol) .decl ts_export_star(c0:symbol, c1:symbol) .decl ts_export_star_helper(c0:symbol) diff --git a/graph/python/engine/resolution/value-flow.dl b/graph/python/engine/resolution/value-flow.dl index 5e96f8689..230c1f1bf 100644 --- a/graph/python/engine/resolution/value-flow.dl +++ b/graph/python/engine/resolution/value-flow.dl @@ -329,14 +329,13 @@ partial_target(site, m) :- call_arg(site, "0", a), expr_denotes_method("client", a, m). -// ── a name holding a partial denotes the partial's target ──────────────────── -binding_value_method(p, b, m) :- - binding_rebinding(p, "1", _, _, b), - expr_binding(p, b, "STORE", tgt), - assign_pair(p, tgt, val), - call_of_expr(val, site), - partial_target(site, m), - p = "client". +// ── a partial denotes the partial's target ─────────────────────────────────── +// The call EXPRESSION, so wherever the partial goes the target goes with it: a name +// (`add_ten = partial(add, 10)`, through (d)'s alias rule), an attribute +// (`self.op = partial(repo.append)`, through field_holds_method), an argument. +expr_denotes_method("client", e, m) :- + call_of_expr(e, site), + partial_target(site, m). // ── param_arg_class(ParamHash, TypeHash) — a CLASS OBJECT reaching a parameter ─ // Distinct from param_arg_type on purpose: param_arg_type means "an INSTANCE of this diff --git a/graph/test/javascript/cases/73-bound-function-identity/src/bound.js b/graph/test/javascript/cases/73-bound-function-identity/src/bound.js new file mode 100644 index 000000000..40374a112 --- /dev/null +++ b/graph/test/javascript/cases/73-bound-function-identity/src/bound.js @@ -0,0 +1,50 @@ +'use strict'; +const { promisify } = require('util'); + +class Repo { + append(x) { return x; } + find(k) { return k; } +} +class Other { + append(x) { return x; } +} + +// (a) a field or variable assigned from x.m.bind(x) is x.m +class Service { + constructor(r) { + this.repo = r; + this.add = this.repo.append.bind(this.repo); + this.lookup = promisify(r.find.bind(r)); + } + go() { return this.add(1); } + get(k) { return this.lookup(k); } +} +function makeService() { return new Service(new Repo()); } +const repo = new Repo(); +const bound = repo.append.bind(repo); +function viaVariable() { return bound(2); } +const wrapped = promisify(repo.find.bind(repo)); +function viaPromisified() { return wrapped('k'); } +const util = require('util'); +const viaNamespace = util.promisify(repo.find.bind(repo)); +function viaNamespaceCall() { return viaNamespace('n'); } + +// (b) a function bound to an object literal sees its keys through `this` +function onEvent(e) { return this.repo.append(e); } +const handler = onEvent.bind({ repo: new Repo() }); +function fire() { return handler(1); } +const handlers = { + created: function onCreated(e) { return this.store.append(e); }, +}; +const onCreatedBound = handlers.created.bind({ store: new Other() }); + +// controls: none of these changes +function unbound(e) { return this.repo.append(e); } +function callsUnbound() { return unbound.call({ repo: new Repo() }, 1); } +const snapshot = repo.find.bind(null); +function openBind(fn) { const g = fn.bind(repo); return g(); } +const own = { promisify(f) { return () => f; } }; +const notUtil = own.promisify(repo.find); +function viaOwnPromisify() { return notUtil(); } + +module.exports = { makeService, viaVariable, viaPromisified, fire, onCreatedBound, callsUnbound, snapshot, openBind, viaNamespaceCall, viaOwnPromisify }; diff --git a/graph/test/javascript/expected/73-bound-function-identity.diag b/graph/test/javascript/expected/73-bound-function-identity.diag new file mode 100644 index 000000000..fcdd22f91 --- /dev/null +++ b/graph/test/javascript/expected/73-bound-function-identity.diag @@ -0,0 +1,11 @@ +import_cause bound.js:28:14 util builtin +import_cause bound.js:2:9 util builtin +package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> - +unresolved bound.js:17:19 FUNCTION_CALL promisify no_target +unresolved bound.js:26:17 FUNCTION_CALL promisify no_target +unresolved bound.js:29:22 METHOD_CALL promisify no_target +unresolved bound.js:42:30 METHOD_CALL append receiver_untyped +unresolved bound.js:45:35 FUNCTION_CALL_BIND fn callee_untyped +unresolved bound.js:45:57 FUNCTION_CALL g callee_untyped +value_callee bound.js:45:35 fn.bind parameter +value_callee bound.js:45:57 g local diff --git a/graph/test/javascript/expected/73-bound-function-identity.edges b/graph/test/javascript/expected/73-bound-function-identity.edges new file mode 100644 index 000000000..6cab99516 --- /dev/null +++ b/graph/test/javascript/expected/73-bound-function-identity.edges @@ -0,0 +1,35 @@ +bound.js:16:16 FUNCTION_CALL_BIND this.repo.append.bind -> known_edge bound.js:5:3 append +bound.js:17:19 FUNCTION_CALL promisify -> ambient_terminal - +bound.js:17:19 FUNCTION_CALL promisify -> callback_registered bound.js:6:3 find +bound.js:17:29 FUNCTION_CALL_BIND r.find.bind -> known_edge bound.js:6:3 find +bound.js:19:17 METHOD_CALL this.add -> known_edge bound.js:5:3 append +bound.js:20:19 METHOD_CALL this.lookup -> known_edge bound.js:6:3 find +bound.js:22:33 CONSTRUCTOR_CALL Service -> known_edge bound.js:14:3 +bound.js:22:45 CONSTRUCTOR_CALL Repo -> implicit_constructor - +bound.js:23:14 CONSTRUCTOR_CALL Repo -> implicit_constructor - +bound.js:24:15 FUNCTION_CALL_BIND repo.append.bind -> known_edge bound.js:5:3 append +bound.js:25:33 FUNCTION_CALL bound -> known_edge bound.js:5:3 append +bound.js:26:17 FUNCTION_CALL promisify -> ambient_terminal - +bound.js:26:17 FUNCTION_CALL promisify -> callback_registered bound.js:6:3 find +bound.js:26:27 FUNCTION_CALL_BIND repo.find.bind -> known_edge bound.js:6:3 find +bound.js:27:36 FUNCTION_CALL wrapped -> known_edge bound.js:6:3 find +bound.js:29:22 METHOD_CALL util.promisify -> ambient_terminal - +bound.js:29:22 METHOD_CALL util.promisify -> callback_registered bound.js:6:3 find +bound.js:29:37 FUNCTION_CALL_BIND repo.find.bind -> known_edge bound.js:6:3 find +bound.js:30:38 FUNCTION_CALL viaNamespace -> known_edge bound.js:6:3 find +bound.js:33:30 METHOD_CALL this.repo.append -> known_edge bound.js:5:3 append +bound.js:34:17 FUNCTION_CALL_BIND onEvent.bind -> known_edge bound.js:33:1 onEvent +bound.js:34:38 CONSTRUCTOR_CALL Repo -> implicit_constructor - +bound.js:35:26 FUNCTION_CALL handler -> known_edge bound.js:33:1 onEvent +bound.js:37:43 METHOD_CALL this.store.append -> known_edge bound.js:9:3 append +bound.js:39:24 FUNCTION_CALL_BIND handlers.created.bind -> known_edge bound.js:37:12 onCreated +bound.js:39:55 CONSTRUCTOR_CALL Other -> implicit_constructor - +bound.js:42:30 METHOD_CALL this.repo.append -> ambiguous_unknown - +bound.js:43:34 FUNCTION_CALL_CALL unbound.call -> known_edge bound.js:42:1 unbound +bound.js:43:55 CONSTRUCTOR_CALL Repo -> implicit_constructor - +bound.js:44:18 FUNCTION_CALL_BIND repo.find.bind -> known_edge bound.js:6:3 find +bound.js:45:35 FUNCTION_CALL_BIND fn.bind -> ambiguous_unknown - +bound.js:45:57 FUNCTION_CALL g -> ambiguous_unknown - +bound.js:47:17 METHOD_CALL own.promisify -> callback_registered bound.js:6:3 find +bound.js:47:17 METHOD_CALL own.promisify -> known_edge bound.js:46:15 promisify +bound.js:48:37 FUNCTION_CALL notUtil -> known_edge bound.js:46:37 diff --git a/graph/test/javascript/expected/73-bound-function-identity.oracle b/graph/test/javascript/expected/73-bound-function-identity.oracle new file mode 100644 index 000000000..01c9e34bd --- /dev/null +++ b/graph/test/javascript/expected/73-bound-function-identity.oracle @@ -0,0 +1,15 @@ +bound.js:22:33 CONSTRUCTOR_CALL Service EXACT bound.js:14:3 +bound.js:22:45 CONSTRUCTOR_CALL Repo SYNTHESIZED_OK +bound.js:23:14 CONSTRUCTOR_CALL Repo SYNTHESIZED_OK +bound.js:24:15 FUNCTION_CALL_BIND bind EXACT bound.js:5:3 +bound.js:26:27 FUNCTION_CALL_BIND bind EXACT bound.js:6:3 +bound.js:29:37 FUNCTION_CALL_BIND bind EXACT bound.js:6:3 +bound.js:34:17 FUNCTION_CALL_BIND bind EXACT bound.js:33:1 +bound.js:34:38 CONSTRUCTOR_CALL Repo SYNTHESIZED_OK +bound.js:39:24 FUNCTION_CALL_BIND bind EXACT bound.js:37:12 +bound.js:39:55 CONSTRUCTOR_CALL Other SYNTHESIZED_OK +bound.js:43:34 FUNCTION_CALL_CALL call EXACT bound.js:42:1 +bound.js:43:55 CONSTRUCTOR_CALL Repo SYNTHESIZED_OK +bound.js:44:18 FUNCTION_CALL_BIND bind EXACT bound.js:6:3 +bound.js:47:17 METHOD_CALL promisify EXACT bound.js:46:15 +# defects: 0 diff --git a/graph/test/python/cases/08-callables/src/main.py b/graph/test/python/cases/08-callables/src/main.py index 80ac9186e..baa116aaa 100644 --- a/graph/test/python/cases/08-callables/src/main.py +++ b/graph/test/python/cases/08-callables/src/main.py @@ -9,7 +9,8 @@ partial functools.partial exposes `.func`, so CPython can name the target lambda stored in a dict, reached by subscript: no name at the call site attribute a plain function assigned to an instance attribute, which does NOT - go through the descriptor protocol and so is not a bound method + go through the descriptor protocol and so is not a bound method; + a partial stored there calls its target the same way """ import functools @@ -31,10 +32,16 @@ class Holder: def __init__(self): # A function on an INSTANCE attribute. Not a method: no `self` is bound. self.op = add + self.bump = functools.partial(add, 1) + # control: a partial over a builtin stays the platform's + self.biggest = functools.partial(max, 0) def use(self): return self.op(1, 2) + def use_partial(self): + return self.bump(2) + self.biggest(3) + TABLE = { # A lambda reached by subscript. There is no name to resolve. @@ -52,6 +59,7 @@ def main(): print(TABLE["double"](21)) print(Holder().use()) + print(Holder().use_partial()) if __name__ == "__main__": diff --git a/graph/test/python/expected/08-callables.edges b/graph/test/python/expected/08-callables.edges index a89f21f40..5b0c619d2 100644 --- a/graph/test/python/expected/08-callables.edges +++ b/graph/test/python/expected/08-callables.edges @@ -1,7 +1,11 @@ +ambiguous_unknown SELF_CALL main.Holder.use_partial -> - +boundary_lib METHOD_CALL main.Holder.__init__ -> external:functools.partial boundary_lib METHOD_CALL main.main -> external:functools.partial boundary_lib SIMPLE_CALL main.main -> builtin:print known_edge CHAINED_CALL main.main -> main.Holder.use +known_edge CHAINED_CALL main.main -> main.Holder.use_partial known_edge SELF_CALL main.Holder.use -> main.add +known_edge SELF_CALL main.Holder.use_partial -> main.add known_edge SIMPLE_CALL main. -> main.main known_edge SIMPLE_CALL main.main -> main.Holder.__init__ known_edge SIMPLE_CALL main.main -> main.Multiplier.__call__ diff --git a/graph/test/python/expected/08-callables.tiers b/graph/test/python/expected/08-callables.tiers index 8c81e90ff..9d83aad23 100644 --- a/graph/test/python/expected/08-callables.tiers +++ b/graph/test/python/expected/08-callables.tiers @@ -1,26 +1,28 @@ -distinct call sites emitted: 13 +distinct call sites emitted: 20 --- by tier: edge ROWS, and the distinct SITES they cover --- - 5 rows 5 sites boundary_lib - 8 rows 8 sites known_edge + 1 rows 1 sites ambiguous_unknown + 8 rows 8 sites boundary_lib + 11 rows 11 sites known_edge --- edge rows by call kind --- - 1 CHAINED_CALL - 1 METHOD_CALL - 1 SELF_CALL - 9 SIMPLE_CALL + 2 CHAINED_CALL + 3 METHOD_CALL + 3 SELF_CALL + 11 SIMPLE_CALL 1 SUBSCRIPT_CALL --- unresolved reasons --- - (none — every site resolved) + 1 no_rule --- the engine's own conservation ledger --- - 13 _total_sites - 5 boundary_lib - 8 known_edge + 20 _total_sites + 1 ambiguous_unknown + 8 boundary_lib + 11 known_edge --- reconciling rows against the conserved site count --- - edge rows 13 + edge rows 20 minus extra rows from multi-target sites 0 - = tier/site pairs 13 - engine's conserved site total 13 + = tier/site pairs 20 + engine's conserved site total 20 diff --git a/graph/test/typescript/cases/84-declared-this-parameter/src/x.ts b/graph/test/typescript/cases/84-declared-this-parameter/src/x.ts new file mode 100644 index 000000000..e462aa74d --- /dev/null +++ b/graph/test/typescript/cases/84-declared-this-parameter/src/x.ts @@ -0,0 +1,17 @@ +class Repo { append(x: number): number { return x; } } +class Other { append(x: number): number { return x; } } +interface Ctx { repo: Repo } + +// A declared `this` parameter is what `this` is inside the function, whoever binds it. +function onEvent(this: Ctx, e: number) { return this.repo.append(e); } +function onShape(this: { other: Other }, e: number) { return this.other.append(e); } +const h = onEvent.bind({ repo: new Repo() }); +const s = onShape.bind({ other: new Other() }); + +// controls: an untyped `this` stays unknown; a class member's `this` is still its class +function onUntyped(this: any, e: number) { return this.repo.append(e); } +class Owner { + repo = new Repo(); + run(e: number) { return this.repo.append(e); } +} +export { h, s, onUntyped, Owner }; diff --git a/graph/test/typescript/expected/84-declared-this-parameter.edges b/graph/test/typescript/expected/84-declared-this-parameter.edges new file mode 100644 index 000000000..184ea7af5 --- /dev/null +++ b/graph/test/typescript/expected/84-declared-this-parameter.edges @@ -0,0 +1,9 @@ +ambiguous_unknown METHOD_CALL x#() @L8 -> - +ambiguous_unknown METHOD_CALL x#() @L9 -> - +ambiguous_unknown METHOD_CALL x#onUntyped(number) @L12 -> - +known_edge CONSTRUCTOR_CALL x#() @L14 -> Repo#() +known_edge CONSTRUCTOR_CALL x#() @L8 -> Repo#() +known_edge CONSTRUCTOR_CALL x#() @L9 -> Other#() +known_edge METHOD_CALL Owner#run(number) @L15 -> Repo#append(number) +known_edge METHOD_CALL x#onEvent(number) @L6 -> Repo#append(number) +known_edge METHOD_CALL x#onShape(number) @L7 -> Other#append(number) diff --git a/graph/test/typescript/expected/84-declared-this-parameter.entries b/graph/test/typescript/expected/84-declared-this-parameter.entries new file mode 100644 index 000000000..8e41e4b47 --- /dev/null +++ b/graph/test/typescript/expected/84-declared-this-parameter.entries @@ -0,0 +1,3 @@ +── entry_point (2) ── + exported_from_entry_module x#onUntyped x.ts:12 + unimported_module x# x.ts:1 diff --git a/graph/test/typescript/expected/84-declared-this-parameter.fields b/graph/test/typescript/expected/84-declared-this-parameter.fields new file mode 100644 index 000000000..de3e89d91 --- /dev/null +++ b/graph/test/typescript/expected/84-declared-this-parameter.fields @@ -0,0 +1,4 @@ +ambiguous_unknown read x#onUntyped(number) -> - +known_edge read Owner#run(number) -> Owner#repo +known_edge read x#onEvent(number) -> Ctx#repo +known_edge read x#onShape(number) -> { other: Other }#other diff --git a/graph/test/typescript/expected/84-declared-this-parameter.fields-oracle b/graph/test/typescript/expected/84-declared-this-parameter.fields-oracle new file mode 100644 index 000000000..0a3c5b6fb --- /dev/null +++ b/graph/test/typescript/expected/84-declared-this-parameter.fields-oracle @@ -0,0 +1,9 @@ +84-declared-this-parameter [fields] + precision 0.6667 (2 correct, 1 wrong) + recall 0.6667 (2 of 3 the compiler resolved) + sites 4 resolved 3 (75.0%) + tiers ambiguous_unknown=1 known_edge=3 + access read=4 + not scored: 1 rows whose target is not a client declaration + WRONG x#onShape(number) READ { other: Other }#other + MISSING x#onShape(number) READ x#other diff --git a/graph/test/typescript/expected/84-declared-this-parameter.oracle b/graph/test/typescript/expected/84-declared-this-parameter.oracle new file mode 100644 index 000000000..6c4bb5cbb --- /dev/null +++ b/graph/test/typescript/expected/84-declared-this-parameter.oracle @@ -0,0 +1 @@ +oracle=5 engine=5 agree=5 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/84-declared-this-parameter.type-use b/graph/test/typescript/expected/84-declared-this-parameter.type-use new file mode 100644 index 000000000..f183c671d --- /dev/null +++ b/graph/test/typescript/expected/84-declared-this-parameter.type-use @@ -0,0 +1,6 @@ +known_edge FIELD_TYPE 0 Ctx [FIELD] -> Repo +known_edge FIELD_TYPE 1 x [METHOD_PARAM] -> Other +known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> Ctx +known_edge OBJECT_CREATION_TYPE 0 Owner [EXPRESSION] -> Repo +known_edge OBJECT_CREATION_TYPE 0 x [EXPRESSION] -> Other +known_edge OBJECT_CREATION_TYPE 0 x [EXPRESSION] -> Repo diff --git a/graph/test/typescript/expected/84-declared-this-parameter.types-oracle b/graph/test/typescript/expected/84-declared-this-parameter.types-oracle new file mode 100644 index 000000000..d0200fc91 --- /dev/null +++ b/graph/test/typescript/expected/84-declared-this-parameter.types-oracle @@ -0,0 +1,7 @@ +84-declared-this-parameter [types] + precision 1.0000 (5 correct, 0 wrong) + recall 1.0000 (5 of 5 the compiler resolved) + sites 6 resolved 6 (100.0%) + tiers known_edge=6 + contexts FIELD_TYPE=2 METHOD_PARAM=1 OBJECT_CREATION_TYPE=3 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/ground-truth/tsc-program.mjs b/graph/test/typescript/ground-truth/tsc-program.mjs index ef0341117..907c664c2 100644 --- a/graph/test/typescript/ground-truth/tsc-program.mjs +++ b/graph/test/typescript/ground-truth/tsc-program.mjs @@ -204,7 +204,10 @@ export function loadProgram(srcDir, libDir, toolName, programDir) { const mods = ts.canHaveModifiers(decl) ? (ts.getModifiers(decl) ?? []) : []; if (mods.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)) name = 'static ' + name; - const ps = (decl.parameters ?? []).map((param) => { + // A declared `this` parameter types `this`, it is no argument: the engine's label + // leaves it out, so this one does too. + const ps = (decl.parameters ?? []).filter((param) => + !(ts.isIdentifier(param.name) && param.name.text === 'this')).map((param) => { let t = param.type ? param.type.getText(sf) : '?'; if (param.dotDotDotToken && !t.endsWith('[]')) t += '[]'; return simple(t); diff --git a/graph/typescript/engine/expression-resolution/expr-type.dl b/graph/typescript/engine/expression-resolution/expr-type.dl index 7e0a11fc4..c049ce6d2 100644 --- a/graph/typescript/engine/expression-resolution/expr-type.dl +++ b/graph/typescript/engine/expression-resolution/expr-type.dl @@ -169,6 +169,16 @@ expr_type(e, "client", t) :- expr_referenced("client", "THIS", _, e), expr_enclosing_type(e, t). expr_type(e, "client", t) :- expr_kind("client", "THIS_REFERENCE", _, e), expr_enclosing_type(e, t). +// A declared `this` parameter (`function onEvent(this: Ctx, e: Evt)`) is what `this` is in +// that function, whoever binds it (`onEvent.bind(ctx)`, `.call(ctx)`, a framework): the +// compiler types `this.repo` from it, and without it the site had no receiver type at all. +expr_type(e, prov, t) :- this_expr(e), expr_enclosing_method(e, m), method_declared_this(m, p), + param_type(p, prov, t). +expr_shape(e, s) :- this_expr(e), expr_enclosing_method(e, m), method_declared_this(m, p), + param_shape_target(p, s). +this_expr(e) :- expr_referenced("client", "THIS", _, e). +this_expr(e) :- expr_kind("client", "THIS_REFERENCE", _, e). +method_declared_this(m, p) :- param_shape("client", "THIS", _, _, p), param_decl("client", _, _, _, m, p). // `this` INSIDE A STATIC METHOD IS THE CLASS, so its members are the STATICS. A helper // class that calls its own statics through `this` is ordinary TypeScript — diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl index 36d89cb37..1d7675e05 100644 --- a/graph/typescript/souffle/decls_all.dl +++ b/graph/typescript/souffle/decls_all.dl @@ -370,6 +370,8 @@ .decl method_signature_role(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl method_signature_role_of(c0:symbol,c1:symbol) .decl method_this_param(c0:symbol) +.decl this_expr(c0:symbol) +.decl method_declared_this(c0:symbol,c1:symbol) .decl module_ambient_specifier(c0:symbol,c1:symbol,c2:symbol) .decl module_decl(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol) .decl module_default_export(c0:symbol,c1:symbol,c2:symbol)