From 009f8536ca19bb25fc65505a08c069f092890e05 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Mon, 28 Sep 2026 04:39:03 -0700 Subject: [PATCH] typescript: a callback handed to a library higher-order function is reached from the site that hands it over, not from every caller of the same signature `xs.map((x) => ...)` stored the arrow in the parameter of a bodiless `map` declaration, so it became a value candidate of one function type shared by every `.map` call: impact through one map callback reached every caller of `.map` in the repository, and a walk down from any caller reached every map callback. Where no project body runs at the call site (a .d.ts member, an ambient declaration, an unresolved callee), the argument no longer flows into the parameter. The site itself gets a `callback_registered` edge to the function it hands over, the tier the JavaScript engine already uses for the same shape. A project higher-order function with a body keeps its dispatch through the parameter. Golden case 78 covers inline, named, local-held and ambient hand-overs, with a project HOF as control. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- graph/bundle/SCHEMA.md | 4 +- graph/bundle/schema.ts | 4 +- .../src/app.ts | 49 ++++++++++++ .../src/globals.d.ts | 15 ++++ .../src/rate.ts | 1 + .../src/tsconfig.json | 9 +++ .../08-function-values-and-callbacks.edges | 1 + ...08-function-values-and-callbacks.lib.edges | 1 + .../expected/17-constrained-generics.edges | 1 + .../17-constrained-generics.lib.edges | 1 + .../expected/19-overload-gauntlet.edges | 1 + .../expected/19-overload-gauntlet.lib.edges | 1 + .../expected/27-readonly-array-receiver.edges | 1 + .../27-readonly-array-receiver.envelope | 1 - .../28-property-chain-container.edges | 4 + .../28-property-chain-container.envelope | 4 - .../expected/34-callback-of-callback.edges | 4 + .../expected/34-callback-of-callback.envelope | 3 - .../expected/38-dynamic-import.edges | 1 + .../expected/41-library-generic-return.edges | 1 + .../69-named-and-inline-handlers.edges | 3 + .../69-named-and-inline-handlers.envelope | 1 - .../70-function-type-target-dispatch.edges | 3 + .../70-function-type-target-dispatch.envelope | 3 - ...bject-literal-and-expression-callees.edges | 2 + ...ct-literal-and-expression-callees.envelope | 1 - .../expected/74-jsx-component-forms.edges | 4 + .../expected/74-jsx-component-forms.envelope | 4 - .../expected/75-jsx-wrapper-guards.edges | 12 +++ .../expected/75-jsx-wrapper-guards.envelope | 8 -- .../76-jsx-default-nested-wrappers.edges | 6 ++ .../76-jsx-default-nested-wrappers.envelope | 4 - .../78-hof-callback-at-library-boundary.edges | 15 ++++ ...8-hof-callback-at-library-boundary.entries | 10 +++ ...-hof-callback-at-library-boundary.envelope | 2 + ...78-hof-callback-at-library-boundary.fields | 0 ...callback-at-library-boundary.fields-oracle | 7 ++ ...78-hof-callback-at-library-boundary.oracle | 1 + ...-hof-callback-at-library-boundary.type-use | 2 + ...-callback-at-library-boundary.types-oracle | 7 ++ .../test/typescript/tools/normalize_edges.py | 4 + .../engine/call-edge-generation/call_chain.dl | 10 +++ .../engine/resolution/value-flow.dl | 45 ++++++++++- graph/typescript/souffle/decls_all.dl | 3 + .../case.json | 80 +++++++++++++++++++ .../src/app.ts | 49 ++++++++++++ .../src/globals.d.ts | 15 ++++ .../src/rate.ts | 1 + .../src/tsconfig.json | 9 +++ .../typescript/registered-handler/case.json | 2 +- 50 files changed, 385 insertions(+), 35 deletions(-) create mode 100644 graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/app.ts create mode 100644 graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/globals.d.ts create mode 100644 graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/rate.ts create mode 100644 graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/tsconfig.json delete mode 100644 graph/test/typescript/expected/27-readonly-array-receiver.envelope delete mode 100644 graph/test/typescript/expected/28-property-chain-container.envelope delete mode 100644 graph/test/typescript/expected/69-named-and-inline-handlers.envelope create mode 100644 graph/test/typescript/expected/78-hof-callback-at-library-boundary.edges create mode 100644 graph/test/typescript/expected/78-hof-callback-at-library-boundary.entries create mode 100644 graph/test/typescript/expected/78-hof-callback-at-library-boundary.envelope create mode 100644 graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields create mode 100644 graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields-oracle create mode 100644 graph/test/typescript/expected/78-hof-callback-at-library-boundary.oracle create mode 100644 graph/test/typescript/expected/78-hof-callback-at-library-boundary.type-use create mode 100644 graph/test/typescript/expected/78-hof-callback-at-library-boundary.types-oracle create mode 100644 tests/cases/typescript/hof-callback-at-library-boundary/case.json create mode 100644 tests/cases/typescript/hof-callback-at-library-boundary/src/app.ts create mode 100644 tests/cases/typescript/hof-callback-at-library-boundary/src/globals.d.ts create mode 100644 tests/cases/typescript/hof-callback-at-library-boundary/src/rate.ts create mode 100644 tests/cases/typescript/hof-callback-at-library-boundary/src/tsconfig.json diff --git a/graph/bundle/SCHEMA.md b/graph/bundle/SCHEMA.md index 7a17d7bf9..7bd4051a8 100644 --- a/graph/bundle/SCHEMA.md +++ b/graph/bundle/SCHEMA.md @@ -552,7 +552,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h | `implicit_constructor` | javascript | `new C()` / `super()` where no constructor exists up the chain: the synthesized default runs. A correct end; callee is NULL. | | `dynamic_terminal` | javascript | `obj[expr]()`, `eval`, `import()`: no static target by construction; callee is NULL. | | `fan_capped` | javascript, java, csharp | More targets than --dispatch-cap: the set was refused rather than emitted. JavaScript: callee is NULL. Java and C#: callee is the declared base method the fan would have started from; dispatch-capped-sites.csv carries the refused count. | -| `callback_registered` | javascript | The site HANDS the callee this function (`xs.forEach(f)`, `p.then(f)`, `emitter.on('x', h)`, `setTimeout(f)`), which may invoke it. Not the site's own callee; a reachability edge, labelled so it is never read as a resolved call. | +| `callback_registered` | javascript, typescript | The site HANDS the callee this function (`xs.forEach(f)`, `p.then(f)`, `emitter.on('x', h)`, `setTimeout(f)`), which may invoke it. Not the site's own callee; a reachability edge, labelled so it is never read as a resolved call. | | `event_dispatch` | javascript | `x.emit('name')` reaching a handler registered by `x.on('name', h)` on a value x may hold — name-sensitive for literal names, every handler on that value for a computed one. | | `event_dispatch` | java | A Spring application event: `publishEvent(e)` reaching each listener (`@EventListener`, `@TransactionalEventListener`, `ApplicationListener.onApplicationEvent`) whose declared event type e's static type is, or is a subtype of. Added beside the publishEvent boundary row, never in place of it (call-edge-generation/event_dispatch.dl). | | `intrinsic_terminal` | typescript | The site is a JSX intrinsic element or a dynamic `import()` — a runtime intrinsic, not a function the graph can name. | @@ -574,7 +574,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h - **python** — A `boundary_lib` edge may point at a builtin (callee_provenance builtin, callee_label `builtin:NAME`) or at an unstaged import path (callee_provenance external) — neither has a methods row. - **java** — A `boundary_lib` edge with callee_provenance external names a method of an ancestor type no staged IR declares (callee_label `external:.`, no methods row). A site whose receiver is declared as such a type is multi_inferred even with one client override: the platform method itself, and the platform's own subclasses, are the other possible targets. Stage the library to replace the label with the real method. - **python** — The reason a site is ambiguous_unknown is exported per site in ext_call_site_unresolved (site, caller, reason, detail). -- **all** — THE TRUST LINE, and it is not the same set of tiers in every language. RESOLVED (callee_method_id is set): known_edge and multi_inferred in every language, and boundary_lib where the library is staged (--library) — without it boundary_lib names the target in callee_label and leaves callee_method_id NULL; ALSO ambient_terminal in TypeScript, fan_capped in Java and C# (the declared base, the fan refused), and runtime_observed in C#. HANDED OVER (callee set, but the site passes the function rather than calling it): callback_registered and event_dispatch in JavaScript; event_dispatch in C# too, where a mediator Send or Publish runs the handler for the request type, beside the row for the site itself. CORRECT END (callee NULL, and nothing is missing): intrinsic_terminal in TypeScript; ambient_terminal, implicit_constructor and dynamic_terminal in JavaScript; known_implicit_ctor, known_builtin_operator and boundary_generated (callee_label set) in C#. BLIND SPOT (callee NULL; exactly the tiers named `ambiguous_*`, which are what unresolved_sites holds): ambiguous_unknown everywhere, ALSO ambiguous_anon in Java and ambiguous_dynamic in C#. CAPPED (callee NULL, not in unresolved_sites): fan_capped in JavaScript. Python emits only the four shared tiers. A filter written as `tier IN (known_edge, multi_inferred)` therefore drops resolved edges in every language but Python — derive the set from this note or from unresolved_sites, never from a hardcoded list. +- **all** — THE TRUST LINE, and it is not the same set of tiers in every language. RESOLVED (callee_method_id is set): known_edge and multi_inferred in every language, and boundary_lib where the library is staged (--library) — without it boundary_lib names the target in callee_label and leaves callee_method_id NULL; ALSO ambient_terminal in TypeScript, fan_capped in Java and C# (the declared base, the fan refused), and runtime_observed in C#. HANDED OVER (callee set, but the site passes the function rather than calling it): callback_registered and event_dispatch in JavaScript, callback_registered in TypeScript; event_dispatch in C# too, where a mediator Send or Publish runs the handler for the request type, beside the row for the site itself. CORRECT END (callee NULL, and nothing is missing): intrinsic_terminal in TypeScript; ambient_terminal, implicit_constructor and dynamic_terminal in JavaScript; known_implicit_ctor, known_builtin_operator and boundary_generated (callee_label set) in C#. BLIND SPOT (callee NULL; exactly the tiers named `ambiguous_*`, which are what unresolved_sites holds): ambiguous_unknown everywhere, ALSO ambiguous_anon in Java and ambiguous_dynamic in C#. CAPPED (callee NULL, not in unresolved_sites): fan_capped in JavaScript. Python emits only the four shared tiers. A filter written as `tier IN (known_edge, multi_inferred)` therefore drops resolved edges in every language but Python — derive the set from this note or from unresolved_sites, never from a hardcoded list. - **java** — A multi_inferred fan is CHA-wide: it is every override the hierarchy admits, bounded only by the dispatch cap. type_instantiated is computed and exported but NOT read by any rule, so the fan is not narrowed to types the program constructs. Narrow it yourself by joining dispatch_candidates to type_instantiated — see the dispatch_envelope_of query. A receiver is ALSO typed by what flows into it (a local's initializer, the arguments callers pass to a parameter, the receivers callers invoke a method on for its `this`), and each flow-in type resolves its member directly, outside the fan: that is why a `fan_capped` site still carries edges, and why they are the types the program was seen to hand over, not the whole hierarchy. - **typescript** — A multi_inferred fan is CHA-wide, as in Java: type_instantiated is computed and exported but NOT read by any rule. The fan also has sources that are not virtual dispatch at all — an overload set or a union-typed receiver produces one too. - **python** — A multi_inferred fan IS narrowed by the instantiation set: type_instantiated_reachable (the constructed classes and their bases) bounds dispatch in resolution/dispatch.dl. Python is the only front end where that narrowing is applied, so a fan here is tighter than the same shape would be in Java or TypeScript. diff --git a/graph/bundle/schema.ts b/graph/bundle/schema.ts index c4bfee466..8467c9467 100644 --- a/graph/bundle/schema.ts +++ b/graph/bundle/schema.ts @@ -511,7 +511,7 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'call_edges', column: 'tier', value: 'implicit_constructor', languages: S, meaning: '`new C()` / `super()` where no constructor exists up the chain: the synthesized default runs. A correct end; callee is NULL.' }, { table: 'call_edges', column: 'tier', value: 'dynamic_terminal', languages: S, meaning: '`obj[expr]()`, `eval`, `import()`: no static target by construction; callee is NULL.' }, { table: 'call_edges', column: 'tier', value: 'fan_capped', languages: ['javascript', 'java', 'csharp'], meaning: 'More targets than --dispatch-cap: the set was refused rather than emitted. JavaScript: callee is NULL. Java and C#: callee is the declared base method the fan would have started from; dispatch-capped-sites.csv carries the refused count.' }, - { table: 'call_edges', column: 'tier', value: 'callback_registered', languages: S, meaning: 'The site HANDS the callee this function (`xs.forEach(f)`, `p.then(f)`, `emitter.on(\'x\', h)`, `setTimeout(f)`), which may invoke it. Not the site\'s own callee; a reachability edge, labelled so it is never read as a resolved call.' }, + { table: 'call_edges', column: 'tier', value: 'callback_registered', languages: ['javascript', 'typescript'], meaning: 'The site HANDS the callee this function (`xs.forEach(f)`, `p.then(f)`, `emitter.on(\'x\', h)`, `setTimeout(f)`), which may invoke it. Not the site\'s own callee; a reachability edge, labelled so it is never read as a resolved call.' }, { table: 'call_edges', column: 'tier', value: 'event_dispatch', languages: S, meaning: '`x.emit(\'name\')` reaching a handler registered by `x.on(\'name\', h)` on a value x may hold — name-sensitive for literal names, every handler on that value for a computed one.' }, { table: 'call_edges', column: 'tier', value: 'event_dispatch', languages: J, meaning: 'A Spring application event: `publishEvent(e)` reaching each listener (`@EventListener`, `@TransactionalEventListener`, `ApplicationListener.onApplicationEvent`) whose declared event type e\'s static type is, or is a subtype of. Added beside the publishEvent boundary row, never in place of it (call-edge-generation/event_dispatch.dl).' }, { table: 'call_edges', column: 'tier', value: 'intrinsic_terminal', languages: T, meaning: 'The site is a JSX intrinsic element or a dynamic `import()` — a runtime intrinsic, not a function the graph can name.' }, @@ -752,7 +752,7 @@ export const NOTES: readonly NoteSpec[] = [ { language: 'python', table: 'entry_points', note: 'Framework entry points only: url, http, orm_hook, task, signal_receiver, fixture, di_provider and grpc_service. There is no test and no main reason: a pytest test is recognised by the query layer from its file and name, not here.' }, { language: 'python', table: 'overrides', note: 'EMPTY — this table is Java-shaped. The Python dispatch envelope is in dispatch_candidates with basis `mro`, and `value` for a function assigned onto an instance\'s member; the raw linearisation is in ext_mro_position.' }, { language: 'python', table: 'type_instantiated', note: 'Every row has how = `new`: the rule set records that some client call constructs the class, not which form.' }, - { language: 'all', table: 'call_edges', note: 'THE TRUST LINE, and it is not the same set of tiers in every language. RESOLVED (callee_method_id is set): known_edge and multi_inferred in every language, and boundary_lib where the library is staged (--library) — without it boundary_lib names the target in callee_label and leaves callee_method_id NULL; ALSO ambient_terminal in TypeScript, fan_capped in Java and C# (the declared base, the fan refused), and runtime_observed in C#. HANDED OVER (callee set, but the site passes the function rather than calling it): callback_registered and event_dispatch in JavaScript; event_dispatch in C# too, where a mediator Send or Publish runs the handler for the request type, beside the row for the site itself. CORRECT END (callee NULL, and nothing is missing): intrinsic_terminal in TypeScript; ambient_terminal, implicit_constructor and dynamic_terminal in JavaScript; known_implicit_ctor, known_builtin_operator and boundary_generated (callee_label set) in C#. BLIND SPOT (callee NULL; exactly the tiers named `ambiguous_*`, which are what unresolved_sites holds): ambiguous_unknown everywhere, ALSO ambiguous_anon in Java and ambiguous_dynamic in C#. CAPPED (callee NULL, not in unresolved_sites): fan_capped in JavaScript. Python emits only the four shared tiers. A filter written as `tier IN (known_edge, multi_inferred)` therefore drops resolved edges in every language but Python — derive the set from this note or from unresolved_sites, never from a hardcoded list.' }, + { language: 'all', table: 'call_edges', note: 'THE TRUST LINE, and it is not the same set of tiers in every language. RESOLVED (callee_method_id is set): known_edge and multi_inferred in every language, and boundary_lib where the library is staged (--library) — without it boundary_lib names the target in callee_label and leaves callee_method_id NULL; ALSO ambient_terminal in TypeScript, fan_capped in Java and C# (the declared base, the fan refused), and runtime_observed in C#. HANDED OVER (callee set, but the site passes the function rather than calling it): callback_registered and event_dispatch in JavaScript, callback_registered in TypeScript; event_dispatch in C# too, where a mediator Send or Publish runs the handler for the request type, beside the row for the site itself. CORRECT END (callee NULL, and nothing is missing): intrinsic_terminal in TypeScript; ambient_terminal, implicit_constructor and dynamic_terminal in JavaScript; known_implicit_ctor, known_builtin_operator and boundary_generated (callee_label set) in C#. BLIND SPOT (callee NULL; exactly the tiers named `ambiguous_*`, which are what unresolved_sites holds): ambiguous_unknown everywhere, ALSO ambiguous_anon in Java and ambiguous_dynamic in C#. CAPPED (callee NULL, not in unresolved_sites): fan_capped in JavaScript. Python emits only the four shared tiers. A filter written as `tier IN (known_edge, multi_inferred)` therefore drops resolved edges in every language but Python — derive the set from this note or from unresolved_sites, never from a hardcoded list.' }, { language: 'java', table: 'call_edges', note: 'A multi_inferred fan is CHA-wide: it is every override the hierarchy admits, bounded only by the dispatch cap. type_instantiated is computed and exported but NOT read by any rule, so the fan is not narrowed to types the program constructs. Narrow it yourself by joining dispatch_candidates to type_instantiated — see the dispatch_envelope_of query. A receiver is ALSO typed by what flows into it (a local\'s initializer, the arguments callers pass to a parameter, the receivers callers invoke a method on for its `this`), and each flow-in type resolves its member directly, outside the fan: that is why a `fan_capped` site still carries edges, and why they are the types the program was seen to hand over, not the whole hierarchy.' }, { language: 'typescript', table: 'call_edges', note: 'A multi_inferred fan is CHA-wide, as in Java: type_instantiated is computed and exported but NOT read by any rule. The fan also has sources that are not virtual dispatch at all — an overload set or a union-typed receiver produces one too.' }, { language: 'python', table: 'call_edges', note: 'A multi_inferred fan IS narrowed by the instantiation set: type_instantiated_reachable (the constructed classes and their bases) bounds dispatch in resolution/dispatch.dl. Python is the only front end where that narrowing is applied, so a fan here is tighter than the same shape would be in Java or TypeScript.' }, 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 new file mode 100644 index 000000000..198ebefae --- /dev/null +++ b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/app.ts @@ -0,0 +1,49 @@ +// A FUNCTION HANDED TO A HIGHER-ORDER FUNCTION WITH NO BODY. `map`, `forEach` and `onReady` are declarations +// only, so the site that hands the callback over is what reaches it: each function below reaches its own +// callback and nothing another function handed to the same `map`. +import { RATE } from './rate' + +const seen: number[] = [] +function record(n: number): void { + seen[0] = n +} + +export function scaled(xs: number[]): number[] { + return xs.map((x) => x * RATE) +} + +export function doubled(xs: number[]): number[] { + return xs.map((x) => x * 2) +} + +function double(n: number): number { + return n * 2 +} + +// a named function handed over: still reached, from the site that names it +export function named(xs: number[]): number[] { + return xs.map(double) +} + +// …and one held by a local first +export function viaLocal(xs: number[]): void { + const log = (n: number) => record(n) + xs.forEach(log) +} + +// an ambient declaration with no body anywhere: a host API that calls back +declare function onReady(cb: () => void): void + +export function atStartup(): void { + onReady(() => record(RATE)) +} + +// CONTROL: a project higher-order function has a body that calls its parameter, so the callback is +// still a dispatch candidate of the parameter's function type, reached through `each`'s own call. +export function each(xs: number[], fn: (n: number) => void): void { + for (const x of xs) fn(x) +} + +export function useEach(xs: number[]): void { + each(xs, (n) => record(n + RATE)) +} diff --git a/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/globals.d.ts b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/globals.d.ts new file mode 100644 index 000000000..32f96ef81 --- /dev/null +++ b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/globals.d.ts @@ -0,0 +1,15 @@ +// The slice of the standard library this case hands callbacks to. Declared here, as client code, so the +// client-only pass sees `map` and `forEach` as what they are to the project: signatures with no body. +interface Array { + map(fn: (v: T) => U): U[]; + forEach(fn: (v: T) => void): void; +} +interface Object {} +interface Boolean {} +interface Number {} +interface String {} +interface Function {} +interface CallableFunction extends Function {} +interface NewableFunction extends Function {} +interface IArguments {} +interface RegExp {} diff --git a/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/rate.ts b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/rate.ts new file mode 100644 index 000000000..b8af0f524 --- /dev/null +++ b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/rate.ts @@ -0,0 +1 @@ +export const RATE = 3 diff --git a/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/tsconfig.json b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/tsconfig.json new file mode 100644 index 000000000..904cf7a11 --- /dev/null +++ b/graph/test/typescript/cases/78-hof-callback-at-library-boundary/src/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true + } +} diff --git a/graph/test/typescript/expected/08-function-values-and-callbacks.edges b/graph/test/typescript/expected/08-function-values-and-callbacks.edges index c0af49567..c0aeef314 100644 --- a/graph/test/typescript/expected/08-function-values-and-callbacks.edges +++ b/graph/test/typescript/expected/08-function-values-and-callbacks.edges @@ -1,5 +1,6 @@ ambiguous_unknown FUNCTION_CALL values#useLibrary() @L40 -> - ambiguous_unknown METHOD_CALL values#drive() @L29 -> - +callback_registered FUNCTION_CALL values#useLibrary() @L40 -> values#local(?) known_edge CONSTRUCTOR_CALL values#drive() @L27 -> Pipeline#() known_edge FUNCTION_CALL values#apply(Transform,string) @L12 -> values#(string) known_edge FUNCTION_CALL values#drive() @L30 -> values#(string) diff --git a/graph/test/typescript/expected/08-function-values-and-callbacks.lib.edges b/graph/test/typescript/expected/08-function-values-and-callbacks.lib.edges index 7cd0700d6..20caf154d 100644 --- a/graph/test/typescript/expected/08-function-values-and-callbacks.lib.edges +++ b/graph/test/typescript/expected/08-function-values-and-callbacks.lib.edges @@ -2,6 +2,7 @@ ambiguous_unknown METHOD_CALL values#drive() @L29 -> - boundary_lib FUNCTION_CALL values#useLibrary() @L40 -> hooks#(string) boundary_lib FUNCTION_CALL values#useLibrary() @L40 -> hooks#identity(?) boundary_lib FUNCTION_CALL values#useLibrary() @L40 -> hooks#withHook(Hook,string) +callback_registered FUNCTION_CALL values#useLibrary() @L40 -> values#local(?) known_edge CONSTRUCTOR_CALL values#drive() @L27 -> Pipeline#() known_edge FUNCTION_CALL values#apply(Transform,string) @L12 -> values#(string) known_edge FUNCTION_CALL values#drive() @L30 -> values#(string) diff --git a/graph/test/typescript/expected/17-constrained-generics.edges b/graph/test/typescript/expected/17-constrained-generics.edges index 765f00dc8..a2f3599cc 100644 --- a/graph/test/typescript/expected/17-constrained-generics.edges +++ b/graph/test/typescript/expected/17-constrained-generics.edges @@ -7,6 +7,7 @@ ambiguous_unknown FUNCTION_CALL consumer#widestLast() @L35 -> - ambiguous_unknown METHOD_CALL consumer#ownConstraintNamed(T) @L46 -> - ambiguous_unknown METHOD_CALL curried-local#pluck(unknown,unknown) @L33 -> - ambiguous_unknown METHOD_CALL curried-local#widest(unknown) @L49 -> - +callback_registered METHOD_CALL curried-local#pluck(unknown,unknown) @L33 -> curried-local#()@L33 known_edge FUNCTION_CALL consumer#localDataFirst() @L56 -> curried-local#pluck(readonly T[],'name') known_edge FUNCTION_CALL consumer#localDataLast() @L59 -> curried-local#pluck('name') known_edge FUNCTION_CALL consumer#localKeyed(Row) @L62 -> curried-local#at(T,T) diff --git a/graph/test/typescript/expected/17-constrained-generics.lib.edges b/graph/test/typescript/expected/17-constrained-generics.lib.edges index 6655ffe07..a31de0df3 100644 --- a/graph/test/typescript/expected/17-constrained-generics.lib.edges +++ b/graph/test/typescript/expected/17-constrained-generics.lib.edges @@ -7,6 +7,7 @@ boundary_lib FUNCTION_CALL consumer#keyedFirst(Row) @L23 -> curried#at(T,T) boundary_lib FUNCTION_CALL consumer#keyedLast() @L27 -> curried#at(T) boundary_lib FUNCTION_CALL consumer#widestFirst() @L31 -> curried#widest(readonly T[]) boundary_lib FUNCTION_CALL consumer#widestLast() @L35 -> curried#widest() +callback_registered METHOD_CALL curried-local#pluck(unknown,unknown) @L33 -> curried-local#()@L33 known_edge FUNCTION_CALL consumer#localDataFirst() @L56 -> curried-local#pluck(readonly T[],'name') known_edge FUNCTION_CALL consumer#localDataLast() @L59 -> curried-local#pluck('name') known_edge FUNCTION_CALL consumer#localKeyed(Row) @L62 -> curried-local#at(T,T) diff --git a/graph/test/typescript/expected/19-overload-gauntlet.edges b/graph/test/typescript/expected/19-overload-gauntlet.edges index a56fdb1ac..62ee92406 100644 --- a/graph/test/typescript/expected/19-overload-gauntlet.edges +++ b/graph/test/typescript/expected/19-overload-gauntlet.edges @@ -8,5 +8,6 @@ ambiguous_unknown FUNCTION_CALL consumer#viaMappedType(Readonly) @L34 -> - ambiguous_unknown FUNCTION_CALL consumer#viaRequired(Required) @L38 -> - ambiguous_unknown METHOD_CALL consumer#viaReceiver(Store,Row,Cell) @L52 -> - ambiguous_unknown METHOD_CALL consumer#viaReceiver(Store,Row,Cell) @L53 -> - +callback_registered FUNCTION_CALL consumer#viaContextualCallback(readonly Row[]) @L23 -> consumer#(?) known_edge FUNCTION_CALL consumer#viaInferredReturn() @L16 -> consumer#makeCount() known_edge FUNCTION_CALL consumer#viaInferredReturn() @L17 -> consumer#makeLabel() diff --git a/graph/test/typescript/expected/19-overload-gauntlet.lib.edges b/graph/test/typescript/expected/19-overload-gauntlet.lib.edges index b5604bc5f..30f1eb6d1 100644 --- a/graph/test/typescript/expected/19-overload-gauntlet.lib.edges +++ b/graph/test/typescript/expected/19-overload-gauntlet.lib.edges @@ -8,5 +8,6 @@ boundary_lib FUNCTION_CALL consumer#viaMappedType(Readonly) @L34 -> api#shape(Ce boundary_lib FUNCTION_CALL consumer#viaRequired(Required) @L38 -> api#shape(Cell) boundary_lib METHOD_CALL consumer#viaReceiver(Store,Row,Cell) @L52 -> Store#put(string,Row) boundary_lib METHOD_CALL consumer#viaReceiver(Store,Row,Cell) @L53 -> Store#put(string,Cell) +callback_registered FUNCTION_CALL consumer#viaContextualCallback(readonly Row[]) @L23 -> consumer#(?) known_edge FUNCTION_CALL consumer#viaInferredReturn() @L16 -> consumer#makeCount() known_edge FUNCTION_CALL consumer#viaInferredReturn() @L17 -> consumer#makeLabel() diff --git a/graph/test/typescript/expected/27-readonly-array-receiver.edges b/graph/test/typescript/expected/27-readonly-array-receiver.edges index a19575d39..eb4616e15 100644 --- a/graph/test/typescript/expected/27-readonly-array-receiver.edges +++ b/graph/test/typescript/expected/27-readonly-array-receiver.edges @@ -1,3 +1,4 @@ +callback_registered METHOD_CALL readonly#useEvery(readonly ((v: number) =[]) @L11 -> readonly#(?) known_edge FUNCTION_CALL readonly#(?) @L11 -> readonly#(number) known_edge METHOD_CALL readonly#mutablePush(string[]) @L27 -> Array#push(T) known_edge METHOD_CALL readonly#mutableSlice(string[]) @L22 -> Array#slice(number,number) diff --git a/graph/test/typescript/expected/27-readonly-array-receiver.envelope b/graph/test/typescript/expected/27-readonly-array-receiver.envelope deleted file mode 100644 index 17c00930a..000000000 --- a/graph/test/typescript/expected/27-readonly-array-receiver.envelope +++ /dev/null @@ -1 +0,0 @@ -value globals#@11:15 -> readonly# diff --git a/graph/test/typescript/expected/28-property-chain-container.edges b/graph/test/typescript/expected/28-property-chain-container.edges index e62a0689a..e5c7a9d31 100644 --- a/graph/test/typescript/expected/28-property-chain-container.edges +++ b/graph/test/typescript/expected/28-property-chain-container.edges @@ -1,3 +1,7 @@ +callback_registered METHOD_CALL Builder#viaField() @L17 -> Builder#(?) +callback_registered METHOD_CALL Builder#viaGetter() @L22 -> Builder#(?) +callback_registered METHOD_CALL Builder#viaLocal() @L28 -> Builder#(?) +callback_registered METHOD_CALL chain#viaAnnotated(Column[]) @L34 -> chain#(?) known_edge METHOD_CALL Builder#(?) @L17 -> Column#isVirtual() known_edge METHOD_CALL Builder#(?) @L22 -> Column#isVirtual() known_edge METHOD_CALL Builder#(?) @L28 -> Column#isVirtual() diff --git a/graph/test/typescript/expected/28-property-chain-container.envelope b/graph/test/typescript/expected/28-property-chain-container.envelope deleted file mode 100644 index 31bc2ad2f..000000000 --- a/graph/test/typescript/expected/28-property-chain-container.envelope +++ /dev/null @@ -1,4 +0,0 @@ -value globals#@2:16 -> chain# -value globals#@2:16 -> chain#Builder.@17 -value globals#@2:16 -> chain#Builder.@22 -value globals#@2:16 -> chain#Builder.@28 diff --git a/graph/test/typescript/expected/34-callback-of-callback.edges b/graph/test/typescript/expected/34-callback-of-callback.edges index cd263c396..d2ff99be3 100644 --- a/graph/test/typescript/expected/34-callback-of-callback.edges +++ b/graph/test/typescript/expected/34-callback-of-callback.edges @@ -1,3 +1,7 @@ +callback_registered CONSTRUCTOR_CALL exec#viaExecutor() @L12 -> exec#(?,?) +callback_registered CONSTRUCTOR_CALL exec#viaRenamedExecutor() @L20 -> exec#(?,?) +callback_registered FUNCTION_CALL exec#withSink((emit: Sink) =) @L29 -> exec#() +callback_registered METHOD_CALL exec#viaNamedParam(Payload[]) @L40 -> exec#(?) known_edge CONSTRUCTOR_CALL exec#(?,?) @L13 -> Payload#() known_edge CONSTRUCTOR_CALL exec#(?,?) @L21 -> Payload#() known_edge CONSTRUCTOR_CALL exec#(?) @L33 -> Payload#() diff --git a/graph/test/typescript/expected/34-callback-of-callback.envelope b/graph/test/typescript/expected/34-callback-of-callback.envelope index af22a1296..1fc52a968 100644 --- a/graph/test/typescript/expected/34-callback-of-callback.envelope +++ b/graph/test/typescript/expected/34-callback-of-callback.envelope @@ -1,5 +1,2 @@ value exec#@28:31 -> exec#@32 -value globals#@12:21 -> exec#@12 -value globals#@12:21 -> exec#@20 -value globals#@17:14 -> exec#@40 value_call exec#withSink -> exec#@32 diff --git a/graph/test/typescript/expected/38-dynamic-import.edges b/graph/test/typescript/expected/38-dynamic-import.edges index 944dc653e..1dd6e8c36 100644 --- a/graph/test/typescript/expected/38-dynamic-import.edges +++ b/graph/test/typescript/expected/38-dynamic-import.edges @@ -1,5 +1,6 @@ ambiguous_unknown METHOD_CALL r352#ctlComputedSpecifier(string) @L39 -> - ambiguous_unknown METHOD_CALL r352#viaThenCallback() @L27 -> - +callback_registered METHOD_CALL r352#viaThenCallback() @L27 -> r352#(?) intrinsic_terminal DYNAMIC_IMPORT_CALL r352#ctlComputedSpecifier(string) @L38 -> - intrinsic_terminal DYNAMIC_IMPORT_CALL r352#viaLiteralBare() @L17 -> - intrinsic_terminal DYNAMIC_IMPORT_CALL r352#viaLiteralDestructured() @L22 -> - diff --git a/graph/test/typescript/expected/41-library-generic-return.edges b/graph/test/typescript/expected/41-library-generic-return.edges index af6638f56..50c29bb94 100644 --- a/graph/test/typescript/expected/41-library-generic-return.edges +++ b/graph/test/typescript/expected/41-library-generic-return.edges @@ -1,5 +1,6 @@ ambiguous_unknown METHOD_CALL r356#ctlNonGenericLibReturn(string) @L28 -> - ambiguous_unknown METHOD_CALL r356#viaGenericLibReturn() @L35 -> - +callback_registered METHOD_CALL r356#viaGenericLibReturn() @L35 -> r356#(AlphaOp) known_edge CONSTRUCTOR_CALL r356#ctlClientGenericReturn() @L43 -> AlphaOp#() known_edge CONSTRUCTOR_CALL r356#makeAlpha() @L48 -> AlphaOp#() known_edge CONSTRUCTOR_CALL r356#viaGenericLibReturn() @L35 -> AlphaOp#() diff --git a/graph/test/typescript/expected/69-named-and-inline-handlers.edges b/graph/test/typescript/expected/69-named-and-inline-handlers.edges index cf666d4da..9f3b7b6cd 100644 --- a/graph/test/typescript/expected/69-named-and-inline-handlers.edges +++ b/graph/test/typescript/expected/69-named-and-inline-handlers.edges @@ -1,6 +1,9 @@ ambient_terminal FUNCTION_CALL routes#() @L35 -> routes#it(string,() =) ambient_terminal FUNCTION_CALL routes#() @L50 -> routes#registerAll(Record string) ambiguous_unknown METHOD_CALL routes#() @L40 -> - +callback_registered FUNCTION_CALL routes#() @L35 -> routes#namedSpec() +callback_registered METHOD_CALL routes#() @L24 -> routes#() +callback_registered METHOD_CALL routes#() @L30 -> routes#namedHandler() known_edge FUNCTION_CALL routes#() @L24 -> routes#inlineWork() known_edge FUNCTION_CALL routes#() @L40 -> routes#notAHandler() known_edge FUNCTION_CALL routes#namedHandler() @L28 -> routes#namedWork() diff --git a/graph/test/typescript/expected/69-named-and-inline-handlers.envelope b/graph/test/typescript/expected/69-named-and-inline-handlers.envelope deleted file mode 100644 index 8b02681eb..000000000 --- a/graph/test/typescript/expected/69-named-and-inline-handlers.envelope +++ /dev/null @@ -1 +0,0 @@ -value routes#@21:41 -> routes#namedSpec diff --git a/graph/test/typescript/expected/70-function-type-target-dispatch.edges b/graph/test/typescript/expected/70-function-type-target-dispatch.edges index 0001be686..619b9e47d 100644 --- a/graph/test/typescript/expected/70-function-type-target-dispatch.edges +++ b/graph/test/typescript/expected/70-function-type-target-dispatch.edges @@ -1,3 +1,6 @@ +callback_registered METHOD_CALL targets#viaAnnotatedParam(CB[]) @L14 -> targets#(?) +callback_registered METHOD_CALL targets#viaAsCast(unknown) @L19 -> targets#(?) +callback_registered METHOD_CALL targets#viaGenericMap(number[]) @L31 -> targets#(?)@L31 known_edge FUNCTION_CALL targets#(?) @L14 -> targets#(number) known_edge FUNCTION_CALL targets#(?) @L19 -> targets#(number) known_edge FUNCTION_CALL targets#viaForOf(CB[]) @L23 -> targets#(number) diff --git a/graph/test/typescript/expected/70-function-type-target-dispatch.envelope b/graph/test/typescript/expected/70-function-type-target-dispatch.envelope index 17ac3194b..1dd6161a1 100644 --- a/graph/test/typescript/expected/70-function-type-target-dispatch.envelope +++ b/graph/test/typescript/expected/70-function-type-target-dispatch.envelope @@ -1,5 +1,2 @@ -value globals#@4:14 -> targets#@14 -value globals#@4:14 -> targets#@19 -value globals#@4:14 -> targets#@31 value targets#@6:18 -> targets#@10 value targets#@6:18 -> targets#@8 diff --git a/graph/test/typescript/expected/72-object-literal-and-expression-callees.edges b/graph/test/typescript/expected/72-object-literal-and-expression-callees.edges index f50f45644..178b6e843 100644 --- a/graph/test/typescript/expected/72-object-literal-and-expression-callees.edges +++ b/graph/test/typescript/expected/72-object-literal-and-expression-callees.edges @@ -1,3 +1,5 @@ +callback_registered FUNCTION_CALL middleware#dispatch(Entry[][],number) @L11 -> middleware#() +callback_registered METHOD_CALL pool#usePool(Pool) @L21 -> pool#() known_edge FUNCTION_CALL fetcher#get(string,Options) @L17 -> fetcher#(string) known_edge FUNCTION_CALL fetcher#load() @L21 -> fetcher#get(string,Options) known_edge FUNCTION_CALL middleware#boot() @L15 -> middleware#dispatch(Entry[][],number) diff --git a/graph/test/typescript/expected/72-object-literal-and-expression-callees.envelope b/graph/test/typescript/expected/72-object-literal-and-expression-callees.envelope index baaffa735..fc04d0270 100644 --- a/graph/test/typescript/expected/72-object-literal-and-expression-callees.envelope +++ b/graph/test/typescript/expected/72-object-literal-and-expression-callees.envelope @@ -1,6 +1,5 @@ value fetcher#@4:21 -> fetcher# value middleware#@4:23 -> middleware#@7 -value pool#@6:11 -> pool#@21 value pool#Pool.run -> pool#@11 value pool#Pool.run -> pool#@16 value pool#Pool.run -> pool#@25 diff --git a/graph/test/typescript/expected/74-jsx-component-forms.edges b/graph/test/typescript/expected/74-jsx-component-forms.edges index 4bbd01501..887e1bbaf 100644 --- a/graph/test/typescript/expected/74-jsx-component-forms.edges +++ b/graph/test/typescript/expected/74-jsx-component-forms.edges @@ -7,6 +7,10 @@ ambiguous_unknown FUNCTION_CALL parts#FieldImpl({ label: string },unknown) @L39 ambiguous_unknown FUNCTION_CALL settings#Settings() @L6 -> - ambiguous_unknown METHOD_CALL app#App() @L15 -> - ambiguous_unknown METHOD_CALL parts#formatName(string) @L25 -> - +callback_registered FUNCTION_CALL app#() @L4 -> app#() +callback_registered FUNCTION_CALL parts#() @L33 -> parts#UserCard({ id: string }) +callback_registered FUNCTION_CALL parts#() @L41 -> parts#FieldImpl({ label: string },unknown) +callback_registered FUNCTION_CALL parts#() @L73 -> parts#buildDefault() intrinsic_terminal DYNAMIC_IMPORT_CALL app#() @L4 -> - known_edge FUNCTION_CALL app#useFactory() @L23 -> parts#()@L22 known_edge FUNCTION_CALL parts#FieldImpl({ label: string },unknown) @L39 -> parts#trackClick(string) diff --git a/graph/test/typescript/expected/74-jsx-component-forms.envelope b/graph/test/typescript/expected/74-jsx-component-forms.envelope index 3b3998b48..690fd675e 100644 --- a/graph/test/typescript/expected/74-jsx-component-forms.envelope +++ b/graph/test/typescript/expected/74-jsx-component-forms.envelope @@ -1,5 +1 @@ value parts#@18:21 -> parts# -value parts#@18:21 -> parts#UserCard -value parts#@20:47 -> parts#FieldImpl -value parts#@21:39 -> app# -value parts#@22:47 -> parts#buildDefault diff --git a/graph/test/typescript/expected/75-jsx-wrapper-guards.edges b/graph/test/typescript/expected/75-jsx-wrapper-guards.edges index 68ca2dc39..78ebdfe74 100644 --- a/graph/test/typescript/expected/75-jsx-wrapper-guards.edges +++ b/graph/test/typescript/expected/75-jsx-wrapper-guards.edges @@ -19,6 +19,18 @@ ambiguous_unknown METHOD_CALL app#() @L15 -> - ambiguous_unknown METHOD_CALL app#() @L16 -> - ambiguous_unknown METHOD_CALL app#() @L18 -> - ambiguous_unknown METHOD_CALL app#ArrowRenameTag({ icon: Fc }[]) @L57 -> - +callback_registered FUNCTION_CALL app#() @L15 -> app#() +callback_registered FUNCTION_CALL app#() @L16 -> app#() +callback_registered FUNCTION_CALL app#() @L18 -> app#() +callback_registered FUNCTION_CALL app#() @L19 -> app#() +callback_registered FUNCTION_CALL app#() @L21 -> app#() +callback_registered FUNCTION_CALL app#() @L22 -> app#() +callback_registered FUNCTION_CALL app#() @L29 -> app#MemoInner() +callback_registered FUNCTION_CALL app#() @L30 -> app#ObservedImpl() +callback_registered METHOD_CALL app#() @L15 -> app#(?) +callback_registered METHOD_CALL app#() @L16 -> app#(?) +callback_registered METHOD_CALL app#() @L18 -> app#(?) +callback_registered METHOD_CALL app#ArrowRenameTag({ icon: Fc }[]) @L57 -> app#(?,?) intrinsic_terminal DYNAMIC_IMPORT_CALL app#() @L15 -> - intrinsic_terminal DYNAMIC_IMPORT_CALL app#() @L16 -> - intrinsic_terminal DYNAMIC_IMPORT_CALL app#() @L18 -> - diff --git a/graph/test/typescript/expected/75-jsx-wrapper-guards.envelope b/graph/test/typescript/expected/75-jsx-wrapper-guards.envelope index 4274ea0da..ae87f78fd 100644 --- a/graph/test/typescript/expected/75-jsx-wrapper-guards.envelope +++ b/graph/test/typescript/expected/75-jsx-wrapper-guards.envelope @@ -1,12 +1,4 @@ -value parts#@15:21 -> app#MemoInner -value parts#@15:21 -> app#ObservedImpl value parts#@15:21 -> parts# -value parts#@18:36 -> app#@15 -value parts#@18:36 -> app#@18 -value parts#@18:36 -> app#@21 -value parts#@19:39 -> app#@16 -value parts#@19:39 -> app#@19 -value parts#@19:39 -> app#@22 value parts#@22:30 -> app#@25 value parts#@26:35 -> app#@26 value_call app#Page -> app#MadeInner diff --git a/graph/test/typescript/expected/76-jsx-default-nested-wrappers.edges b/graph/test/typescript/expected/76-jsx-default-nested-wrappers.edges index 5b6145e44..c0fb8e397 100644 --- a/graph/test/typescript/expected/76-jsx-default-nested-wrappers.edges +++ b/graph/test/typescript/expected/76-jsx-default-nested-wrappers.edges @@ -12,6 +12,12 @@ ambiguous_unknown FUNCTION_CALL parts#MemoFwdInner({},unknown) @L40 -> - ambiguous_unknown JSX_COMPONENT_CALL app#App() @L19 -> - ambiguous_unknown METHOD_CALL app#() @L6 -> - ambiguous_unknown METHOD_CALL app#makeName(number) @L26 -> - +callback_registered FUNCTION_CALL app#() @L10 -> parts#() +callback_registered FUNCTION_CALL app#() @L6 -> app#() +callback_registered FUNCTION_CALL app#() @L8 -> app#() +callback_registered FUNCTION_CALL defmemo#() @L4 -> parts#DefaultMemoInner() +callback_registered FUNCTION_CALL parts#() @L39 -> parts#MemoFwdInner({},unknown) +callback_registered METHOD_CALL app#() @L6 -> app#(?) intrinsic_terminal DYNAMIC_IMPORT_CALL app#() @L6 -> - intrinsic_terminal DYNAMIC_IMPORT_CALL app#() @L8 -> - known_edge FUNCTION_CALL app#() @L10 -> parts#pickOther(Fc) diff --git a/graph/test/typescript/expected/76-jsx-default-nested-wrappers.envelope b/graph/test/typescript/expected/76-jsx-default-nested-wrappers.envelope index 539de9007..509328b85 100644 --- a/graph/test/typescript/expected/76-jsx-default-nested-wrappers.envelope +++ b/graph/test/typescript/expected/76-jsx-default-nested-wrappers.envelope @@ -1,7 +1,3 @@ value parts#@14:21 -> parts# -value parts#@14:21 -> parts#DefaultMemoInner value parts#@14:21 -> parts#NestedPickedInner value parts#@14:21 -> parts#PickedInner -value parts#@16:47 -> parts#MemoFwdInner -value parts#@17:36 -> app#@6 -value parts#@17:36 -> app#@8 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 new file mode 100644 index 000000000..4c3712657 --- /dev/null +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.edges @@ -0,0 +1,15 @@ +ambient_terminal FUNCTION_CALL app#atStartup() @L38 -> app#onReady(() =) +callback_registered FUNCTION_CALL app#atStartup() @L38 -> 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#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 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) =) +known_edge METHOD_CALL app#viaLocal(number[]) @L31 -> Array#forEach((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 new file mode 100644 index 000000000..5c3d7421a --- /dev/null +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.entries @@ -0,0 +1,10 @@ +── entry_point (9) ── + exported_from_entry_module app#atStartup app.ts:37 + 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 + exported_from_entry_module app#scaled app.ts:11 + exported_from_entry_module app#useEach app.ts:47 + exported_from_entry_module app#viaLocal app.ts:29 + unimported_module app# app.ts:1 + unimported_module globals# globals.d.ts:1 diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.envelope b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.envelope new file mode 100644 index 000000000..234b7d617 --- /dev/null +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.envelope @@ -0,0 +1,2 @@ +value app#@43:40 -> app#@48 +value_call app#each -> app#@48 diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields new file mode 100644 index 000000000..e69de29bb diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields-oracle b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields-oracle new file mode 100644 index 000000000..be4f8be64 --- /dev/null +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields-oracle @@ -0,0 +1,7 @@ +78-hof-callback-at-library-boundary [fields] + precision 0.0000 (0 correct, 0 wrong) + recall 0.0000 (0 of 0 the compiler resolved) + sites 0 resolved 0 + tiers + access + not scored: 0 rows whose target is not a client declaration 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 new file mode 100644 index 000000000..f31a2b293 --- /dev/null +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.oracle @@ -0,0 +1 @@ +oracle=10 engine=10 agree=10 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.type-use b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.type-use new file mode 100644 index 000000000..960a82a1b --- /dev/null +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.type-use @@ -0,0 +1,2 @@ +known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function +known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.types-oracle b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.types-oracle new file mode 100644 index 000000000..e7a4988d6 --- /dev/null +++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.types-oracle @@ -0,0 +1,7 @@ +78-hof-callback-at-library-boundary [types] + precision 1.0000 (2 correct, 0 wrong) + recall 1.0000 (2 of 2 the compiler resolved) + sites 2 resolved 2 (100.0%) + tiers known_edge=2 + contexts SUPER_TYPE=2 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/tools/normalize_edges.py b/graph/test/typescript/tools/normalize_edges.py index e4cdb0361..291ccaa44 100644 --- a/graph/test/typescript/tools/normalize_edges.py +++ b/graph/test/typescript/tools/normalize_edges.py @@ -282,6 +282,10 @@ def main(): if pairs_only: if f[1] not in n.m or f[3] not in n.m: continue + # a function the site HANDS OVER (`xs.map(cb)`) is not a call the compiler lists at that + # site: the pairs are scored against the compiler, the .edges golden still carries it + if f[5] == 'callback_registered': + continue seen.add(f"{n.label(f[1])} -> {n.label(f[3])}") else: ln = site_line.get(f[0], '?') diff --git a/graph/typescript/engine/call-edge-generation/call_chain.dl b/graph/typescript/engine/call-edge-generation/call_chain.dl index 212c7fadc..565187993 100644 --- a/graph/typescript/engine/call-edge-generation/call_chain.dl +++ b/graph/typescript/engine/call-edge-generation/call_chain.dl @@ -124,6 +124,16 @@ call_chain_edge(ce, caller, "-", "-", "-", cls, kind) :- class_is_terminal("ambiguous_unknown"). class_is_terminal("intrinsic_terminal"). +// A function the site HANDS to a callee with no client body (resolution/value-flow.dl, +// handed_function): `xs.map(cb)`, `p.then(cb)`. Not the site's own callee, so its own +// tier, the one the JavaScript engine gives the same hand-over. +call_chain_edge(ce, caller, "-", m, "client", "callback_registered", kind) :- + handed_function(ce, m), + method_prov(m, "client"), + call_from(ce, caller), + caller != m, + invocation_site(ce, kind). + // ── call_runs_edge(FromMethod, ToMethod, Prov) ────────────────────────────── // The chain a consumer should WALK: overload signatures re-pointed to the body that // actually executes (resolution/overload-sets.dl). Kept separate from diff --git a/graph/typescript/engine/resolution/value-flow.dl b/graph/typescript/engine/resolution/value-flow.dl index b64ea09d4..1989b9460 100644 --- a/graph/typescript/engine/resolution/value-flow.dl +++ b/graph/typescript/engine/resolution/value-flow.dl @@ -188,7 +188,50 @@ holder_value(v, e) :- var_initializer("client", _, e, v), e != "". holder_value(v, e) :- var_reassigned_value(v, e). holder_value(p, a) :- expr_child("client", ce, "ARGUMENT", pos, a), expr_resolves_to_method(ce, callee), # the overload the call SELECTS, not every candidate: - param_decl("client", _, pos, _, callee, p). # an arrow passed to on("close", …) is not a listener of every on + param_decl("client", _, pos, _, callee, p), # an arrow passed to on("close", …) is not a listener of every on + !hof_boundary_site(ce). # …and a parameter no body reads holds nothing (below) + +// ============================================================================ +// A FUNCTION HANDED TO A HIGHER-ORDER FUNCTION WITH NO BODY (the library boundary) +// +// `xs.map((x) => f(x))` where `map` is a declaration with no body in the client: a `.d.ts` +// interface member, an ambient `declare function`, an interface method nothing implements. +// Stored in the parameter, the arrow became a `value` candidate of the parameter's function +// type, and that ONE signature is shared by every `.map` call in the repository: a walk up +// from any map callback reached every caller of `.map`, and a walk down from any caller of +// `.map` reached every map callback. One constant read inside one arrow reached 1,078 +// callables in 198 files. No body can call through the parameter, so the envelope row +// said nothing true that the call site does not say better. +// +// The call SITE is what hands the function over, so the edge is the site's own: the +// function that writes `xs.map(cb)` reaches `cb` (call_chain.dl, `callback_registered`), +// and a callback passed at one site is never reached from another. +// +// Only where no CLIENT body runs at the site. A project function that calls its parameter +// keeps the holder rule: its body is what runs the callback, and that is a real call. +// ============================================================================ +call_runs_client_body(ce) :- call_runs_method(ce, m), method_prov(m, "client"). +hof_boundary_site(ce) :- call_site("client", _, _, _, _, ce, _), !call_runs_client_body(ce). + +value_branch(a, a) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a). +handed_function(ce, m) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a), + value_branch(a, x), + !expr_kind("client", "CALL_EXPRESSION", _, x), # `xs.map(make())` hands over what make RETURNS + expr_callable(x, m). +handed_function(ce, m) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a), + value_branch(a, x), + expr_referenced("client", k, h, x), + holder_ref_kind(k), + holder_holds_function(h, m). +handed_function(ce, m) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a), + value_branch(a, x), + ts_field_access_target(x, f), + holder_holds_function(f, m). +handed_function(ce, m) :- hof_boundary_site(ce), expr_child("client", ce, "ARGUMENT", _, a), + value_branch(a, x), + expr_kind("client", "CALL_EXPRESSION", _, x), + call_runs(x, f), + holder_holds_function(f, m). // ── value_branch(Root, Expr): Expr is a value Root can evaluate to ── value_branch(e, e) :- holder_value(_, e). diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl index 0210d202f..e5ffef4be 100644 --- a/graph/typescript/souffle/decls_all.dl +++ b/graph/typescript/souffle/decls_all.dl @@ -197,6 +197,9 @@ .decl value_branch(c0:symbol,c1:symbol) .decl binary_op_yields_operand(c0:symbol,c1:symbol) .decl holder_holds_function(c0:symbol,c1:symbol) +.decl call_runs_client_body(c0:symbol) +.decl hof_boundary_site(c0:symbol) +.decl handed_function(c0:symbol,c1:symbol) .decl called_through(c0:symbol,c1:symbol) .decl method_enclosing_member(c0:symbol,c1:symbol) .decl call_runs(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 new file mode 100644 index 000000000..4f1d456b7 --- /dev/null +++ b/tests/cases/typescript/hof-callback-at-library-boundary/case.json @@ -0,0 +1,80 @@ +{ + "lang": "typescript", + "src": "src", + "checks": [ + { + "why": "a callback handed to a library higher-order function (a `map` with no body) is reached from the function that hands it over, and from no other caller of `map`: one shared function-type node made every `.map` callback reachable from every `.map` call", + "run": [ + "impact", + "RATE" + ], + "want": [ + "scaled src/app.ts:11", + "atStartup src/app.ts:37", + "useEach src/app.ts:47" + ], + "avoid": [ + "src/app.ts:15", + "src/app.ts:24", + "src/app.ts:29" + ] + }, + { + "why": "the function that writes `xs.map((x) => …)` reaches its own callback", + "run": [ + "path", + "scaled", + "src/app.ts:12" + ], + "want": [ + "1 of 1 target(s) reached" + ], + "avoid": [ + "Array.map" + ] + }, + { + "why": "and never a callback another function handed to the same `map`", + "run": [ + "path", + "doubled", + "src/app.ts:12" + ], + "want": [], + "avoid": [ + "1 of 1 target(s) reached" + ], + "expect_error": true + }, + { + "why": "CONTROL: a NAMED function handed to `map` is still reached, from the site that names it", + "run": [ + "path", + "named", + "double" + ], + "want": [ + "1 of 1 target(s) reached", + "[callback_registered" + ], + "avoid": [ + "no chain of resolved calls" + ] + }, + { + "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": [ + "path", + "each", + "src/app.ts:48" + ], + "want": [ + "1 of 1 target(s) reached", + "[dispatch]" + ], + "avoid": [ + "no chain of resolved calls" + ] + } + ] +} 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 new file mode 100644 index 000000000..198ebefae --- /dev/null +++ b/tests/cases/typescript/hof-callback-at-library-boundary/src/app.ts @@ -0,0 +1,49 @@ +// A FUNCTION HANDED TO A HIGHER-ORDER FUNCTION WITH NO BODY. `map`, `forEach` and `onReady` are declarations +// only, so the site that hands the callback over is what reaches it: each function below reaches its own +// callback and nothing another function handed to the same `map`. +import { RATE } from './rate' + +const seen: number[] = [] +function record(n: number): void { + seen[0] = n +} + +export function scaled(xs: number[]): number[] { + return xs.map((x) => x * RATE) +} + +export function doubled(xs: number[]): number[] { + return xs.map((x) => x * 2) +} + +function double(n: number): number { + return n * 2 +} + +// a named function handed over: still reached, from the site that names it +export function named(xs: number[]): number[] { + return xs.map(double) +} + +// …and one held by a local first +export function viaLocal(xs: number[]): void { + const log = (n: number) => record(n) + xs.forEach(log) +} + +// an ambient declaration with no body anywhere: a host API that calls back +declare function onReady(cb: () => void): void + +export function atStartup(): void { + onReady(() => record(RATE)) +} + +// CONTROL: a project higher-order function has a body that calls its parameter, so the callback is +// still a dispatch candidate of the parameter's function type, reached through `each`'s own call. +export function each(xs: number[], fn: (n: number) => void): void { + for (const x of xs) fn(x) +} + +export function useEach(xs: number[]): void { + each(xs, (n) => record(n + RATE)) +} diff --git a/tests/cases/typescript/hof-callback-at-library-boundary/src/globals.d.ts b/tests/cases/typescript/hof-callback-at-library-boundary/src/globals.d.ts new file mode 100644 index 000000000..32f96ef81 --- /dev/null +++ b/tests/cases/typescript/hof-callback-at-library-boundary/src/globals.d.ts @@ -0,0 +1,15 @@ +// The slice of the standard library this case hands callbacks to. Declared here, as client code, so the +// client-only pass sees `map` and `forEach` as what they are to the project: signatures with no body. +interface Array { + map(fn: (v: T) => U): U[]; + forEach(fn: (v: T) => void): void; +} +interface Object {} +interface Boolean {} +interface Number {} +interface String {} +interface Function {} +interface CallableFunction extends Function {} +interface NewableFunction extends Function {} +interface IArguments {} +interface RegExp {} diff --git a/tests/cases/typescript/hof-callback-at-library-boundary/src/rate.ts b/tests/cases/typescript/hof-callback-at-library-boundary/src/rate.ts new file mode 100644 index 000000000..b8af0f524 --- /dev/null +++ b/tests/cases/typescript/hof-callback-at-library-boundary/src/rate.ts @@ -0,0 +1 @@ +export const RATE = 3 diff --git a/tests/cases/typescript/hof-callback-at-library-boundary/src/tsconfig.json b/tests/cases/typescript/hof-callback-at-library-boundary/src/tsconfig.json new file mode 100644 index 000000000..904cf7a11 --- /dev/null +++ b/tests/cases/typescript/hof-callback-at-library-boundary/src/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true + } +} diff --git a/tests/cases/typescript/registered-handler/case.json b/tests/cases/typescript/registered-handler/case.json index 97ed399d0..c259a1474 100644 --- a/tests/cases/typescript/registered-handler/case.json +++ b/tests/cases/typescript/registered-handler/case.json @@ -10,6 +10,6 @@ "avoid": ["no dependent at any certainty"]}, {"why": "a declaration passed to anything else is reported as handed over as a value, never as a route: the receiving call is not named as one that calls it later, since that was wrong for every synchronous collection operation (#1166)", "run": ["impact", "auditLog"], - "want": ["names it as a value", "src/server.ts:7"], + "want": ["handed over as a value", "src/server.ts:7"], "avoid": ["registered as a", "the graph can see: nothing"]} ]}