Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions graph/javascript/engine/resolution/ambient.dl
Original file line number Diff line number Diff line change
Expand Up @@ -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").
Expand Down
17 changes: 17 additions & 0 deletions graph/javascript/engine/resolution/value-flow.dl
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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).
Expand Down
4 changes: 4 additions & 0 deletions graph/javascript/souffle/decls_all.dl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 7 additions & 8 deletions graph/python/engine/resolution/value-flow.dl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 };
11 changes: 11 additions & 0 deletions graph/test/javascript/expected/73-bound-function-identity.diag
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions graph/test/javascript/expected/73-bound-function-identity.edges
Original file line number Diff line number Diff line change
@@ -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 <constructor>
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 <arrow>
Original file line number Diff line number Diff line change
@@ -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
10 changes: 9 additions & 1 deletion graph/test/python/cases/08-callables/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand All @@ -52,6 +59,7 @@ def main():

print(TABLE["double"](21))
print(Holder().use())
print(Holder().use_partial())


if __name__ == "__main__":
Expand Down
4 changes: 4 additions & 0 deletions graph/test/python/expected/08-callables.edges
Original file line number Diff line number Diff line change
@@ -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.<module> -> main.main
known_edge SIMPLE_CALL main.main -> main.Holder.__init__
known_edge SIMPLE_CALL main.main -> main.Multiplier.__call__
Expand Down
30 changes: 16 additions & 14 deletions graph/test/python/expected/08-callables.tiers
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions graph/test/typescript/cases/84-declared-this-parameter/src/x.ts
Original file line number Diff line number Diff line change
@@ -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 };
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
ambiguous_unknown METHOD_CALL x#<module-init>() @L8 -> -
ambiguous_unknown METHOD_CALL x#<module-init>() @L9 -> -
ambiguous_unknown METHOD_CALL x#onUntyped(number) @L12 -> -
known_edge CONSTRUCTOR_CALL x#<module-init>() @L14 -> Repo#<new>()
known_edge CONSTRUCTOR_CALL x#<module-init>() @L8 -> Repo#<new>()
known_edge CONSTRUCTOR_CALL x#<module-init>() @L9 -> Other#<new>()
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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
── entry_point (2) ──
exported_from_entry_module x#onUntyped x.ts:12
unimported_module x#<module> x.ts:1
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
oracle=5 engine=5 agree=5 missing=0 (known 0, NEW 0) extra=0
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
5 changes: 4 additions & 1 deletion graph/test/typescript/ground-truth/tsc-program.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions graph/typescript/engine/expression-resolution/expr-type.dl
Original file line number Diff line number Diff line change
Expand Up @@ -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 —
Expand Down
Loading
Loading