From ad2dcd99a5229cede5fda8f485baae06e6e09150 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Tue, 3 Nov 2026 02:19:10 -0800 Subject: [PATCH] typescript: a destructured parameter typed by its context, and a literal nested in a typed literal `const poll: (ctx: Ctx) => R = async ({ data, utils }) => utils.http(..)` and `define({ lifecycle: { start: async ({ utils }) => utils.http(..) } })` left every call on `utils` unresolved, while `ctx.utils.http(..)` on the same arrow resolved. Two causes: - a bound name reads its pattern's type through param_type, which only an annotation fills; contextual typing (lambda_param_type) typed references to the parameter and never the names destructured from it. The bound name now takes the type (and shape) of the field it names from the contextual type, typed at its uses beside the plain parameter's, not through param_type, which overload rejection reads under negation. - an object literal written as a property of a typed literal had no scope, so functions in a nested slot took no parameter types. It now takes the type of the field it fills. Case 99 pins both, with controls: an `any` pattern and a parameter typed by an object type literal stay unresolved. Every other TypeScript case is unchanged (109/109). On a Deno connector repository (~750 endpoint definitions): known_edge rows 7,410 -> 8,923, ambiguous_unknown 35,820 -> 34,300, multi_inferred 63 -> 77, no edge lost; 194 of 198 hook calls of the framework's HTTP helper now resolve. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../src/globals.d.ts | 35 +++++++++++++ .../src/tsconfig.json | 1 + .../src/use.ts | 51 +++++++++++++++++++ .../99-contextual-destructured-param.edges | 9 ++++ .../99-contextual-destructured-param.entries | 5 ++ .../99-contextual-destructured-param.envelope | 5 ++ .../99-contextual-destructured-param.fields | 4 ++ ...ontextual-destructured-param.fields-oracle | 7 +++ ...ontextual-destructured-param.known-missing | 14 +++++ .../99-contextual-destructured-param.oracle | 8 +++ .../99-contextual-destructured-param.type-use | 12 +++++ ...contextual-destructured-param.types-oracle | 7 +++ .../engine/resolution/contextual-params.dl | 36 +++++++++++++ graph/typescript/souffle/decls_all.dl | 3 ++ 14 files changed, 197 insertions(+) create mode 100644 graph/test/typescript/cases/99-contextual-destructured-param/src/globals.d.ts create mode 100644 graph/test/typescript/cases/99-contextual-destructured-param/src/tsconfig.json create mode 100644 graph/test/typescript/cases/99-contextual-destructured-param/src/use.ts create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.edges create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.entries create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.envelope create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.fields create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.fields-oracle create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.known-missing create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.oracle create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.type-use create mode 100644 graph/test/typescript/expected/99-contextual-destructured-param.types-oracle diff --git a/graph/test/typescript/cases/99-contextual-destructured-param/src/globals.d.ts b/graph/test/typescript/cases/99-contextual-destructured-param/src/globals.d.ts new file mode 100644 index 000000000..bf77e7993 --- /dev/null +++ b/graph/test/typescript/cases/99-contextual-destructured-param/src/globals.d.ts @@ -0,0 +1,35 @@ +// The case's OWN standard library, for the reason 21 gives: under `noLib` the target has +// to be declared HERE or neither side of the comparison can name it. +// +// Every signature below is copied from lib.es5.d.ts / lib.es2015.promise.d.ts as written, +// because the shape of the callback parameter IS the case: `then` takes a UNION of a +// function type with `undefined | null`, and `map` declares its own `U` next to Array's `T`. +interface Array { + length: number; + forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void; + map(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; + filter(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): T[]; + some(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): boolean; +} +interface ReadonlyArray { + readonly length: number; + forEach(callbackfn: (value: T, index: number, array: readonly T[]) => void, thisArg?: any): void; + map(callbackfn: (value: T, index: number, array: readonly T[]) => U, thisArg?: any): U[]; + some(predicate: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: any): boolean; +} +interface PromiseLike { + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): PromiseLike; +} +interface Promise { + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise; + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise; +} +interface Object { toString(): string; } +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/99-contextual-destructured-param/src/tsconfig.json b/graph/test/typescript/cases/99-contextual-destructured-param/src/tsconfig.json new file mode 100644 index 000000000..c901fced9 --- /dev/null +++ b/graph/test/typescript/cases/99-contextual-destructured-param/src/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "noLib": true, "target": "es2022", "module": "commonjs" } } diff --git a/graph/test/typescript/cases/99-contextual-destructured-param/src/use.ts b/graph/test/typescript/cases/99-contextual-destructured-param/src/use.ts new file mode 100644 index 000000000..a0567197b --- /dev/null +++ b/graph/test/typescript/cases/99-contextual-destructured-param/src/use.ts @@ -0,0 +1,51 @@ +// A parameter destructured where its type comes from the context, not an annotation. +export interface Utils { + send(url: string): number; + sleep(ms: number): void; +} +export interface Ctx { + url: string; + utils: Utils; +} + +// typed by the const's function-type annotation: `utils.send` is Utils.send +export const viaConst: (ctx: Ctx) => number = ({ url, utils }) => { + return utils.send(url); +}; + +// the same arrow with a plain parameter (already resolved; must not change) +export const plainConst: (ctx: Ctx) => number = (ctx) => { + return ctx.utils.send(ctx.url); +}; + +// an annotated pattern (already resolved; must not change) +export function annotated({ url, utils }: Ctx): number { + return utils.send(url); +} + +// control: a destructured parameter with no type anywhere stays unresolved +export const untyped = ({ url, utils }: any) => { + return utils.send(url); +}; + +// control: a local object of another type with a member of the same name +export interface Other { send(url: string): string } +export const otherConst: (o: { utils: Other; url: string }) => string = ({ url, utils }) => { + return utils.send(url); +}; + +// written in a slot of a nested literal whose type comes from the call's parameter +type Slots = { start?: (ctx: Ctx) => number }; +export function define(seed: { name: string } & { lifecycle?: Slots }) { return seed; } +export const op = define({ + name: "op", + lifecycle: { + start: ({ url, utils }) => utils.send(url), + }, +}); +export const op2 = define({ + name: "op2", + lifecycle: { + start: (ctx) => ctx.utils.send(ctx.url), + }, +}); diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.edges b/graph/test/typescript/expected/99-contextual-destructured-param.edges new file mode 100644 index 000000000..679f22e1a --- /dev/null +++ b/graph/test/typescript/expected/99-contextual-destructured-param.edges @@ -0,0 +1,9 @@ +ambiguous_unknown METHOD_CALL use#otherConst(?,?,?) @L34 -> - +ambiguous_unknown METHOD_CALL use#untyped(any,any,any) @L28 -> - +known_edge FUNCTION_CALL use#() @L40 -> use#define({ name: string } & { lifecycle?: Slots }) +known_edge FUNCTION_CALL use#() @L46 -> use#define({ name: string } & { lifecycle?: Slots }) +known_edge METHOD_CALL use#(?,?,?) @L43 -> Utils#send(string) +known_edge METHOD_CALL use#(?) @L49 -> Utils#send(string) +known_edge METHOD_CALL use#annotated(Ctx,Ctx,Ctx) @L23 -> Utils#send(string) +known_edge METHOD_CALL use#plainConst(?) @L18 -> Utils#send(string) +known_edge METHOD_CALL use#viaConst(?,?,?) @L13 -> Utils#send(string) diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.entries b/graph/test/typescript/expected/99-contextual-destructured-param.entries new file mode 100644 index 000000000..0b69514d5 --- /dev/null +++ b/graph/test/typescript/expected/99-contextual-destructured-param.entries @@ -0,0 +1,5 @@ +── entry_point (4) ── + exported_from_entry_module use#annotated use.ts:22 + exported_from_entry_module use#define use.ts:39 + unimported_module globals# globals.d.ts:1 + unimported_module use# use.ts:1 diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.envelope b/graph/test/typescript/expected/99-contextual-destructured-param.envelope new file mode 100644 index 000000000..1f7efd137 --- /dev/null +++ b/graph/test/typescript/expected/99-contextual-destructured-param.envelope @@ -0,0 +1,5 @@ +value use#@12:24 -> use#@12 +value use#@17:26 -> use#@17 +value use#@33:26 -> use#@33 +value use#@38:24 -> use#@43 +value use#@38:24 -> use#@49 diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.fields b/graph/test/typescript/expected/99-contextual-destructured-param.fields new file mode 100644 index 000000000..e203e14a0 --- /dev/null +++ b/graph/test/typescript/expected/99-contextual-destructured-param.fields @@ -0,0 +1,4 @@ +known_edge read use#(?) -> Ctx#url +known_edge read use#(?) -> Ctx#utils +known_edge read use#plainConst(?) -> Ctx#url +known_edge read use#plainConst(?) -> Ctx#utils diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.fields-oracle b/graph/test/typescript/expected/99-contextual-destructured-param.fields-oracle new file mode 100644 index 000000000..c9021690f --- /dev/null +++ b/graph/test/typescript/expected/99-contextual-destructured-param.fields-oracle @@ -0,0 +1,7 @@ +99-contextual-destructured-param [fields] + precision 1.0000 (4 correct, 0 wrong) + recall 1.0000 (4 of 4 the compiler resolved) + sites 4 resolved 4 (100.0%) + tiers known_edge=4 + access read=4 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.known-missing b/graph/test/typescript/expected/99-contextual-destructured-param.known-missing new file mode 100644 index 000000000..e86f77872 --- /dev/null +++ b/graph/test/typescript/expected/99-contextual-destructured-param.known-missing @@ -0,0 +1,14 @@ +# Accepted gaps for 99-contextual-destructured-param — each line is an edge the TypeScript compiler +# resolves and this engine does not. A NEW missing edge fails the suite; +# a line here that STARTS working also fails, so the debt cannot rot. +# +# The engine DOES resolve these three (they show as `extra` with the same target): the edge differs +# only in its caller's label. A function whose parameter is destructured is labelled with one slot +# per bound name as well as the pattern, `(?,?,?)` for `({ url, utils })`, where the compiler names +# one parameter. `annotated` had this before contextual typing was added; it is the label, not the edge. +use#(?) -> Utils#send(string) +use#annotated(Ctx) -> Utils#send(string) +use#viaConst(?) -> Utils#send(string) +# A parameter typed by an object type LITERAL ({ utils: Other }) has a shape, not a type +# declaration, and a bound name reads only a declaration's fields. Not covered yet. +use#otherConst(?) -> Other#send(string) diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.oracle b/graph/test/typescript/expected/99-contextual-destructured-param.oracle new file mode 100644 index 000000000..90a473678 --- /dev/null +++ b/graph/test/typescript/expected/99-contextual-destructured-param.oracle @@ -0,0 +1,8 @@ +oracle=7 engine=6 agree=3 missing=4 (known 4, NEW 0) extra=3 + known use#(?) -> Utils#send(string) + known use#annotated(Ctx) -> Utils#send(string) + known use#otherConst(?) -> Other#send(string) + known use#viaConst(?) -> Utils#send(string) + extra use#(?,?,?) -> Utils#send(string) + extra use#annotated(Ctx,Ctx,Ctx) -> Utils#send(string) + extra use#viaConst(?,?,?) -> Utils#send(string) diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.type-use b/graph/test/typescript/expected/99-contextual-destructured-param.type-use new file mode 100644 index 000000000..271aea409 --- /dev/null +++ b/graph/test/typescript/expected/99-contextual-destructured-param.type-use @@ -0,0 +1,12 @@ +known_edge FIELD_TYPE 0 Ctx [FIELD] -> Utils +known_edge FIELD_TYPE 2 use [METHOD_PARAM] -> Slots +known_edge FIELD_TYPE 2 use [VARIABLE] -> Other +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Ctx +known_edge METHOD_PARAM 1 use [VARIABLE] -> Ctx +known_edge METHOD_PARAM 2 Slots [TYPE] -> Ctx +known_edge METHOD_RETURN 0 Promise [METHOD] -> Promise +known_edge METHOD_RETURN 0 PromiseLike [METHOD] -> PromiseLike +known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function +known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function +known_edge TYPE_ELEMENT 4 Promise [METHOD_PARAM] -> PromiseLike +known_edge TYPE_ELEMENT 4 PromiseLike [METHOD_PARAM] -> PromiseLike diff --git a/graph/test/typescript/expected/99-contextual-destructured-param.types-oracle b/graph/test/typescript/expected/99-contextual-destructured-param.types-oracle new file mode 100644 index 000000000..22b7a3067 --- /dev/null +++ b/graph/test/typescript/expected/99-contextual-destructured-param.types-oracle @@ -0,0 +1,7 @@ +99-contextual-destructured-param [types] + precision 1.0000 (10 correct, 0 wrong) + recall 1.0000 (10 of 10 the compiler resolved) + sites 17 resolved 17 (100.0%) + tiers known_edge=17 + contexts FIELD_TYPE=3 METHOD_PARAM=4 METHOD_RETURN=3 SUPER_TYPE=2 TYPE_ELEMENT=5 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/typescript/engine/resolution/contextual-params.dl b/graph/typescript/engine/resolution/contextual-params.dl index 99b05b5a1..e54f5131c 100644 --- a/graph/typescript/engine/resolution/contextual-params.dl +++ b/graph/typescript/engine/resolution/contextual-params.dl @@ -370,6 +370,33 @@ expr_decl_ref(e, c) :- expr_referenced("client", "PARAMETER", p, e), expr_type(e, prov, t) :- expr_referenced("client", "PARAMETER", p, e), lambda_param_type(p, prov, t). +// ── …AND THE NAMES A CONTEXTUALLY TYPED PARAMETER IS DESTRUCTURED INTO ────── +// `const poll: (ctx: TickCtx) => R = async ({ data, utils }) => utils.http(..)`, and the same +// arrow written where an interface member or a function-typed field is expected. The pattern +// parameter takes its type from that context above, but a bound name reads the pattern's type +// through param_type (reference-types.dl, binding_pattern_type), which only an annotation fills: +// so `utils` stayed untyped and every call on it was unresolved, while `ctx.utils.http(..)` on +// the same arrow resolved. The bound name takes the type of the field it names, exactly as for +// an annotated pattern; its uses are typed here, beside the plain parameter's, and not through +// param_type, which overload rejection reads under negation. +lambda_binding_target(bh, prov, t) :- param_binding(_, _, pos, "PROPERTY", _, m, bh), + param_decl(_, _, pos, _, m, ph), + lambda_param_type(ph, prov, t). +lambda_binding_type(bh, prov, ft) :- param_binding(_, _, _, "PROPERTY", src, _, bh), + lambda_binding_target(bh, _, t), + scope_sibling(t, sib), + declared_field(sib, src, _, f), + field_type(f, prov, ft). +lambda_binding_shape(bh, sh) :- param_binding(_, _, _, "PROPERTY", src, _, bh), + lambda_binding_target(bh, _, t), + scope_sibling(t, sib), + declared_field(sib, src, _, f), + field_shape(f, sh). +expr_type(e, prov, t) :- expr_referenced("client", "PARAMETER", bh, e), + lambda_binding_type(bh, prov, t). +expr_shape(e, sh) :- expr_referenced("client", "PARAMETER", bh, e), + lambda_binding_shape(bh, sh). + // ── …AND A CALLBACK THAT RECEIVES A MODULE NAMESPACE ──────────────────────── // `import("./m").then((m) => m.run(1))` — the same namespace as the awaited form, only // reached through a callback. `await` is not required for a dynamic import to be @@ -653,6 +680,15 @@ objlit_ctx_ref(l, r) :- expr_child("client", ce, "ARGUMENT", pos, l), param_type_ref(_, r, p). objlit_ctx_scope(l, s) :- objlit_ctx_ref(l, r), ref_type_target(r, _, s). objlit_ctx_scope(l, s) :- objlit_ctx_ref(l, r), ref_shape_target(r, s). +// …and a literal written as a PROPERTY of a typed literal takes the type of the field it fills: +// `define({ lifecycle: { start: async (ctx) => .. } })`, where the parameter is +// `{ lifecycle?: Slots }`. The outer literal had a scope and the inner one none, so every +// function in a nested slot (a config's hooks, a router's handlers) took no parameter types. +objlit_ctx_ref(l, r) :- objlit_prop(p, name, l), + expr_kind("client", "OBJECT_LITERAL", _, l), + objlit_ctx_scope(p, s), + field_in_scope(s, name, _, f), + field_type_ref(_, r, f). // a property is a KEY and a VALUE at the same position under the literal; a method is its own row objlit_prop(l, name, val) :- expr_child("client", l, "OBJECT_PROPERTY_KEY", pos, key), diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl index a703e6015..223d5f526 100644 --- a/graph/typescript/souffle/decls_all.dl +++ b/graph/typescript/souffle/decls_all.dl @@ -64,6 +64,9 @@ .decl builtin_promise_ref(c0:symbol,c1:symbol) .decl lambda_param_ref(c0:symbol,c1:symbol) .decl fn_value_kind(c0:symbol) +.decl lambda_binding_shape(c0:symbol,c1:symbol) +.decl lambda_binding_target(c0:symbol,c1:symbol,c2:symbol) +.decl lambda_binding_type(c0:symbol,c1:symbol,c2:symbol) .decl lambda_param_shape(c0:symbol,c1:symbol) .decl lambda_param_namespace(c0:symbol,c1:symbol,c2:symbol) .decl lambda_param_type(c0:symbol,c1:symbol,c2:symbol)