From 57458ad43f503fbef64803198c94a411e45ab5c4 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:01:25 -0700 Subject: [PATCH] engine: a function wrapped by a library call and kept in a const is registered where the const is handed over TypeScript: a const whose initializer is a boundary call handed a function (fp(async (app) => ...), defineExtension(fn)) hands that function on to a library registration it is passed to, by name or through an import. Not a reassignment, not a call through a project value, not a call whose typed result has no call signature. JavaScript: the same, for a call rooted at a package import, carried as its own value kind that is never a callee. path answered 'independent' for a plugin registered this way although the bare form was reached. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../engine/call-edge-generation/callbacks.dl | 28 +++++++++++++ .../callee-resolution.dl | 2 +- graph/javascript/souffle/decls_all.dl | 4 ++ .../34-options-object-callbacks/src/main.js | 6 +++ .../expected/34-options-object-callbacks.diag | 4 ++ .../34-options-object-callbacks.edges | 7 ++++ .../34-options-object-callbacks.lib.diag | 2 + .../34-options-object-callbacks.lib.edges | 7 ++++ .../34-options-object-callbacks.oracle | 1 + .../70-single-file-component-scripts.edges | 1 + .../src/app.ts | 19 +++++++++ .../src/plugin.ts | 12 ++++++ .../78-hof-callback-at-library-boundary.edges | 10 +++++ ...8-hof-callback-at-library-boundary.entries | 5 ++- ...78-hof-callback-at-library-boundary.oracle | 2 +- .../engine/resolution/value-flow.dl | 28 +++++++++++++ graph/typescript/souffle/decls_all.dl | 3 ++ .../case.json | 40 +++++++++++++++++++ .../src/app.ts | 19 +++++++++ .../src/plugin.ts | 12 ++++++ 20 files changed, 209 insertions(+), 3 deletions(-) create mode 100644 graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/plugin.ts create mode 100644 tests/cases/typescript/hof-callback-at-library-boundary/src/plugin.ts diff --git a/graph/javascript/engine/call-edge-generation/callbacks.dl b/graph/javascript/engine/call-edge-generation/callbacks.dl index 107842dbd..6dbc84000 100644 --- a/graph/javascript/engine/call-edge-generation/callbacks.dl +++ b/graph/javascript/engine/call-edge-generation/callbacks.dl @@ -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". diff --git a/graph/javascript/engine/expression-resolution/callee-resolution.dl b/graph/javascript/engine/expression-resolution/callee-resolution.dl index c41600158..946d7cb34 100644 --- a/graph/javascript/engine/expression-resolution/callee-resolution.dl +++ b/graph/javascript/engine/expression-resolution/callee-resolution.dl @@ -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 diff --git a/graph/javascript/souffle/decls_all.dl b/graph/javascript/souffle/decls_all.dl index 1f91eb17b..902a468c8 100644 --- a/graph/javascript/souffle/decls_all.dl +++ b/graph/javascript/souffle/decls_all.dl @@ -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) diff --git a/graph/test/javascript/cases/34-options-object-callbacks/src/main.js b/graph/test/javascript/cases/34-options-object-callbacks/src/main.js index 0a29fdf73..baabbc0fc 100644 --- a/graph/test/javascript/cases/34-options-object-callbacks/src/main.js +++ b/graph/test/javascript/cases/34-options-object-callbacks/src/main.js @@ -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 }; diff --git a/graph/test/javascript/expected/34-options-object-callbacks.diag b/graph/test/javascript/expected/34-options-object-callbacks.diag index f261bb398..2937e0b0a 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.diag +++ b/graph/test/javascript/expected/34-options-object-callbacks.diag @@ -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 diff --git a/graph/test/javascript/expected/34-options-object-callbacks.edges b/graph/test/javascript/expected/34-options-object-callbacks.edges index 04390e7a1..2336e81fe 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.edges +++ b/graph/test/javascript/expected/34-options-object-callbacks.edges @@ -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 +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 +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 diff --git a/graph/test/javascript/expected/34-options-object-callbacks.lib.diag b/graph/test/javascript/expected/34-options-object-callbacks.lib.diag index 99d2f7ba0..72bf6bbbc 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.lib.diag +++ b/graph/test/javascript/expected/34-options-object-callbacks.lib.diag @@ -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 diff --git a/graph/test/javascript/expected/34-options-object-callbacks.lib.edges b/graph/test/javascript/expected/34-options-object-callbacks.lib.edges index f5582b137..10163a112 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.lib.edges +++ b/graph/test/javascript/expected/34-options-object-callbacks.lib.edges @@ -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 +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 +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 diff --git a/graph/test/javascript/expected/34-options-object-callbacks.oracle b/graph/test/javascript/expected/34-options-object-callbacks.oracle index be68d1524..f31f7ea51 100644 --- a/graph/test/javascript/expected/34-options-object-callbacks.oracle +++ b/graph/test/javascript/expected/34-options-object-callbacks.oracle @@ -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 diff --git a/graph/test/javascript/expected/70-single-file-component-scripts.edges b/graph/test/javascript/expected/70-single-file-component-scripts.edges index 0bb4c4cbb..21bf8d411 100644 --- a/graph/test/javascript/expected/70-single-file-component-scripts.edges +++ b/graph/test/javascript/expected/70-single-file-component-scripts.edges @@ -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 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 diff --git a/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/app.ts b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/app.ts index 198ebefae..10e32c63f 100644 --- a/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/app.ts +++ b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/app.ts @@ -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) +} diff --git a/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/plugin.ts b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/plugin.ts new file mode 100644 index 000000000..3f31a5673 --- /dev/null +++ b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/plugin.ts @@ -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 + +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) diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.edges b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.edges index 4c3712657..52529f7bd 100644 --- a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.edges +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.edges @@ -1,14 +1,24 @@ ambient_terminal FUNCTION_CALL app#atStartup() @L38 -> app#onReady(() =) +ambient_terminal FUNCTION_CALL plugin#() @L12 -> plugin#wrap(T) +ambient_terminal FUNCTION_CALL plugin#() @L7 -> plugin#wrap(T) callback_registered FUNCTION_CALL app#atStartup() @L38 -> app#() +callback_registered FUNCTION_CALL plugin#() @L7 -> plugin#() +callback_registered METHOD_CALL app#boot() @L57 -> plugin#() +callback_registered METHOD_CALL app#bootBare() @L62 -> app#() callback_registered METHOD_CALL app#doubled(number[]) @L16 -> app#(?) callback_registered METHOD_CALL app#named(number[]) @L25 -> app#double(number) callback_registered METHOD_CALL app#scaled(number[]) @L12 -> app#(?) callback_registered METHOD_CALL app#viaLocal(number[]) @L31 -> app#log(number) known_edge FUNCTION_CALL app#() @L38 -> app#record(number) known_edge FUNCTION_CALL app#(?) @L48 -> app#record(number) +known_edge FUNCTION_CALL app#() @L62 -> plugin#migrate() known_edge FUNCTION_CALL app#each(number[],(n: number) =) @L44 -> app#(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#() @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) =) diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.entries b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.entries index 5c3d7421a..79860ca6f 100644 --- a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.entries +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.entries @@ -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 diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.oracle b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.oracle index f31a2b293..e6f0ab28d 100644 --- a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.oracle +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.oracle @@ -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 diff --git a/graph/typescript/engine/resolution/value-flow.dl b/graph/typescript/engine/resolution/value-flow.dl index b8c6aba61..439dac718 100644 --- a/graph/typescript/engine/resolution/value-flow.dl +++ b/graph/typescript/engine/resolution/value-flow.dl @@ -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 diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl index 36d89cb37..deb07ceae 100644 --- a/graph/typescript/souffle/decls_all.dl +++ b/graph/typescript/souffle/decls_all.dl @@ -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) diff --git a/tests/cases/typescript/hof-callback-at-library-boundary/case.json b/tests/cases/typescript/hof-callback-at-library-boundary/case.json index 4f1d456b7..d59b77d3f 100644 --- a/tests/cases/typescript/hof-callback-at-library-boundary/case.json +++ b/tests/cases/typescript/hof-callback-at-library-boundary/case.json @@ -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": [ diff --git a/tests/cases/typescript/hof-callback-at-library-boundary/src/app.ts b/tests/cases/typescript/hof-callback-at-library-boundary/src/app.ts index 198ebefae..10e32c63f 100644 --- a/tests/cases/typescript/hof-callback-at-library-boundary/src/app.ts +++ b/tests/cases/typescript/hof-callback-at-library-boundary/src/app.ts @@ -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) +} diff --git a/tests/cases/typescript/hof-callback-at-library-boundary/src/plugin.ts b/tests/cases/typescript/hof-callback-at-library-boundary/src/plugin.ts new file mode 100644 index 000000000..3f31a5673 --- /dev/null +++ b/tests/cases/typescript/hof-callback-at-library-boundary/src/plugin.ts @@ -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 + +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)