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
28 changes: 28 additions & 0 deletions graph/javascript/engine/call-edge-generation/callbacks.dl
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,34 @@ options_value_kind("obj").
call_has_client_target(ce) :- expr_resolves_to_method(ce, m), method_prov(m, "client").
call_has_client_target(ce) :- new_constructs(ce, t), type_decl("client", _, _, _, _, t).

// A function WRAPPED BY A PACKAGE CALL and kept in a binding: `export const plugin = fp(async (app) => …)`,
// `const ext = Prisma.defineExtension((client) => …)`, then `app.register(plugin)`, `client.$extends(ext)`.
// The package returns the function it was handed, or one that runs it, and nothing in the project says
// which, so the binding had no value and the registration reached nothing, although the same literal
// handed to it bare is registered. The wrapping site is a value of its own, ("libwrap", site), carried
// wherever a value goes (a const, an import, an export), and a registration handed it registers what the
// site was handed. The site is recognised syntactically, by a callee rooted at a binding imported from a
// package, so the value stays below the resolver's negations; a project wrapper is followed through its
// body instead (value-flow.dl, "wrap").
lib_rooted(e) :- expr_kind(_, "IDENTIFIER", _, e), expr_binding(_, v, e), var_import(_, imp, v),
import_decl(_, _, _, _, _, _, out, _, imp), import_outcome_is_package(out).
lib_rooted(e) :- expr_kind(_, k, _, e), access_kind_reads_member(k), expr_child(_, e, "ACCESS_TARGET", _, r), lib_rooted(r).
import_outcome_is_package("RESOLVED_EXTERNAL").
import_outcome_is_package("UNRESOLVED_MISSING").
access_kind_reads_member("PROPERTY_ACCESS").
access_kind_reads_member("OPTIONAL_ACCESS").
lib_wrap_site(s) :- call_site(_, ck, _, _, _, _, s, _, _), call_kind_is_callee_form(ck),
expr_child(_, s, "CALLEE", _, c), lib_rooted(c).
lib_wrap_site(s) :- call_site(_, ck, _, "SYNTACTIC", _, _, s, _, _), call_kind_is_member_form(ck),
expr_child(_, s, "RECEIVER", _, r), lib_rooted(r).
// Handed over by NAME: `register(wrap(f))` needs nothing new, since the inner site already registers f
// from the same caller. The value is not a callee (callee-resolution.dl): what a call of it runs is still
// unknown, and says so.
expr_value(s, "libwrap", s) :- lib_wrap_site(s), call_arg(s, _, a), expr_value(a, "func", _).
callback_registered(ce, m) :- invocation_site(ce, _), !call_has_client_target(ce), !reflective_site(ce),
call_arg(ce, _, arg), !expr_kind(_, "CALL", _, arg),
expr_value(arg, "libwrap", s), call_arg(s, _, a), expr_value(a, "func", m).

// A listener runs with the EMITTER as `this` (`e.on('x', function () { this.other(); })`).
this_value(m, k, i) :- event_handler(k, i, _, m), method_this_binding(_, "DYNAMIC", m), !method_owner_type(m, _), k != "module".

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

// ── callee_value(CallExpr, K, I) — the value being invoked ─────────────────
callee_value(ce, k, i) :- call_site(_, ck, _, _, _, _, ce, _, _), call_kind_is_callee_form(ck),
expr_child(_, ce, "CALLEE", _, c), expr_value(c, k, i).
expr_child(_, ce, "CALLEE", _, c), expr_value(c, k, i), k != "libwrap". # what a package wrapper returns runs nothing known (callbacks.dl)
// `f.call(o)`: f runs. But the parser classifies by NAME, so `selector.apply(node)`
// on an object with its own `apply` method is here too — and for that reading the
// CALLEE child (the object) is the receiver and its member is the target. Both
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 @@ -451,6 +451,10 @@
.decl options_object_member(c0:symbol, c1:symbol, c2:symbol, c3:symbol)
.decl options_value_kind(c0:symbol)
.decl call_has_client_target(c0:symbol)
.decl lib_rooted(c0:symbol)
.decl import_outcome_is_package(c0:symbol)
.decl access_kind_reads_member(c0:symbol)
.decl lib_wrap_site(c0:symbol)
.decl callback_registered(c0:symbol, c1:symbol)
.decl event_handler(c0:symbol, c1:symbol, c2:symbol, c3:symbol)
.decl event_register_method(c0:symbol)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ function viaCtor() { return new Transform({ transform }); }
function viaProject() { return localWalk([1], { filter: keep }); }
function main() { direct(); viaVar(); viaPlatform(); viaCtor(); viaProject(); }
main();
// a function wrapped by a package call and kept in a const, then handed to a package registration: registered
function migrate() { return 1; }
const plugin = walk(async () => migrate());
const settings = walk(42);
function viaWrapped() { walk.register(plugin); walk.register(settings); }
module.exports = { viaWrapped };
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ unresolved main.js:12:86 METHOD_CALL destroy no_target
unresolved main.js:12:86 METHOD_CALL on no_target
unresolved main.js:12:86 METHOD_CALL request no_target
unresolved main.js:13:29 CONSTRUCTOR_CALL Transform no_target
unresolved main.js:19:16 FUNCTION_CALL walk callee_untyped
unresolved main.js:20:18 FUNCTION_CALL walk callee_untyped
unresolved main.js:21:25 METHOD_CALL register receiver_untyped
unresolved main.js:21:48 METHOD_CALL register receiver_untyped
unresolved main.js:8:38 FUNCTION_CALL cb callee_untyped
unresolved main.js:9:42 METHOD_CALL map no_target
value_callee main.js:8:38 cb parameter
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ main.js:15:39 FUNCTION_CALL viaPlatform -> known_edge main.js:12:1 viaPlatfor
main.js:15:54 FUNCTION_CALL viaCtor -> known_edge main.js:13:1 viaCtor
main.js:15:65 FUNCTION_CALL viaProject -> known_edge main.js:14:1 viaProject
main.js:16:1 FUNCTION_CALL main -> known_edge main.js:15:1 main
main.js:19:16 FUNCTION_CALL walk -> ambiguous_unknown -
main.js:19:16 FUNCTION_CALL walk -> callback_registered main.js:19:21 <arrow>
main.js:19:33 FUNCTION_CALL migrate -> known_edge main.js:18:1 migrate
main.js:20:18 FUNCTION_CALL walk -> ambiguous_unknown -
main.js:21:25 METHOD_CALL walk.register -> ambiguous_unknown -
main.js:21:25 METHOD_CALL walk.register -> callback_registered main.js:19:21 <arrow>
main.js:21:48 METHOD_CALL walk.register -> ambiguous_unknown -
main.js:8:38 FUNCTION_CALL cb -> ambiguous_unknown -
main.js:9:42 METHOD_CALL items.map -> ambient_terminal -
main.js:9:42 METHOD_CALL items.map -> callback_registered main.js:9:52 <arrow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ unresolved main.js:12:86 METHOD_CALL destroy no_target
unresolved main.js:12:86 METHOD_CALL on no_target
unresolved main.js:12:86 METHOD_CALL request no_target
unresolved main.js:13:29 CONSTRUCTOR_CALL Transform no_target
unresolved main.js:21:25 METHOD_CALL register member_absent
unresolved main.js:21:48 METHOD_CALL register member_absent
unresolved main.js:8:38 FUNCTION_CALL cb callee_untyped
unresolved main.js:9:42 METHOD_CALL map no_target
value_callee main.js:8:38 cb parameter
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ main.js:15:39 FUNCTION_CALL viaPlatform -> known_edge main.js:12:1 viaPlatfor
main.js:15:54 FUNCTION_CALL viaCtor -> known_edge main.js:13:1 viaCtor
main.js:15:65 FUNCTION_CALL viaProject -> known_edge main.js:14:1 viaProject
main.js:16:1 FUNCTION_CALL main -> known_edge main.js:15:1 main
main.js:19:16 FUNCTION_CALL walk -> boundary_lib lib:index.js:2:1 walk
main.js:19:16 FUNCTION_CALL walk -> callback_registered main.js:19:21 <arrow>
main.js:19:33 FUNCTION_CALL migrate -> known_edge main.js:18:1 migrate
main.js:20:18 FUNCTION_CALL walk -> boundary_lib lib:index.js:2:1 walk
main.js:21:25 METHOD_CALL walk.register -> ambiguous_unknown -
main.js:21:25 METHOD_CALL walk.register -> callback_registered main.js:19:21 <arrow>
main.js:21:48 METHOD_CALL walk.register -> ambiguous_unknown -
main.js:8:38 FUNCTION_CALL cb -> ambiguous_unknown -
main.js:9:42 METHOD_CALL items.map -> ambient_terminal -
main.js:9:42 METHOD_CALL items.map -> callback_registered main.js:9:52 <arrow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ main.js:15:39 FUNCTION_CALL viaPlatform EXACT main.js:12:1
main.js:15:54 FUNCTION_CALL viaCtor EXACT main.js:13:1
main.js:15:65 FUNCTION_CALL viaProject EXACT main.js:14:1
main.js:16:1 FUNCTION_CALL main EXACT main.js:15:1
main.js:19:33 FUNCTION_CALL migrate EXACT main.js:18:1
# defects: 0
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ components/List.svelte:5:39 FUNCTION_CALL fetchProducts -> known_edge lib/for
components/List.svelte:6:29 FUNCTION_CALL formatPrice -> known_edge lib/format.js:3:1 formatPrice
components/Price.vue:1:25 FUNCTION_CALL onClick -> known_edge components/Price.vue:8:1 onClick
components/Price.vue:1:50 FUNCTION_CALL onlyInMarkup -> ambiguous_unknown -
components/Price.vue:1:50 FUNCTION_CALL onlyInMarkup -> callback_registered components/Price.vue:7:24 <arrow>
components/Price.vue:6:15 FUNCTION_CALL defineProps -> ambiguous_unknown -
components/Price.vue:7:15 FUNCTION_CALL computed -> ambiguous_unknown -
components/Price.vue:7:15 FUNCTION_CALL computed -> callback_registered components/Price.vue:7:24 <arrow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,22 @@ export function each(xs: number[], fn: (n: number) => void): void {
export function useEach(xs: number[]): void {
each(xs, (n) => record(n + RATE))
}

// a host registration with no body: the const handed to it holds a library-wrapped function literal, and
// registering the const reaches that literal as registering the literal bare does
import { migrate, plugin, settings } from './plugin'
declare const host: { register(p: unknown): void }

export function boot(): void {
host.register(plugin)
}

// CONTROL: the literal handed bare, already reached from the site that hands it
export function bootBare(): void {
host.register(async () => migrate())
}

// CONTROL: a const with no function in it registers nothing
export function bootSettings(): void {
host.register(settings)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// A FUNCTION WRAPPED BY A LIBRARY CALL AND KEPT IN A CONST. `wrap` is a declaration only, so the const holds
// what a library returns; the function literal it was handed is what a registration of the const runs.
declare function wrap<F>(f: F): F

export function migrate(): void {}

export const plugin = wrap(async () => {
migrate()
})

// CONTROL: a const holding a plain value built by a library call hands over no function
export const settings = wrap(42)
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
ambient_terminal FUNCTION_CALL app#atStartup() @L38 -> app#onReady(() =)
ambient_terminal FUNCTION_CALL plugin#<module-init>() @L12 -> plugin#wrap(T)
ambient_terminal FUNCTION_CALL plugin#<module-init>() @L7 -> plugin#wrap(T)
callback_registered FUNCTION_CALL app#atStartup() @L38 -> app#<arrow@38>()
callback_registered FUNCTION_CALL plugin#<module-init>() @L7 -> plugin#<arrow@7>()
callback_registered METHOD_CALL app#boot() @L57 -> plugin#<arrow@7>()
callback_registered METHOD_CALL app#bootBare() @L62 -> app#<arrow@62>()
callback_registered METHOD_CALL app#doubled(number[]) @L16 -> app#<arrow@16>(?)
callback_registered METHOD_CALL app#named(number[]) @L25 -> app#double(number)
callback_registered METHOD_CALL app#scaled(number[]) @L12 -> app#<arrow@12>(?)
callback_registered METHOD_CALL app#viaLocal(number[]) @L31 -> app#log(number)
known_edge FUNCTION_CALL app#<arrow@38>() @L38 -> app#record(number)
known_edge FUNCTION_CALL app#<arrow@48>(?) @L48 -> app#record(number)
known_edge FUNCTION_CALL app#<arrow@62>() @L62 -> plugin#migrate()
known_edge FUNCTION_CALL app#each(number[],(n: number) =) @L44 -> app#<arrow@43>(number)
known_edge FUNCTION_CALL app#log(number) @L30 -> app#record(number)
known_edge FUNCTION_CALL app#useEach(number[]) @L48 -> app#each(number[],(n: number) =)
known_edge FUNCTION_CALL plugin#<arrow@7>() @L8 -> plugin#migrate()
known_edge METHOD_CALL app#boot() @L57 -> app#register(unknown)
known_edge METHOD_CALL app#bootBare() @L62 -> app#register(unknown)
known_edge METHOD_CALL app#bootSettings() @L67 -> app#register(unknown)
known_edge METHOD_CALL app#doubled(number[]) @L16 -> Array#map((v: T) =)
known_edge METHOD_CALL app#named(number[]) @L25 -> Array#map((v: T) =)
known_edge METHOD_CALL app#scaled(number[]) @L12 -> Array#map((v: T) =)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
── entry_point (9) ──
── entry_point (12) ──
exported_from_entry_module app#atStartup app.ts:37
exported_from_entry_module app#boot app.ts:56
exported_from_entry_module app#bootBare app.ts:61
exported_from_entry_module app#bootSettings app.ts:66
exported_from_entry_module app#doubled app.ts:15
exported_from_entry_module app#each app.ts:43
exported_from_entry_module app#named app.ts:24
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
oracle=10 engine=10 agree=10 missing=0 (known 0, NEW 0) extra=0
oracle=16 engine=16 agree=16 missing=0 (known 0, NEW 0) extra=0
28 changes: 28 additions & 0 deletions graph/typescript/engine/resolution/value-flow.dl
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,34 @@ handed_function(ce, m) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUM
value_branch(a, x),
method_value(x, m).

// …AND WHAT A LIBRARY CALL WRAPPED, KEPT IN A HOLDER. `export const plugin = fp(async (app) => …)`, then
// `app.register(plugin)`: the holder keeps what a call with no client body RETURNED, so no holder rule above
// sees a function in it, and the registration reached nothing although the same literal handed to it bare
// is reached. A library wrapper hands back the function it was handed, or one that runs it (`fp`,
// `defineExtension`, `debounce`), so the function the wrapping site was handed is what the holder hands on.
// Only a wrapping site at the library boundary: a project wrapper has a body, and what it returns is
// followed through that body (fn_value_call, below). The const is named here or imported from the
// module that wraps it (expr_holder), which is the usual shape: a plugin module, a registering app.
handed_function(ce, m) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a),
value_branch(a, x),
expr_holder(x, h),
holder_wraps_handed_function(h, m).
// A const's own initializer only, and only a call of a declaration, not of a value the project holds: a
// reassignment (`state = createState(set, get)`) and a call through a parameter hand back what the project
// function returns, which is data as often as it is the function it was handed.
// And not a call whose result the types say is data: `setTimeout(cb)` returns a timer, `xs.filter(cb)` an
// array, and `clearTimeout(timer)` runs nothing. A result typed with no call signature is data; an untyped
// one (a package with no types staged) or a callable one (`fp`'s plugin type, a mock) is kept.
holder_wraps_handed_function(h, m) :- var_initializer("client", _, e, h), e != "",
value_branch(e, w),
expr_kind("client", "CALL_EXPRESSION", _, w),
hof_boundary_site(w),
!called_through(w, _),
!call_returns_data(w),
handed_function(w, m).
call_returns_data(w) :- expr_type(w, _, t), !call_signature_in_scope(t, _), !call_result_callable(w).
call_result_callable(w) :- expr_shape(w, s), call_signature_in_scope(s, _).

// ── method_value(Expr, Method): an INSTANCE method read as a value, not called ──
// `xs.forEach(this.handle, this)`, `el.addEventListener('click', this.onClick)`,
// `setTimeout(this.onHover.bind(this))`. expr_callable names a free function, an import and
Expand Down
3 changes: 3 additions & 0 deletions graph/typescript/souffle/decls_all.dl
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@
.decl call_runs_client_body(c0:symbol)
.decl hof_boundary_site(c0:symbol)
.decl handed_function(c0:symbol,c1:symbol)
.decl holder_wraps_handed_function(c0:symbol,c1:symbol) // (holder, fn): the holder keeps what a library call returned, and fn was handed to that call
.decl call_returns_data(c0:symbol) // the call's result is typed, and the type has no call signature
.decl call_result_callable(c0:symbol)
.decl method_value(c0:symbol,c1:symbol)
.decl method_is_accessor(c0:symbol)
.decl called_through(c0:symbol,c1:symbol)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,46 @@
"no chain of resolved calls"
]
},
{
"why": "a function literal wrapped by a library call and kept in a const (`const plugin = wrap(async () => …)`), then handed to a library registration (`host.register(plugin)`), is reached from the function that registers it, as the literal handed bare is; it was 'the two are independent in this graph'",
"run": [
"path",
"boot",
"migrate"
],
"want": [
"1 of 1 target(s) reached",
"[callback_registered"
],
"avoid": [
"the two are independent"
]
},
{
"why": "CONTROL: the same literal handed to the registration bare is reached as before",
"run": [
"path",
"bootBare",
"migrate"
],
"want": [
"1 of 1 target(s) reached",
"[callback_registered"
]
},
{
"why": "CONTROL: a const holding what a library call built from a plain value registers no function",
"run": [
"path",
"bootSettings",
"migrate"
],
"want": [],
"avoid": [
"1 of 1 target(s) reached"
],
"expect_error": true
},
{
"why": "CONTROL: a project higher-order function has a body that calls its parameter, so the callback it is handed is still reached through that call",
"run": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,22 @@ export function each(xs: number[], fn: (n: number) => void): void {
export function useEach(xs: number[]): void {
each(xs, (n) => record(n + RATE))
}

// a host registration with no body: the const handed to it holds a library-wrapped function literal, and
// registering the const reaches that literal as registering the literal bare does
import { migrate, plugin, settings } from './plugin'
declare const host: { register(p: unknown): void }

export function boot(): void {
host.register(plugin)
}

// CONTROL: the literal handed bare, already reached from the site that hands it
export function bootBare(): void {
host.register(async () => migrate())
}

// CONTROL: a const with no function in it registers nothing
export function bootSettings(): void {
host.register(settings)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// A FUNCTION WRAPPED BY A LIBRARY CALL AND KEPT IN A CONST. `wrap` is a declaration only, so the const holds
// what a library returns; the function literal it was handed is what a registration of the const runs.
declare function wrap<F>(f: F): F

export function migrate(): void {}

export const plugin = wrap(async () => {
migrate()
})

// CONTROL: a const holding a plain value built by a library call hands over no function
export const settings = wrap(42)
Loading