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 00000000..bf77e799 --- /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 00000000..c901fced --- /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 00000000..a0567197 --- /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 00000000..679f22e1 --- /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 00000000..0b69514d --- /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 00000000..1f7efd13 --- /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 00000000..e203e14a --- /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 00000000..c9021690 --- /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 00000000..e86f7787 --- /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 00000000..90a47367 --- /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 00000000..271aea40 --- /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 00000000..22b7a306 --- /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 99b05b5a..e54f5131 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 a703e601..223d5f52 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)