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
31 changes: 31 additions & 0 deletions graph/javascript/engine/call-edge-generation/calls.dl
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,37 @@ module_variable_from_call(v) :- var_init("client", _, e, v), expr_kind(_, k, _,
!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).
// `promisify(store.find.bind(store))`: a `.bind` always evaluates to a function, typed
// receiver or not. A bound platform function (`Math.max.bind(Math)`) is the platform's.
call_passes_function(e) :- call_arg(e, _, a), call_site(_, "FUNCTION_CALL_BIND", _, _, _, _, a, _, _),
expr_child(_, a, "CALLEE", _, f), !bound_platform_function(f).
bound_platform_function(f) :- expr_kind(_, "PROPERTY_ACCESS", _, f), expr_child(_, f, "ACCESS_TARGET", _, r),
expr_value(r, "ambient", _).
// A holder whose value is what a PLATFORM call returned when handed a project function
// (`this.find = promisify(s.find.bind(s))`, `const f = util.callbackify(g)`): the value
// is a wrapper the platform made around that function, and calling it runs the
// function. The platform value on the holder made the call an ambient terminal, a
// claimed correct end, and path said "independent" of the very method the wrapper
// runs. A platform value made from no project function (`promisify(setTimeout)`)
// keeps its platform reading.
// When the graph knows the function the wrapper was made from (a function value, or
// the method a `.bind` site resolves to), the call runs it: one of a set, beside the
// platform row the call keeps. Only when it knows none is the callee an open value.
unresolved_value_callee(ce, "field") :- wrapper_holder_call(ce, val), !wrapper_call_target(ce, _), made_from_function(val),
field_call(ce, _, _, _).
unresolved_value_callee(ce, "module_variable") :- wrapper_holder_call(ce, e), !wrapper_call_target(ce, _), made_from_function(e),
!field_call(ce, _, _, _).
wrapper_holder_call(ce, val) :- field_call(ce, k, t, n), !call_resolved(ce), field_assignment(k, t, n, val).
wrapper_holder_call(ce, e) :- value_callee_unresolved(ce, c), expr_binding(_, v, c),
var_decl("client", _, _, _, _, _, v), !var_owner_method("client", _, v), !var_import(_, _, v), !expr_param(_, _, c),
var_init("client", _, e, v), !variable_reassigned(v).
made_from_function(e) :- expr_kind(_, "CALL", _, e), expr_value(e, "ambient", _), call_passes_function(e).
wrapper_runs(e, m) :- made_from_function(e), call_arg(e, _, a), expr_value(a, "func", m).
wrapper_runs(e, m) :- made_from_function(e), call_arg(e, _, a), call_site(_, "FUNCTION_CALL_BIND", _, _, _, _, a, _, _),
expr_resolves_to_method(a, m).
wrapper_call_target(ce, m) :- wrapper_holder_call(ce, e), wrapper_runs(e, m), call_target_count(ce, 0).
call_chain_edge(ce, caller, "-", m, "client", "multi_inferred", kind) :-
wrapper_call_target(ce, m), !call_over_cap(ce), call_from(ce, caller), invocation_site(ce, kind).
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.
Expand Down
5 changes: 5 additions & 0 deletions graph/javascript/souffle/decls_all.dl
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@
.decl module_variable_from_call(c0:symbol)
.decl variable_reassigned(c0:symbol)
.decl call_passes_function(c0:symbol)
.decl bound_platform_function(c0:symbol)
.decl made_from_function(c0:symbol)
.decl wrapper_holder_call(c0:symbol, c1:symbol)
.decl wrapper_runs(c0:symbol, c1:symbol)
.decl wrapper_call_target(c0:symbol, c1:symbol)
.decl live_export_variable(c0:symbol, c1:symbol)
.decl this_type_open(c0:symbol)

Expand Down
30 changes: 24 additions & 6 deletions plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ def chain_json(g, chain):
# declarations that describe a callable and have no body (score.py's bodiless_kinds)
BODILESS_KINDS = {'METHOD_SIGNATURE', 'TYPE_LITERAL_METHOD_SIGNATURE', 'CALL_SIGNATURE', 'TYPE_LITERAL_CALL_SIGNATURE',
'FUNCTION_TYPE_SIGNATURE', 'CONSTRUCT_SIGNATURE', 'TYPE_LITERAL_CONSTRUCT_SIGNATURE', 'CONSTRUCTOR_TYPE_SIGNATURE'}
# of those, the ones a call through a VALUE lands on: a function type or a bare call signature, not a member of an interface
FUNCTION_TYPE_KINDS = {'FUNCTION_TYPE_SIGNATURE', 'CALL_SIGNATURE', 'TYPE_LITERAL_CALL_SIGNATURE'}
# the one name a front end gives every lambda it declares (Python and C#; Java declares none): a name that says nothing
# about WHICH lambda, so it is never a target on its own (G.lambda_label, G.lambda_target)
LAMBDA_NAMES = {'<lambda>'}
Expand Down Expand Up @@ -1767,8 +1769,18 @@ def path(g, a, b, show_all=False, limit=10, every=False, max_paths=20):
# `table[k]()`: nothing about the name narrows the target, so the by-name search above finds no lead and
# "independent" was printed for a start that hands control to whatever it was given. The engine marks such a
# site (`unresolved_value_callee`); one in either side's closure makes the connection unknown, not absent.
# A typed front end RESOLVES the same call, to the holder's function type (`find: (e: string) => …`), a signature
# with no body: the chain ends there, and the function the holder is given is what runs. Those count too.
opaque = []
if not (remote_found or lib_side or sends) and g.has('ext_unresolved_value_callee'):
value_sql = []
if g.has('ext_unresolved_value_callee'):
value_sql.append("SELECT s.caller_id c, s.callee_name n, s.file_path f, s.start_line ln, v.c1 why FROM ext_unresolved_value_callee v"
" JOIN call_sites s ON s.id = v.c0 WHERE s.caller_id IN (%s)")
if g.has('methods') and g.has('call_edges'):
value_sql.append("SELECT DISTINCT s.caller_id c, s.callee_name n, s.file_path f, s.start_line ln, 'function_type' why FROM call_edges e"
" JOIN call_sites s ON s.id = e.call_site_id JOIN methods m ON m.id = e.callee_method_id"
f" WHERE m.kind IN ({','.join(repr(k) for k in sorted(FUNCTION_TYPE_KINDS))}) AND s.caller_id IN (%s)")
if not (remote_found or lib_side or sends) and value_sql:
for xs, lx in ((A_, la), (B_, lb)):
seen = {i for i in xs if i in g.sym and g.sym[i]['kind'] not in ('library', 'written')}; fr = list(seen)
while fr:
Expand All @@ -1778,8 +1790,7 @@ def path(g, a, b, show_all=False, limit=10, every=False, max_paths=20):
ids = sorted(seen); rows = []
for i in range(0, len(ids), 900):
part = ids[i:i + 900]
rows += g.q("SELECT s.caller_id c, s.callee_name n, s.file_path f, s.start_line ln, v.c1 why FROM ext_unresolved_value_callee v"
" JOIN call_sites s ON s.id = v.c0 WHERE s.caller_id IN (%s)" % ','.join('?' * len(part)), *part)
for sql in value_sql: rows += g.q(sql % ','.join('?' * len(part)), *part)
if rows: opaque.append((lx, sorted(rows, key=lambda r: (g.site_file(r['f']), r['ln'] or 0))))
# …but a framework may still connect them, and the graph holds the evidence for it: the target is registered
# under a key, and the source writes that key. That is not a chain of calls, so it is reported and not walked.
Expand Down Expand Up @@ -1821,15 +1832,22 @@ def path(g, a, b, show_all=False, limit=10, every=False, max_paths=20):
'instance_member': 'a function stored on the instance from outside the class',
'parameter_member': 'a member of a parameter', 'expression': 'computed by an expression',
'getattr': 'an attribute looked up by a name computed at run time',
'imported_variable': "another module's exported variable, which that module reassigns"}
'imported_variable': "another module's exported variable, which that module reassigns",
'function_type': 'held by a field, variable or parameter of a function type, whatever function it is given'}
# A value callee that carries the OTHER endpoint's name (`this.find()` where the field holds a wrapper
# around find) is the likeliest connection: listed first, and said so.
names = {la: {g.sym[i]['name'] for i in B_ if i in g.sym}, lb: {g.sym[i]['name'] for i in A_ if i in g.sym}}
for lx, rows in opaque:
other = lb if lx == la else la
rows = sorted(rows, key=lambda r: r['n'] not in names[lx])
# the sites that make the answer unknown, in --json too: a consumer reading only `answers` saw "no chain"
RESULT['value_calls'] = RESULT.get('value_calls', []) + [
{'side': lx, 'caller': g.disp(r['c']), 'callee': r['n'] or '', 'at': f"{g.site_file(r['f'])}:{r['ln']}",
'callee_is': WHY.get(r['why'], r['why'])} for r in rows[:50]]
'callee_is': WHY.get(r['why'], r['why']), 'named_like_target': r['n'] in names[lx]} for r in rows[:50]]
print(f" {lx} reaches {len(rows)} call(s) through a value:")
for r in rows[:4]:
print(f" `{r['n'] or '[…]'}()` in {g.disp(r['c'])} at {g.site_file(r['f'])}:{r['ln']} — the callee is {WHY.get(r['why'], r['why'])}")
print(f" `{r['n'] or '[…]'}()` in {g.disp(r['c'])} at {g.site_file(r['f'])}:{r['ln']} — the callee is {WHY.get(r['why'], r['why'])}"
+ (f" — named like the target: if it holds {other}, the chain is real" if r['n'] in names[lx] else ''))
if len(rows) > 4: print(f" … +{len(rows) - 4} more")
for line in fw: print(line)
return 1
Expand Down
75 changes: 75 additions & 0 deletions tests/cases/javascript/stored-field-callee-shapes/case.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,81 @@
"avoid": [
"connection is UNKNOWN, not absent"
]
},
{
"why": "Svc.login calls this.find(), a field holding what a platform call (promisify) returned for a bound project method: the wrapper runs that method, so path says unknown and names the same-named site, never independent",
"run": [
"path",
"Svc.login",
"Store.find"
],
"expect_error": true,
"want": [
"connection is UNKNOWN, not absent",
"`find()` in Svc.login at src/shapes.js:42 — the callee is a function stored in a field",
"named like the target: if it holds Store.find, the chain is real"
],
"avoid": [
"the two are independent in this graph"
]
},
{
"why": "the same wrapper held in a module variable, bound to a method the graph resolves: the wrapper runs it, so the chain is real",
"run": [
"path",
"viaModule",
"Store.find"
],
"want": [
"→ [multi_inferred · call @ src/shapes.js:44] Store.find"
],
"avoid": [
"the two are independent in this graph"
]
},
{
"why": "a field wrapper whose bound receiver is typed by the argument its constructor is given resolves the same way",
"run": [
"path",
"Auth.login",
"Store.find"
],
"want": [
"→ [multi_inferred · call @ src/shapes.js:45] Store.find"
],
"avoid": []
},
{
"why": "control: a field holding what a platform call returned for a platform function (promisify(setTimeout)) is a platform call",
"run": [
"path",
"Clock.tick",
"Store.find"
],
"expect_error": true,
"want": [
"the two are independent in this graph"
],
"avoid": [
"connection is UNKNOWN, not absent",
"through a value"
]
},
{
"why": "control: a platform call handed a bound PLATFORM function (promisify(Math.max.bind(Math))) is a platform call",
"run": [
"path",
"Reader.go",
"Store.find"
],
"expect_error": true,
"want": [
"the two are independent in this graph"
],
"avoid": [
"connection is UNKNOWN, not absent",
"through a value"
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,20 @@ const tag = require('util').format;
function tags() { return tag('%s', 'x'); }
const wrapped = debug(alpha);
function callsWrapped() { return wrapped(); }
const { promisify } = require('util');
class Store { find(e) { return e; } }
class Svc { constructor(s) { this.find = promisify(s.find.bind(s)); } login(e) { return this.find(e); } }
const findAsync = promisify(new Store().find.bind(new Store()));
function viaModule(e) { return findAsync(e); }
class Auth { constructor(s) { this.find = promisify(s.find.bind(s)); } login(e) { return this.find(e); } }
function makeAuth() { return new Auth(new Store()); }
class Clock { constructor() { this.wait = promisify(setTimeout); } tick() { return this.wait(1); } }
class Reader { constructor() { this.read = promisify(Math.max.bind(Math)); } go() { return this.read(1); } }
// 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 };
module.exports = { alpha, FieldNull, CtorNull, Fallback, Static, StaticField, Alias, Maker, logs, tags, callsWrapped, Bus, Known, Fixed, useOwn, Store, Svc, viaModule, Auth, makeAuth, Clock, Reader };
17 changes: 17 additions & 0 deletions tests/cases/typescript/function-stored-in-a-holder/case.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,23 @@
"src/app.ts: "
],
"avoid": []
},
{
"why": "a call through a function-typed field the graph cannot fill (a library-made wrapper) ends on the type's signature: path says unknown, names the site and that it carries the target's name, never independent",
"run": [
"path",
"Svc.login",
"Store.find"
],
"expect_error": true,
"want": [
"connection is UNKNOWN, not absent",
"— the callee is held by a field, variable or parameter of a function type",
"named like the target: if it holds Store.find, the chain is real"
],
"avoid": [
"the two are independent in this graph"
]
}
]
}
14 changes: 14 additions & 0 deletions tests/cases/typescript/function-stored-in-a-holder/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,17 @@ export function price(registry: Registry, n: number): string {
export function checkout(registry: Registry): string {
return price(registry, 3)
}

import { promisify } from 'util'

export class Store {
find(e: string): string { return e }
}

// A field of a function type holding what a library returned for a bound method: the call resolves to the
// field's signature, which has no body; the wrapped method is what runs.
export class Svc {
private find: (e: string) => Promise<string>
constructor(s: Store) { this.find = promisify(s.find.bind(s)) as any }
login(e: string) { return this.find(e) }
}
Loading