From 5ae943e3b6ad9ed205b253a02b837f3dc0051bd7 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:39:18 -0700 Subject: [PATCH 1/2] typescript: type a callback parameter from context through an optional callback and without a staged lib `p.then(u => u.m())` left `u` untyped: `then`'s callback parameter is written `((value: T) => R) | undefined | null`, and the rule only looked for a bare function type. Unwrap unions and parentheses to reach it. With no standard library staged (a default index), `xs.map(u => u.m())`, `forEach`, `filter`, `reduce` and `promise.then` had no declaration to read a signature from, so the parameter stayed untyped while `for (const u of xs)` resolved. Type the element parameter of the built-in element callbacks from the receiver's container reference, and `then`'s value from a Promise/PromiseLike reference. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../src/globals.d.ts | 35 +++++++ .../src/tsconfig.json | 1 + .../80-contextual-lambda-param/src/use.ts | 97 +++++++++++++++++++ .../src/tsconfig.json | 1 + .../src/use.ts | 87 +++++++++++++++++ .../expected/80-contextual-lambda-param.edges | 49 ++++++++++ .../80-contextual-lambda-param.entries | 16 +++ .../80-contextual-lambda-param.fields | 3 + .../80-contextual-lambda-param.fields-oracle | 7 ++ .../80-contextual-lambda-param.oracle | 1 + .../80-contextual-lambda-param.type-use | 22 +++++ .../80-contextual-lambda-param.types-oracle | 7 ++ .../81-contextual-lambda-param-no-lib.edges | 36 +++++++ .../81-contextual-lambda-param-no-lib.entries | 12 +++ ...81-contextual-lambda-param-no-lib.envelope | 4 + .../81-contextual-lambda-param-no-lib.fields | 3 + ...ntextual-lambda-param-no-lib.fields-oracle | 7 ++ .../81-contextual-lambda-param-no-lib.oracle | 1 + ...81-contextual-lambda-param-no-lib.type-use | 19 ++++ ...ontextual-lambda-param-no-lib.types-oracle | 7 ++ .../engine/resolution/contextual-params.dl | 63 +++++++++++- .../engine/resolution/type-resolution.dl | 3 +- graph/typescript/souffle/decls_all.dl | 3 + 23 files changed, 481 insertions(+), 3 deletions(-) create mode 100644 graph/test/typescript/cases/80-contextual-lambda-param/src/globals.d.ts create mode 100644 graph/test/typescript/cases/80-contextual-lambda-param/src/tsconfig.json create mode 100644 graph/test/typescript/cases/80-contextual-lambda-param/src/use.ts create mode 100644 graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/tsconfig.json create mode 100644 graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/use.ts create mode 100644 graph/test/typescript/expected/80-contextual-lambda-param.edges create mode 100644 graph/test/typescript/expected/80-contextual-lambda-param.entries create mode 100644 graph/test/typescript/expected/80-contextual-lambda-param.fields create mode 100644 graph/test/typescript/expected/80-contextual-lambda-param.fields-oracle create mode 100644 graph/test/typescript/expected/80-contextual-lambda-param.oracle create mode 100644 graph/test/typescript/expected/80-contextual-lambda-param.type-use create mode 100644 graph/test/typescript/expected/80-contextual-lambda-param.types-oracle create mode 100644 graph/test/typescript/expected/81-contextual-lambda-param-no-lib.edges create mode 100644 graph/test/typescript/expected/81-contextual-lambda-param-no-lib.entries create mode 100644 graph/test/typescript/expected/81-contextual-lambda-param-no-lib.envelope create mode 100644 graph/test/typescript/expected/81-contextual-lambda-param-no-lib.fields create mode 100644 graph/test/typescript/expected/81-contextual-lambda-param-no-lib.fields-oracle create mode 100644 graph/test/typescript/expected/81-contextual-lambda-param-no-lib.oracle create mode 100644 graph/test/typescript/expected/81-contextual-lambda-param-no-lib.type-use create mode 100644 graph/test/typescript/expected/81-contextual-lambda-param-no-lib.types-oracle diff --git a/graph/test/typescript/cases/80-contextual-lambda-param/src/globals.d.ts b/graph/test/typescript/cases/80-contextual-lambda-param/src/globals.d.ts new file mode 100644 index 000000000..bf77e7993 --- /dev/null +++ b/graph/test/typescript/cases/80-contextual-lambda-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/80-contextual-lambda-param/src/tsconfig.json b/graph/test/typescript/cases/80-contextual-lambda-param/src/tsconfig.json new file mode 100644 index 000000000..c901fced9 --- /dev/null +++ b/graph/test/typescript/cases/80-contextual-lambda-param/src/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "noLib": true, "target": "es2022", "module": "commonjs" } } diff --git a/graph/test/typescript/cases/80-contextual-lambda-param/src/use.ts b/graph/test/typescript/cases/80-contextual-lambda-param/src/use.ts new file mode 100644 index 000000000..0184ceedb --- /dev/null +++ b/graph/test/typescript/cases/80-contextual-lambda-param/src/use.ts @@ -0,0 +1,97 @@ +// A callback parameter with no annotation takes its type from the signature it is passed to: +// `Array.map((value: T) => U)`, `forEach`, `Promise.then((value: T) => …)`. Two classes +// share every member name, so a name match cannot pass for a resolution — the owner is the answer. +// +// Each shape is written twice, `(u) => …` and `u => …`, because a parameter written without +// parentheses is the commoner spelling and the two must type alike. +export class User { + display(): string { + return 'd'; + } + save(): string { + return 's'; + } + touch(): string { + return 't'; + } +} +export class Admin { + display(): string { + return 'D'; + } + save(): string { + return 'S'; + } + touch(): string { + return 'T'; + } +} + +export class Repo { + constructor(private readonly pending: Promise, private readonly users: User[]) {} + all(): User[] { + return this.users; + } + one(): Promise { + return this.pending; + } + readonlyAll(): readonly User[] { + return this.users; + } +} + +export interface Handler { + handle(n: number): number; +} + +// ── map: the method declares its own `U` beside Array's `T` ───────────────── +export function mapParen(r: Repo): string[] { + return r.all().map((u) => u.display()); +} +export function mapBare(r: Repo): string[] { + return r.all().map(u => u.display()); +} +export function mapDeclared(hs: Handler[]): number[] { + return hs.map(h => h.handle(1)); +} + +// ── forEach / filter / some: no type parameter of their own ─────────────── +export function forEachParen(r: Repo): void { + r.all().forEach((u) => { + u.save(); + }); +} +export function forEachBare(r: Repo): void { + r.all().forEach(u => { + u.save(); + }); +} +export function filterBare(r: Repo): User[] { + return r.all().filter(u => u.save() !== ''); +} +export function someReadonly(r: Repo): boolean { + return r.readonlyAll().some(u => u.touch() === 't'); +} + +// ── then: the callback's parameter type is `((value: T) => …) | undefined | null` ── +export function thenParen(r: Repo): Promise { + return r.one().then((u) => u.touch()); +} +export function thenBare(r: Repo): Promise { + return r.one().then(u => u.touch()); +} +export function thenDeclared(p: Promise): Promise { + return p.then(a => a.touch()); +} + +// ── CONTROLS: these must keep resolving, and only to their own owner ──────── +export function ctlAnnotated(r: Repo): string[] { + return r.all().map((u: User) => u.save()); +} +export function ctlOtherOwner(as: Admin[]): void { + as.forEach((a) => a.display()); +} +// `catch`'s parameter is `any`: nothing to type, so `e.touch()` must stay unresolved. +export function ctlCatchAny(r: Repo): Promise { + return r.one().catch(e => e.touch()); +} diff --git a/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/tsconfig.json b/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/tsconfig.json new file mode 100644 index 000000000..c3d356335 --- /dev/null +++ b/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "target": "es2022", "module": "commonjs" } } diff --git a/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/use.ts b/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/use.ts new file mode 100644 index 000000000..11171b358 --- /dev/null +++ b/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/use.ts @@ -0,0 +1,87 @@ +// 80's shapes with NO standard library in the graph, which is what a default index is: +// `map`, `forEach` and `then` have no declaration to read a callback signature from, so +// the parameter's type has to come off the receiver's own reference, the way a `for…of` +// binding over the same receiver already does. Two classes share every member name, so a +// name match cannot pass for a resolution. +export class User { + display(): string { + return 'd'; + } + save(): string { + return 's'; + } + touch(): string { + return 't'; + } +} +export class Admin { + display(): string { + return 'D'; + } + save(): string { + return 'S'; + } + touch(): string { + return 'T'; + } +} + +export class Repo { + constructor(private readonly pending: Promise, private readonly users: User[]) {} + all(): User[] { + return this.users; + } + one(): Promise { + return this.pending; + } + readonlyAll(): ReadonlyArray { + return this.users; + } +} + +export function mapBare(r: Repo): string[] { + return r.all().map(u => u.display()); +} +export function forEachParen(r: Repo): void { + r.all().forEach((u) => { + u.save(); + }); +} +export function findReadonly(r: Repo): User | undefined { + return r.readonlyAll().find(u => u.touch() === 't'); +} +export function everyParam(as: Admin[]): boolean { + return as.every(a => a.save() !== ''); +} +// `reduce`'s first parameter is the accumulator: only the second is the element. +export function reduceElement(us: User[]): string { + return us.reduce((acc, u) => acc + u.save(), ''); +} +export function thenBare(r: Repo): Promise { + return r.one().then(u => u.touch()); +} +export function thenParam(p: Promise): Promise { + return p.then(a => a.display()); +} + +// ── CONTROLS ──────────────────────────────────────────────────────────────── +// A class of the user's own with a `then` and a `map`: its own signatures type the +// callback, and nothing about Promise or Array may be added to them. +export class Lazy { + then(cb: (a: Admin) => string): string { + return cb(new Admin()); + } + map(cb: (u: User) => string): string { + return cb(new User()); + } +} +export function ctlOwnThen(l: Lazy): string { + return l.then(a => a.touch()); +} +export function ctlOwnMap(l: Lazy): string { + return l.map(u => u.touch()); +} +// An annotated parameter keeps its annotation. +export function ctlAnnotated(us: User[]): string[] { + return us.map((u: User) => u.save()); +} diff --git a/graph/test/typescript/expected/80-contextual-lambda-param.edges b/graph/test/typescript/expected/80-contextual-lambda-param.edges new file mode 100644 index 000000000..709759a54 --- /dev/null +++ b/graph/test/typescript/expected/80-contextual-lambda-param.edges @@ -0,0 +1,49 @@ +ambiguous_unknown METHOD_CALL use#(?) @L96 -> - +callback_registered METHOD_CALL use#ctlAnnotated(Repo) @L89 -> use#(User) +callback_registered METHOD_CALL use#ctlCatchAny(Repo) @L96 -> use#(?) +callback_registered METHOD_CALL use#ctlOtherOwner(Admin[]) @L92 -> use#(?) +callback_registered METHOD_CALL use#filterBare(Repo) @L70 -> use#(?) +callback_registered METHOD_CALL use#forEachBare(Repo) @L65 -> use#(?) +callback_registered METHOD_CALL use#forEachParen(Repo) @L60 -> use#(?) +callback_registered METHOD_CALL use#mapBare(Repo) @L52 -> use#(?) +callback_registered METHOD_CALL use#mapDeclared(Handler[]) @L55 -> use#(?) +callback_registered METHOD_CALL use#mapParen(Repo) @L49 -> use#(?) +callback_registered METHOD_CALL use#someReadonly(Repo) @L73 -> use#(?) +callback_registered METHOD_CALL use#thenBare(Repo) @L81 -> use#(?) +callback_registered METHOD_CALL use#thenDeclared(Promise) @L84 -> use#(?) +callback_registered METHOD_CALL use#thenParen(Repo) @L78 -> use#(?) +known_edge METHOD_CALL use#(?) @L49 -> User#display() +known_edge METHOD_CALL use#(?) @L52 -> User#display() +known_edge METHOD_CALL use#(?) @L55 -> Handler#handle(number) +known_edge METHOD_CALL use#(?) @L61 -> User#save() +known_edge METHOD_CALL use#(?) @L66 -> User#save() +known_edge METHOD_CALL use#(?) @L70 -> User#save() +known_edge METHOD_CALL use#(?) @L73 -> User#touch() +known_edge METHOD_CALL use#(?) @L78 -> User#touch() +known_edge METHOD_CALL use#(?) @L81 -> User#touch() +known_edge METHOD_CALL use#(?) @L84 -> Admin#touch() +known_edge METHOD_CALL use#(User) @L89 -> User#save() +known_edge METHOD_CALL use#(?) @L92 -> Admin#display() +known_edge METHOD_CALL use#ctlAnnotated(Repo) @L89 -> Array#map((value: T, index: number, array: T[]) =,any) +known_edge METHOD_CALL use#ctlAnnotated(Repo) @L89 -> Repo#all() +known_edge METHOD_CALL use#ctlCatchAny(Repo) @L96 -> Promise#catch(((reason: any) =TResult) +known_edge METHOD_CALL use#ctlCatchAny(Repo) @L96 -> Repo#one() +known_edge METHOD_CALL use#ctlOtherOwner(Admin[]) @L92 -> Array#forEach((value: T, index: number, array: T[]) =,any) +known_edge METHOD_CALL use#filterBare(Repo) @L70 -> Array#filter((value: T, index: number, array: T[]) =,any) +known_edge METHOD_CALL use#filterBare(Repo) @L70 -> Repo#all() +known_edge METHOD_CALL use#forEachBare(Repo) @L65 -> Array#forEach((value: T, index: number, array: T[]) =,any) +known_edge METHOD_CALL use#forEachBare(Repo) @L65 -> Repo#all() +known_edge METHOD_CALL use#forEachParen(Repo) @L60 -> Array#forEach((value: T, index: number, array: T[]) =,any) +known_edge METHOD_CALL use#forEachParen(Repo) @L60 -> Repo#all() +known_edge METHOD_CALL use#mapBare(Repo) @L52 -> Array#map((value: T, index: number, array: T[]) =,any) +known_edge METHOD_CALL use#mapBare(Repo) @L52 -> Repo#all() +known_edge METHOD_CALL use#mapDeclared(Handler[]) @L55 -> Array#map((value: T, index: number, array: T[]) =,any) +known_edge METHOD_CALL use#mapParen(Repo) @L49 -> Array#map((value: T, index: number, array: T[]) =,any) +known_edge METHOD_CALL use#mapParen(Repo) @L49 -> Repo#all() +known_edge METHOD_CALL use#someReadonly(Repo) @L73 -> ReadonlyArray#some((value: T, index: number, array: readonly T[]) =,any) +known_edge METHOD_CALL use#someReadonly(Repo) @L73 -> Repo#readonlyAll() +known_edge METHOD_CALL use#thenBare(Repo) @L81 -> Promise#then(((value: T) =TResult1,((reason: any) =TResult2) +known_edge METHOD_CALL use#thenBare(Repo) @L81 -> Repo#one() +known_edge METHOD_CALL use#thenDeclared(Promise) @L84 -> Promise#then(((value: T) =TResult1,((reason: any) =TResult2) +known_edge METHOD_CALL use#thenParen(Repo) @L78 -> Promise#then(((value: T) =TResult1,((reason: any) =TResult2) +known_edge METHOD_CALL use#thenParen(Repo) @L78 -> Repo#one() diff --git a/graph/test/typescript/expected/80-contextual-lambda-param.entries b/graph/test/typescript/expected/80-contextual-lambda-param.entries new file mode 100644 index 000000000..bec0b3b18 --- /dev/null +++ b/graph/test/typescript/expected/80-contextual-lambda-param.entries @@ -0,0 +1,16 @@ +── entry_point (15) ── + exported_from_entry_module use#ctlAnnotated use.ts:88 + exported_from_entry_module use#ctlCatchAny use.ts:95 + exported_from_entry_module use#ctlOtherOwner use.ts:91 + exported_from_entry_module use#filterBare use.ts:69 + exported_from_entry_module use#forEachBare use.ts:64 + exported_from_entry_module use#forEachParen use.ts:59 + exported_from_entry_module use#mapBare use.ts:51 + exported_from_entry_module use#mapDeclared use.ts:54 + exported_from_entry_module use#mapParen use.ts:48 + exported_from_entry_module use#someReadonly use.ts:72 + exported_from_entry_module use#thenBare use.ts:80 + exported_from_entry_module use#thenDeclared use.ts:83 + exported_from_entry_module use#thenParen use.ts:77 + unimported_module globals# globals.d.ts:1 + unimported_module use# use.ts:1 diff --git a/graph/test/typescript/expected/80-contextual-lambda-param.fields b/graph/test/typescript/expected/80-contextual-lambda-param.fields new file mode 100644 index 000000000..70b51f318 --- /dev/null +++ b/graph/test/typescript/expected/80-contextual-lambda-param.fields @@ -0,0 +1,3 @@ +known_edge read Repo#all() -> Repo#users +known_edge read Repo#one() -> Repo#pending +known_edge read Repo#readonlyAll() -> Repo#users diff --git a/graph/test/typescript/expected/80-contextual-lambda-param.fields-oracle b/graph/test/typescript/expected/80-contextual-lambda-param.fields-oracle new file mode 100644 index 000000000..50318431d --- /dev/null +++ b/graph/test/typescript/expected/80-contextual-lambda-param.fields-oracle @@ -0,0 +1,7 @@ +80-contextual-lambda-param [fields] + precision 1.0000 (3 correct, 0 wrong) + recall 1.0000 (3 of 3 the compiler resolved) + sites 3 resolved 3 (100.0%) + tiers known_edge=3 + access read=3 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/80-contextual-lambda-param.oracle b/graph/test/typescript/expected/80-contextual-lambda-param.oracle new file mode 100644 index 000000000..540c068bb --- /dev/null +++ b/graph/test/typescript/expected/80-contextual-lambda-param.oracle @@ -0,0 +1 @@ +oracle=35 engine=35 agree=35 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/80-contextual-lambda-param.type-use b/graph/test/typescript/expected/80-contextual-lambda-param.type-use new file mode 100644 index 000000000..c5660109f --- /dev/null +++ b/graph/test/typescript/expected/80-contextual-lambda-param.type-use @@ -0,0 +1,22 @@ +known_edge METHOD_PARAM 0 Repo [METHOD_PARAM] -> Promise +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Promise +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Repo +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> User +known_edge METHOD_RETURN 0 Promise [METHOD] -> Promise +known_edge METHOD_RETURN 0 PromiseLike [METHOD] -> PromiseLike +known_edge METHOD_RETURN 0 Repo [METHOD] -> Promise +known_edge METHOD_RETURN 0 use [METHOD] -> Promise +known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function +known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function +known_edge TYPE_ARGUMENT 1 Repo [METHOD] -> User +known_edge TYPE_ARGUMENT 1 Repo [METHOD_PARAM] -> User +known_edge TYPE_ARGUMENT 1 use [METHOD_PARAM] -> Admin +known_edge TYPE_ELEMENT 1 Repo [METHOD] -> User +known_edge TYPE_ELEMENT 1 Repo [METHOD_PARAM] -> User +known_edge TYPE_ELEMENT 1 use [METHOD] -> User +known_edge TYPE_ELEMENT 1 use [METHOD_PARAM] -> Admin +known_edge TYPE_ELEMENT 1 use [METHOD_PARAM] -> Handler +known_edge TYPE_ELEMENT 2 Repo [METHOD] -> User +known_edge TYPE_ELEMENT 2 use [METHOD] -> User +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/80-contextual-lambda-param.types-oracle b/graph/test/typescript/expected/80-contextual-lambda-param.types-oracle new file mode 100644 index 000000000..75a68397a --- /dev/null +++ b/graph/test/typescript/expected/80-contextual-lambda-param.types-oracle @@ -0,0 +1,7 @@ +80-contextual-lambda-param [types] + precision 1.0000 (12 correct, 0 wrong) + recall 1.0000 (12 of 12 the compiler resolved) + sites 38 resolved 38 (100.0%) + tiers known_edge=38 + contexts METHOD_PARAM=13 METHOD_RETURN=8 SUPER_TYPE=2 TYPE_ARGUMENT=3 TYPE_ELEMENT=12 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.edges b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.edges new file mode 100644 index 000000000..3a15f2a14 --- /dev/null +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.edges @@ -0,0 +1,36 @@ +ambiguous_unknown METHOD_CALL use#ctlAnnotated(User[]) @L86 -> - +ambiguous_unknown METHOD_CALL use#everyParam(Admin[]) @L54 -> - +ambiguous_unknown METHOD_CALL use#findReadonly(Repo) @L51 -> - +ambiguous_unknown METHOD_CALL use#forEachParen(Repo) @L46 -> - +ambiguous_unknown METHOD_CALL use#mapBare(Repo) @L43 -> - +ambiguous_unknown METHOD_CALL use#reduceElement(User[]) @L58 -> - +ambiguous_unknown METHOD_CALL use#thenBare(Repo) @L61 -> - +ambiguous_unknown METHOD_CALL use#thenParam(Promise) @L64 -> - +callback_registered METHOD_CALL use#ctlAnnotated(User[]) @L86 -> use#(User) +callback_registered METHOD_CALL use#everyParam(Admin[]) @L54 -> use#(?) +callback_registered METHOD_CALL use#findReadonly(Repo) @L51 -> use#(?) +callback_registered METHOD_CALL use#forEachParen(Repo) @L46 -> use#(?) +callback_registered METHOD_CALL use#mapBare(Repo) @L43 -> use#(?) +callback_registered METHOD_CALL use#reduceElement(User[]) @L58 -> use#(?,?) +callback_registered METHOD_CALL use#thenBare(Repo) @L61 -> use#(?) +callback_registered METHOD_CALL use#thenParam(Promise) @L64 -> use#(?) +known_edge CONSTRUCTOR_CALL Lazy#map((u: User) =) @L75 -> User#() +known_edge CONSTRUCTOR_CALL Lazy#then((a: Admin) =) @L72 -> Admin#() +known_edge FUNCTION_CALL Lazy#map((u: User) =) @L75 -> Lazy#(User) +known_edge FUNCTION_CALL Lazy#then((a: Admin) =) @L72 -> Lazy#(Admin) +known_edge METHOD_CALL use#(?) @L43 -> User#display() +known_edge METHOD_CALL use#(?) @L47 -> User#save() +known_edge METHOD_CALL use#(?) @L51 -> User#touch() +known_edge METHOD_CALL use#(?) @L54 -> Admin#save() +known_edge METHOD_CALL use#(?,?) @L58 -> User#save() +known_edge METHOD_CALL use#(?) @L61 -> User#touch() +known_edge METHOD_CALL use#(?) @L64 -> Admin#display() +known_edge METHOD_CALL use#(?) @L79 -> Admin#touch() +known_edge METHOD_CALL use#(?) @L82 -> User#touch() +known_edge METHOD_CALL use#(User) @L86 -> User#save() +known_edge METHOD_CALL use#ctlOwnMap(Lazy) @L82 -> Lazy#map((u: User) =) +known_edge METHOD_CALL use#ctlOwnThen(Lazy) @L79 -> Lazy#then((a: Admin) =) +known_edge METHOD_CALL use#findReadonly(Repo) @L51 -> Repo#readonlyAll() +known_edge METHOD_CALL use#forEachParen(Repo) @L46 -> Repo#all() +known_edge METHOD_CALL use#mapBare(Repo) @L43 -> Repo#all() +known_edge METHOD_CALL use#thenBare(Repo) @L61 -> Repo#one() diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.entries b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.entries new file mode 100644 index 000000000..789f8d25a --- /dev/null +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.entries @@ -0,0 +1,12 @@ +── entry_point (11) ── + exported_from_entry_module use#ctlAnnotated use.ts:85 + exported_from_entry_module use#ctlOwnMap use.ts:81 + exported_from_entry_module use#ctlOwnThen use.ts:78 + exported_from_entry_module use#everyParam use.ts:53 + exported_from_entry_module use#findReadonly use.ts:50 + exported_from_entry_module use#forEachParen use.ts:45 + exported_from_entry_module use#mapBare use.ts:42 + exported_from_entry_module use#reduceElement use.ts:57 + exported_from_entry_module use#thenBare use.ts:60 + exported_from_entry_module use#thenParam use.ts:63 + unimported_module use# use.ts:1 diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.envelope b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.envelope new file mode 100644 index 000000000..58380ef52 --- /dev/null +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.envelope @@ -0,0 +1,4 @@ +value use#@71:12 -> use#@79 +value use#@74:11 -> use#@82 +value_call use#Lazy.map -> use#@82 +value_call use#Lazy.then -> use#@79 diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.fields b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.fields new file mode 100644 index 000000000..70b51f318 --- /dev/null +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.fields @@ -0,0 +1,3 @@ +known_edge read Repo#all() -> Repo#users +known_edge read Repo#one() -> Repo#pending +known_edge read Repo#readonlyAll() -> Repo#users diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.fields-oracle b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.fields-oracle new file mode 100644 index 000000000..7af09bb25 --- /dev/null +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.fields-oracle @@ -0,0 +1,7 @@ +81-contextual-lambda-param-no-lib [fields] + precision 1.0000 (3 correct, 0 wrong) + recall 1.0000 (3 of 3 the compiler resolved) + sites 3 resolved 3 (100.0%) + tiers known_edge=3 + access read=3 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.oracle b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.oracle new file mode 100644 index 000000000..b063a5494 --- /dev/null +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.oracle @@ -0,0 +1 @@ +oracle=20 engine=20 agree=20 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.type-use b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.type-use new file mode 100644 index 000000000..8be6c0dc4 --- /dev/null +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.type-use @@ -0,0 +1,19 @@ +ambiguous_unknown METHOD_PARAM 0 Repo [METHOD_PARAM] -> - +ambiguous_unknown METHOD_PARAM 0 use [METHOD_PARAM] -> - +ambiguous_unknown METHOD_RETURN 0 Repo [METHOD] -> - +ambiguous_unknown METHOD_RETURN 0 use [METHOD] -> - +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Lazy +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Repo +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> User +known_edge METHOD_PARAM 1 Lazy [METHOD_PARAM] -> Admin +known_edge METHOD_PARAM 1 Lazy [METHOD_PARAM] -> User +known_edge OBJECT_CREATION_TYPE 0 Lazy [EXPRESSION] -> Admin +known_edge OBJECT_CREATION_TYPE 0 Lazy [EXPRESSION] -> User +known_edge TYPE_ARGUMENT 1 Repo [METHOD] -> User +known_edge TYPE_ARGUMENT 1 Repo [METHOD_PARAM] -> User +known_edge TYPE_ARGUMENT 1 use [METHOD_PARAM] -> Admin +known_edge TYPE_ELEMENT 1 Repo [METHOD] -> User +known_edge TYPE_ELEMENT 1 Repo [METHOD_PARAM] -> User +known_edge TYPE_ELEMENT 1 use [METHOD] -> User +known_edge TYPE_ELEMENT 1 use [METHOD_PARAM] -> Admin +known_edge TYPE_ELEMENT 1 use [METHOD_PARAM] -> User diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.types-oracle b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.types-oracle new file mode 100644 index 000000000..aefc3fa34 --- /dev/null +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.types-oracle @@ -0,0 +1,7 @@ +81-contextual-lambda-param-no-lib [types] + precision 1.0000 (7 correct, 0 wrong) + recall 1.0000 (7 of 7 the compiler resolved) + sites 27 resolved 21 (77.8%) + tiers ambiguous_unknown=6 known_edge=21 + contexts METHOD_PARAM=11 METHOD_RETURN=4 OBJECT_CREATION_TYPE=2 TYPE_ARGUMENT=4 TYPE_ELEMENT=6 + not scored: 6 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 b596d41f2..3fe6d4b51 100644 --- a/graph/typescript/engine/resolution/contextual-params.dl +++ b/graph/typescript/engine/resolution/contextual-params.dl @@ -59,8 +59,23 @@ callback_arg(ce, argpos, lm) :- expr_child("client", ce, "ARGUMENT", argpos, lam // parameter, which needs no guard: TypeScript forbids a callback with more parameters // than its target signature, so a lambda parameter's position can never reach it. callee_fn_param_ref(callee, argpos, fref) :- param_decl(_, _, argpos, _, callee, p), - param_type_ref(_, fref, p), - type_ref(_, "FUNCTION_TYPE", _, _, _, _, _, fref). + param_type_ref(_, pref, p), + fn_type_under(pref, fref). + +// …AND WHERE THE FUNCTION TYPE IS WRAPPED. An optional callback is written +// `onfulfilled?: ((value: T) => R) | undefined | null` — `Promise.then`, `catch`, +// `finally`, and every library that copies the lib's style. The parser lays that out +// UNION → PARENTHESIZED → FUNCTION_TYPE, so a rule that asks for FUNCTION_TYPE on the +// parameter's own reference never saw it, and every `p.then(u => u.m())` left `u` +// untyped. The compiler takes the contextual type from the non-nullish member of the +// union; the other members (`undefined`, `null`) are not function types and add nothing. +fn_type_under(r, r) :- type_ref(_, "FUNCTION_TYPE", _, _, _, _, _, r). +fn_type_under(r, f) :- type_ref(_, "UNION", _, _, _, _, _, r), + type_ref_parent(_, r, _, c), + fn_type_under(c, f). +fn_type_under(r, f) :- type_ref(_, "PARENTHESIZED", _, _, _, _, _, r), + type_ref_parent(_, r, _, c), + fn_type_under(c, f). fn_param_tvar(fref, k, pname) :- type_ref_parent(_, fref, k, child), type_ref(_, "TYPE_VARIABLE", _, pname, _, _, _, child), @@ -215,6 +230,50 @@ lambda_param_type(lp, prov, t) :- callback_arg(ce, argpos, lm), ref_element_target(rref, prov, t), param_decl(_, _, k, _, lm, lp). +// (e) …AND WHERE NO STANDARD LIBRARY IS STAGED. Every clause above reads the callee's +// signature, so each needs `Array.map` / `Promise.then` as a DECLARATION — and a default +// index stages no library root, so the call has no candidate and `r.all().map(u => +// u.display())` left `u` untyped while `for (const u of r.all())` over the same receiver +// resolved: the loop takes its element off the receiver's own reference, not off a lib. +// This is that route for the callbacks one of whose parameters IS the element: the +// receiver's declared reference names the container and its element, which is all the +// compiler uses here. Where a library IS staged the clauses above derive the same type, +// so this adds nothing there. +// +// The lib's callbacks that receive the element, and the parameter position it arrives at: +// first for the iteration callbacks of Array, ReadonlyArray, Set and the iterator helpers, +// second for `reduce` / `reduceRight`, whose first is the accumulator. ref_element_target +// only answers for a container reference, so a user class with its own `map` is never +// typed through this. +builtin_element_callback("forEach", "0"). +builtin_element_callback("map", "0"). +builtin_element_callback("flatMap", "0"). +builtin_element_callback("filter", "0"). +builtin_element_callback("some", "0"). +builtin_element_callback("every", "0"). +builtin_element_callback("find", "0"). +builtin_element_callback("findIndex", "0"). +builtin_element_callback("findLast", "0"). +builtin_element_callback("findLastIndex", "0"). +builtin_element_callback("reduce", "1"). +builtin_element_callback("reduceRight", "1"). +lambda_param_type(lp, prov, t) :- call_site("client", _, name, _, recv, ce, _), + builtin_element_callback(name, k), + callback_arg(ce, "0", lm), + expr_decl_ref(recv, rref), + ref_element_target(rref, prov, t), + param_decl(_, _, k, _, lm, lp), + !param_type_ref(_, _, lp). +// …and `promise.then(v => …)`: the fulfilled value is the Promise's type argument. Keyed +// on the reference NAMING Promise / PromiseLike (ref_promise_value), never on +// ref_awaited_target, whose identity clause would type a user class's own `then`. +lambda_param_type(lp, prov, t) :- call_site("client", _, "then", _, recv, ce, _), + callback_arg(ce, "0", lm), + expr_decl_ref(recv, rref), + ref_promise_value(rref, prov, t), + param_decl(_, _, "0", _, lm, lp), + !param_type_ref(_, _, lp). + // ── and type the uses inside the body ─────────────────────────────────────── // The arrow's parameter has no annotation, so param_type is empty for it and this adds // rather than competes. A parameter that IS annotated keeps its declared type. diff --git a/graph/typescript/engine/resolution/type-resolution.dl b/graph/typescript/engine/resolution/type-resolution.dl index 2c8e5ef20..aa113693c 100644 --- a/graph/typescript/engine/resolution/type-resolution.dl +++ b/graph/typescript/engine/resolution/type-resolution.dl @@ -685,7 +685,8 @@ ref_name_is_container("IteratorObject"). // `(await client.get()).body` — the receiver is the AWAITED type, not the Promise. // `async` functions make this ubiquitous, and typing the receiver as Promise instead // resolves every such call to `then`/`catch` or to nothing at all. -ref_awaited_target(ref, prov, t) :- type_ref(rp, "TYPE_REFERENCE", _, tn, _, _, _, ref), +ref_awaited_target(ref, prov, t) :- ref_promise_value(ref, prov, t). +ref_promise_value(ref, prov, t) :- type_ref(rp, "TYPE_REFERENCE", _, tn, _, _, _, ref), ref_name_is_promise(tn), type_ref_parent(rp, ref, "0", child), ref_type_target(child, prov, t). diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl index 815055c16..19d553728 100644 --- a/graph/typescript/souffle/decls_all.dl +++ b/graph/typescript/souffle/decls_all.dl @@ -52,6 +52,8 @@ .decl call_signature_in_scope(c0:symbol,c1:symbol) .decl callback_arg(c0:symbol,c1:symbol,c2:symbol) .decl callee_fn_param_ref(c0:symbol,c1:symbol,c2:symbol) +.decl fn_type_under(c0:symbol,c1:symbol) +.decl builtin_element_callback(c0:symbol,c1:symbol) .decl fn_value_kind(c0:symbol) .decl lambda_param_shape(c0:symbol,c1:symbol) .decl lambda_param_namespace(c0:symbol,c1:symbol,c2:symbol) @@ -481,6 +483,7 @@ .decl readonly_over_array(c0:symbol) .decl receiver_kind_is_value(c0:symbol) .decl ref_awaited_target(c0:symbol,c1:symbol,c2:symbol) +.decl ref_promise_value(c0:symbol,c1:symbol,c2:symbol) .decl ref_base_name(c0:symbol,c1:symbol) .decl ref_context_types_a_value(c0:symbol) .decl ref_element_shape(c0:symbol,c1:symbol) From 860f0b61bea779d348d50413d8bf874c648555b1 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Mon, 28 Sep 2026 20:08:48 -0700 Subject: [PATCH 2/2] typescript: read a callback's element off built-in containers only, through aliases and nested callbacks The no-library route typed a `forEach` / `then` callback parameter from the receiver's type argument by NAME, so a program's own `class Set` or `class Promise`, whose callbacks receive something else, got the type argument as well as its own parameter type. A container name that resolves to a client declaration is no longer read as the built-in. A union of two different function types gives the compiler no contextual type; the union is now looked through only when exactly one member is a function type. The element now travels as a type reference, so the route also reaches `type Users = User[]`, `Alias` over `Array`, a callback over a parameter that was itself typed this way (`xss.forEach(xs => xs.forEach(...))`, `p.then(us => us.map(...))`), and `Map.forEach((value, key) => ...)`. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../src/shadow.ts | 37 ++++++++ .../src/use.ts | 27 ++++++ .../81-contextual-lambda-param-no-lib.edges | 52 ++++++++--- .../81-contextual-lambda-param-no-lib.entries | 18 +++- ...81-contextual-lambda-param-no-lib.envelope | 14 ++- .../81-contextual-lambda-param-no-lib.oracle | 2 +- ...81-contextual-lambda-param-no-lib.type-use | 16 ++++ ...ontextual-lambda-param-no-lib.types-oracle | 12 +-- .../engine/resolution/contextual-params.dl | 87 +++++++++++++++++-- .../engine/resolution/type-resolution.dl | 3 +- graph/typescript/souffle/decls_all.dl | 10 ++- 11 files changed, 240 insertions(+), 38 deletions(-) create mode 100644 graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/shadow.ts diff --git a/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/shadow.ts b/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/shadow.ts new file mode 100644 index 000000000..dddc6bcb3 --- /dev/null +++ b/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/shadow.ts @@ -0,0 +1,37 @@ +// CONTROLS: a module that declares its own `Set` and `Promise`. They are not the built-ins, +// so `forEach` / `then` callbacks take their parameter from these classes' own signatures +// (a Key), never from the type argument (an Item). +export class Item { + size = 1; + display(): string { + return 'i'; + } +} +export class Key { + display(): string { + return 'k'; + } +} +export class Set { + forEach(cb: (k: Key) => void): void { + cb(new Key()); + } +} +export class Promise { + then(cb: (k: Key) => void): void { + cb(new Key()); + } +} +export function ctlOwnSet(s: Set): void { + s.forEach(k => k.display()); +} +export function ctlOwnPromise(p: Promise): void { + p.then(k => k.display()); +} +// A union of two function types over DIFFERENT parameter types gives no contextual type +// (the compiler reports the parameter as implicitly `any`), so neither class is picked. +export function either(cb: ((i: Item) => void) | ((k: Key) => void)): void {} +export function ctlUnionOfFns(): void { + // @ts-expect-error z is implicitly any + either(z => z.display()); +} diff --git a/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/use.ts b/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/use.ts index 11171b358..e48b591fc 100644 --- a/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/use.ts +++ b/graph/test/typescript/cases/81-contextual-lambda-param-no-lib/src/use.ts @@ -63,6 +63,33 @@ export function thenBare(r: Repo): Promise { export function thenParam(p: Promise): Promise { return p.then(a => a.display()); } +// Through a type alias, plain and generic. +export type Users = User[]; +export type Many = Array; +export function aliasPlain(us: Users): void { + us.forEach(u => u.save()); +} +export function aliasGeneric(as: Many): void { + as.forEach(a => a.touch()); +} +// A callback over a parameter that was itself typed from context. +export function nestedArrays(xss: Admin[][]): void { + xss.forEach(xs => { + xs.forEach(a => a.save()); + }); +} +export function thenThenMap(p: Promise): void { + p.then(us => { + us.map(u => u.touch()); + }); +} +// `Map.forEach((value, key) => …)`: the value is type argument 1, the key argument 0. +export function mapForEach(m: Map): void { + m.forEach((v, k) => { + v.display(); + k.touch(); + }); +} // ── CONTROLS ──────────────────────────────────────────────────────────────── // A class of the user's own with a `then` and a `map`: its own signatures type the diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.edges b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.edges index 3a15f2a14..f055468cc 100644 --- a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.edges +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.edges @@ -1,23 +1,50 @@ -ambiguous_unknown METHOD_CALL use#ctlAnnotated(User[]) @L86 -> - +ambiguous_unknown METHOD_CALL shadow#(?) @L36 -> - +ambiguous_unknown METHOD_CALL use#(?) @L78 -> - +ambiguous_unknown METHOD_CALL use#(?) @L83 -> - +ambiguous_unknown METHOD_CALL use#aliasGeneric(Many) @L73 -> - +ambiguous_unknown METHOD_CALL use#aliasPlain(Users) @L70 -> - +ambiguous_unknown METHOD_CALL use#ctlAnnotated(User[]) @L113 -> - ambiguous_unknown METHOD_CALL use#everyParam(Admin[]) @L54 -> - ambiguous_unknown METHOD_CALL use#findReadonly(Repo) @L51 -> - ambiguous_unknown METHOD_CALL use#forEachParen(Repo) @L46 -> - ambiguous_unknown METHOD_CALL use#mapBare(Repo) @L43 -> - +ambiguous_unknown METHOD_CALL use#mapForEach(Map) @L88 -> - +ambiguous_unknown METHOD_CALL use#nestedArrays(Admin[][]) @L77 -> - ambiguous_unknown METHOD_CALL use#reduceElement(User[]) @L58 -> - ambiguous_unknown METHOD_CALL use#thenBare(Repo) @L61 -> - ambiguous_unknown METHOD_CALL use#thenParam(Promise) @L64 -> - -callback_registered METHOD_CALL use#ctlAnnotated(User[]) @L86 -> use#(User) +ambiguous_unknown METHOD_CALL use#thenThenMap(Promise) @L82 -> - +callback_registered METHOD_CALL use#(?) @L78 -> use#(?) +callback_registered METHOD_CALL use#(?) @L83 -> use#(?) +callback_registered METHOD_CALL use#aliasGeneric(Many) @L73 -> use#(?) +callback_registered METHOD_CALL use#aliasPlain(Users) @L70 -> use#(?) +callback_registered METHOD_CALL use#ctlAnnotated(User[]) @L113 -> use#(User) callback_registered METHOD_CALL use#everyParam(Admin[]) @L54 -> use#(?) callback_registered METHOD_CALL use#findReadonly(Repo) @L51 -> use#(?) callback_registered METHOD_CALL use#forEachParen(Repo) @L46 -> use#(?) callback_registered METHOD_CALL use#mapBare(Repo) @L43 -> use#(?) +callback_registered METHOD_CALL use#mapForEach(Map) @L88 -> use#(?,?) +callback_registered METHOD_CALL use#nestedArrays(Admin[][]) @L77 -> use#(?) callback_registered METHOD_CALL use#reduceElement(User[]) @L58 -> use#(?,?) callback_registered METHOD_CALL use#thenBare(Repo) @L61 -> use#(?) callback_registered METHOD_CALL use#thenParam(Promise) @L64 -> use#(?) -known_edge CONSTRUCTOR_CALL Lazy#map((u: User) =) @L75 -> User#() -known_edge CONSTRUCTOR_CALL Lazy#then((a: Admin) =) @L72 -> Admin#() -known_edge FUNCTION_CALL Lazy#map((u: User) =) @L75 -> Lazy#(User) -known_edge FUNCTION_CALL Lazy#then((a: Admin) =) @L72 -> Lazy#(Admin) +callback_registered METHOD_CALL use#thenThenMap(Promise) @L82 -> use#(?) +known_edge CONSTRUCTOR_CALL Lazy#map((u: User) =) @L102 -> User#() +known_edge CONSTRUCTOR_CALL Lazy#then((a: Admin) =) @L99 -> Admin#() +known_edge CONSTRUCTOR_CALL Promise#then((k: Key) =) @L22 -> Key#() +known_edge CONSTRUCTOR_CALL Set#forEach((k: Key) =) @L17 -> Key#() +known_edge FUNCTION_CALL Lazy#map((u: User) =) @L102 -> Lazy#(User) +known_edge FUNCTION_CALL Lazy#then((a: Admin) =) @L99 -> Lazy#(Admin) +known_edge FUNCTION_CALL Promise#then((k: Key) =) @L22 -> Promise#(Key) +known_edge FUNCTION_CALL Set#forEach((k: Key) =) @L17 -> Set#(Key) +known_edge FUNCTION_CALL shadow#ctlUnionOfFns() @L36 -> shadow#either(((i: Item) =) +known_edge METHOD_CALL shadow#(?) @L26 -> Key#display() +known_edge METHOD_CALL shadow#(?) @L29 -> Key#display() +known_edge METHOD_CALL shadow#ctlOwnPromise(Promise) @L29 -> Promise#then((k: Key) =) +known_edge METHOD_CALL shadow#ctlOwnSet(Set) @L26 -> Set#forEach((k: Key) =) +known_edge METHOD_CALL use#(?) @L106 -> Admin#touch() +known_edge METHOD_CALL use#(?) @L109 -> User#touch() +known_edge METHOD_CALL use#(User) @L113 -> User#save() known_edge METHOD_CALL use#(?) @L43 -> User#display() known_edge METHOD_CALL use#(?) @L47 -> User#save() known_edge METHOD_CALL use#(?) @L51 -> User#touch() @@ -25,11 +52,14 @@ known_edge METHOD_CALL use#(?) @L54 -> Admin#save() known_edge METHOD_CALL use#(?,?) @L58 -> User#save() known_edge METHOD_CALL use#(?) @L61 -> User#touch() known_edge METHOD_CALL use#(?) @L64 -> Admin#display() -known_edge METHOD_CALL use#(?) @L79 -> Admin#touch() -known_edge METHOD_CALL use#(?) @L82 -> User#touch() -known_edge METHOD_CALL use#(User) @L86 -> User#save() -known_edge METHOD_CALL use#ctlOwnMap(Lazy) @L82 -> Lazy#map((u: User) =) -known_edge METHOD_CALL use#ctlOwnThen(Lazy) @L79 -> Lazy#then((a: Admin) =) +known_edge METHOD_CALL use#(?) @L70 -> User#save() +known_edge METHOD_CALL use#(?) @L73 -> Admin#touch() +known_edge METHOD_CALL use#(?) @L78 -> Admin#save() +known_edge METHOD_CALL use#(?) @L83 -> User#touch() +known_edge METHOD_CALL use#(?,?) @L89 -> User#display() +known_edge METHOD_CALL use#(?,?) @L90 -> Admin#touch() +known_edge METHOD_CALL use#ctlOwnMap(Lazy) @L109 -> Lazy#map((u: User) =) +known_edge METHOD_CALL use#ctlOwnThen(Lazy) @L106 -> Lazy#then((a: Admin) =) known_edge METHOD_CALL use#findReadonly(Repo) @L51 -> Repo#readonlyAll() known_edge METHOD_CALL use#forEachParen(Repo) @L46 -> Repo#all() known_edge METHOD_CALL use#mapBare(Repo) @L43 -> Repo#all() diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.entries b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.entries index 789f8d25a..687a9aea0 100644 --- a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.entries +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.entries @@ -1,12 +1,22 @@ -── entry_point (11) ── - exported_from_entry_module use#ctlAnnotated use.ts:85 - exported_from_entry_module use#ctlOwnMap use.ts:81 - exported_from_entry_module use#ctlOwnThen use.ts:78 +── entry_point (21) ── + exported_from_entry_module shadow#ctlOwnPromise shadow.ts:28 + exported_from_entry_module shadow#ctlOwnSet shadow.ts:25 + exported_from_entry_module shadow#ctlUnionOfFns shadow.ts:34 + exported_from_entry_module shadow#either shadow.ts:33 + exported_from_entry_module use#aliasGeneric use.ts:72 + exported_from_entry_module use#aliasPlain use.ts:69 + exported_from_entry_module use#ctlAnnotated use.ts:112 + exported_from_entry_module use#ctlOwnMap use.ts:108 + exported_from_entry_module use#ctlOwnThen use.ts:105 exported_from_entry_module use#everyParam use.ts:53 exported_from_entry_module use#findReadonly use.ts:50 exported_from_entry_module use#forEachParen use.ts:45 exported_from_entry_module use#mapBare use.ts:42 + exported_from_entry_module use#mapForEach use.ts:87 + exported_from_entry_module use#nestedArrays use.ts:76 exported_from_entry_module use#reduceElement use.ts:57 exported_from_entry_module use#thenBare use.ts:60 exported_from_entry_module use#thenParam use.ts:63 + exported_from_entry_module use#thenThenMap use.ts:81 + unimported_module shadow# shadow.ts:1 unimported_module use# use.ts:1 diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.envelope b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.envelope index 58380ef52..8c7dd7b70 100644 --- a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.envelope +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.envelope @@ -1,4 +1,10 @@ -value use#@71:12 -> use#@79 -value use#@74:11 -> use#@82 -value_call use#Lazy.map -> use#@82 -value_call use#Lazy.then -> use#@79 +value shadow#@16:15 -> shadow#@26 +value shadow#@21:12 -> shadow#@29 +value shadow#@33:29 -> shadow#@36 +value shadow#@33:51 -> shadow#@36 +value use#@101:11 -> use#@109 +value use#@98:12 -> use#@106 +value_call shadow#Promise.then -> shadow#@29 +value_call shadow#Set.forEach -> shadow#@26 +value_call use#Lazy.map -> use#@109 +value_call use#Lazy.then -> use#@106 diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.oracle b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.oracle index b063a5494..540c068bb 100644 --- a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.oracle +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.oracle @@ -1 +1 @@ -oracle=20 engine=20 agree=20 missing=0 (known 0, NEW 0) extra=0 +oracle=35 engine=35 agree=35 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.type-use b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.type-use index 8be6c0dc4..0622fd295 100644 --- a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.type-use +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.type-use @@ -2,18 +2,34 @@ ambiguous_unknown METHOD_PARAM 0 Repo [METHOD_PARAM] -> - ambiguous_unknown METHOD_PARAM 0 use [METHOD_PARAM] -> - ambiguous_unknown METHOD_RETURN 0 Repo [METHOD] -> - ambiguous_unknown METHOD_RETURN 0 use [METHOD] -> - +ambiguous_unknown TYPE_ALIAS_RHS 0 Many [TYPE] -> - +known_edge METHOD_PARAM 0 shadow [METHOD_PARAM] -> Promise +known_edge METHOD_PARAM 0 shadow [METHOD_PARAM] -> Set known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Lazy +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Many known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Repo known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> User +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Users known_edge METHOD_PARAM 1 Lazy [METHOD_PARAM] -> Admin known_edge METHOD_PARAM 1 Lazy [METHOD_PARAM] -> User +known_edge METHOD_PARAM 1 Promise [METHOD_PARAM] -> Key +known_edge METHOD_PARAM 1 Set [METHOD_PARAM] -> Key +known_edge METHOD_PARAM 3 shadow [METHOD_PARAM] -> Item +known_edge METHOD_PARAM 3 shadow [METHOD_PARAM] -> Key known_edge OBJECT_CREATION_TYPE 0 Lazy [EXPRESSION] -> Admin known_edge OBJECT_CREATION_TYPE 0 Lazy [EXPRESSION] -> User +known_edge OBJECT_CREATION_TYPE 0 Promise [EXPRESSION] -> Key +known_edge OBJECT_CREATION_TYPE 0 Set [EXPRESSION] -> Key known_edge TYPE_ARGUMENT 1 Repo [METHOD] -> User known_edge TYPE_ARGUMENT 1 Repo [METHOD_PARAM] -> User +known_edge TYPE_ARGUMENT 1 shadow [METHOD_PARAM] -> Item known_edge TYPE_ARGUMENT 1 use [METHOD_PARAM] -> Admin +known_edge TYPE_ARGUMENT 1 use [METHOD_PARAM] -> User known_edge TYPE_ELEMENT 1 Repo [METHOD] -> User known_edge TYPE_ELEMENT 1 Repo [METHOD_PARAM] -> User +known_edge TYPE_ELEMENT 1 Users [TYPE] -> User known_edge TYPE_ELEMENT 1 use [METHOD] -> User known_edge TYPE_ELEMENT 1 use [METHOD_PARAM] -> Admin known_edge TYPE_ELEMENT 1 use [METHOD_PARAM] -> User +known_edge TYPE_ELEMENT 2 use [METHOD_PARAM] -> Admin +known_edge TYPE_ELEMENT 2 use [METHOD_PARAM] -> User diff --git a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.types-oracle b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.types-oracle index aefc3fa34..e4ceb4134 100644 --- a/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.types-oracle +++ b/graph/test/typescript/expected/81-contextual-lambda-param-no-lib.types-oracle @@ -1,7 +1,7 @@ 81-contextual-lambda-param-no-lib [types] - precision 1.0000 (7 correct, 0 wrong) - recall 1.0000 (7 of 7 the compiler resolved) - sites 27 resolved 21 (77.8%) - tiers ambiguous_unknown=6 known_edge=21 - contexts METHOD_PARAM=11 METHOD_RETURN=4 OBJECT_CREATION_TYPE=2 TYPE_ARGUMENT=4 TYPE_ELEMENT=6 - not scored: 6 rows whose target is not a client declaration + precision 1.0000 (16 correct, 0 wrong) + recall 1.0000 (16 of 16 the compiler resolved) + sites 48 resolved 39 (81.2%) + tiers ambiguous_unknown=9 known_edge=39 + contexts METHOD_PARAM=21 METHOD_RETURN=4 OBJECT_CREATION_TYPE=4 TYPE_ALIAS_RHS=1 TYPE_ARGUMENT=9 TYPE_ELEMENT=9 + not scored: 9 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 3fe6d4b51..c9f675e15 100644 --- a/graph/typescript/engine/resolution/contextual-params.dl +++ b/graph/typescript/engine/resolution/contextual-params.dl @@ -69,13 +69,26 @@ callee_fn_param_ref(callee, argpos, fref) :- param_decl(_, _, argpos, _, callee, // parameter's own reference never saw it, and every `p.then(u => u.m())` left `u` // untyped. The compiler takes the contextual type from the non-nullish member of the // union; the other members (`undefined`, `null`) are not function types and add nothing. +// A union of TWO function types — `((u: User) => void) | ((k: Key) => void)` — gives the +// compiler no contextual type at all (the parameter is implicitly `any`), so the union is +// only looked through when exactly one member is a function type. fn_type_under(r, r) :- type_ref(_, "FUNCTION_TYPE", _, _, _, _, _, r). fn_type_under(r, f) :- type_ref(_, "UNION", _, _, _, _, _, r), type_ref_parent(_, r, _, c), + fn_type_member(c), + !union_other_fn_member(r, c), fn_type_under(c, f). fn_type_under(r, f) :- type_ref(_, "PARENTHESIZED", _, _, _, _, _, r), type_ref_parent(_, r, _, c), fn_type_under(c, f). +fn_type_member(c) :- type_ref(_, "FUNCTION_TYPE", _, _, _, _, _, c). +fn_type_member(c) :- type_ref(_, "PARENTHESIZED", _, _, _, _, _, c), + type_ref_parent(_, c, _, f), + type_ref(_, "FUNCTION_TYPE", _, _, _, _, _, f). +union_other_fn_member(r, c) :- type_ref_parent(_, r, _, c), + type_ref_parent(_, r, _, c2), + c2 != c, + fn_type_member(c2). fn_param_tvar(fref, k, pname) :- type_ref_parent(_, fref, k, child), type_ref(_, "TYPE_VARIABLE", _, pname, _, _, _, child), @@ -242,9 +255,53 @@ lambda_param_type(lp, prov, t) :- callback_arg(ce, argpos, lm), // // The lib's callbacks that receive the element, and the parameter position it arrives at: // first for the iteration callbacks of Array, ReadonlyArray, Set and the iterator helpers, -// second for `reduce` / `reduceRight`, whose first is the accumulator. ref_element_target -// only answers for a container reference, so a user class with its own `map` is never -// typed through this. +// second for `reduce` / `reduceRight`, whose first is the accumulator. +// +// What travels is the element's type REFERENCE, not a resolved type, so a callback over a +// parameter that was itself typed here — `xss.forEach(xs => xs.forEach(u => …))`, +// `p.then(us => us.map(u => …))` — reads its element off that reference in turn +// (expr_decl_ref below). The container is recognised by NAME, so a name the program +// declares itself — a user `class Set` or `class Promise` with its own `forEach` / +// `then` — is not a built-in and is never read here (builtin_elem_ref). A type alias is +// followed to its right-hand side, substituting the alias's own type arguments +// (`type Alias = Array`, then `Alias`). +builtin_container_ref(ref) :- type_ref(_, "TYPE_REFERENCE", _, _, _, _, _, ref), + !type_ref_resolves("client", ref, _). +builtin_elem_ref(ref, child) :- type_ref(rp, "ARRAY", _, _, _, _, _, ref), + type_ref_parent(rp, ref, _, child). +builtin_elem_ref(ref, child) :- type_ref(rp, "TYPE_REFERENCE", _, tn, _, _, _, ref), + ref_name_is_container(tn), + builtin_container_ref(ref), + type_ref_parent(rp, ref, "0", child). +builtin_elem_ref(parent, x) :- ref_is_readonly_operator(parent), + type_ref_parent(_, parent, _, child), + builtin_elem_ref(child, x). +builtin_elem_ref(ref, x) :- ref_via_alias(ref, rhs), + builtin_elem_ref(rhs, x). +builtin_elem_ref(ref, a) :- ref_via_alias(ref, rhs), + builtin_elem_ref(rhs, x), + type_ref(_, "TYPE_VARIABLE", _, n, _, _, _, x), + type_arg_binding(ref, n, a). +// `Map` / `ReadonlyMap`: `forEach((value, key) => …)` — the value (type argument 1) is the +// callback's first parameter and the key (type argument 0) its second. +builtin_map_ref(ref, argpos, child) :- type_ref(rp, "TYPE_REFERENCE", _, tn, _, _, _, ref), + ref_name_is_map(tn), + builtin_container_ref(ref), + type_ref_parent(rp, ref, argpos, child). +builtin_map_ref(ref, argpos, x) :- ref_via_alias(ref, rhs), + builtin_map_ref(rhs, argpos, x). +ref_name_is_map("Map"). +ref_name_is_map("ReadonlyMap"). +map_callback_param("1", "0"). +map_callback_param("0", "1"). +// `Promise` / `PromiseLike`: `then`'s first callback receives T. +builtin_promise_ref(ref, child) :- type_ref(rp, "TYPE_REFERENCE", _, tn, _, _, _, ref), + ref_name_is_promise(tn), + builtin_container_ref(ref), + type_ref_parent(rp, ref, "0", child). +builtin_promise_ref(ref, x) :- ref_via_alias(ref, rhs), + builtin_promise_ref(rhs, x). + builtin_element_callback("forEach", "0"). builtin_element_callback("map", "0"). builtin_element_callback("flatMap", "0"). @@ -257,22 +314,34 @@ builtin_element_callback("findLast", "0"). builtin_element_callback("findLastIndex", "0"). builtin_element_callback("reduce", "1"). builtin_element_callback("reduceRight", "1"). -lambda_param_type(lp, prov, t) :- call_site("client", _, name, _, recv, ce, _), +lambda_param_ref(lp, c) :- call_site("client", _, name, _, recv, ce, _), builtin_element_callback(name, k), callback_arg(ce, "0", lm), expr_decl_ref(recv, rref), - ref_element_target(rref, prov, t), + builtin_elem_ref(rref, c), + param_decl(_, _, k, _, lm, lp), + !param_type_ref(_, _, lp). +lambda_param_ref(lp, c) :- call_site("client", _, "forEach", _, recv, ce, _), + callback_arg(ce, "0", lm), + expr_decl_ref(recv, rref), + builtin_map_ref(rref, argpos, c), + map_callback_param(argpos, k), param_decl(_, _, k, _, lm, lp), !param_type_ref(_, _, lp). // …and `promise.then(v => …)`: the fulfilled value is the Promise's type argument. Keyed -// on the reference NAMING Promise / PromiseLike (ref_promise_value), never on -// ref_awaited_target, whose identity clause would type a user class's own `then`. -lambda_param_type(lp, prov, t) :- call_site("client", _, "then", _, recv, ce, _), +// on the reference NAMING Promise / PromiseLike, never on ref_awaited_target, whose +// identity clause would type a user class's own `then`. +lambda_param_ref(lp, c) :- call_site("client", _, "then", _, recv, ce, _), callback_arg(ce, "0", lm), expr_decl_ref(recv, rref), - ref_promise_value(rref, prov, t), + builtin_promise_ref(rref, c), param_decl(_, _, "0", _, lm, lp), !param_type_ref(_, _, lp). +lambda_param_type(lp, prov, t) :- lambda_param_ref(lp, c), + ref_type_target(c, prov, t). +// The parameter's reference is its declared reference, as an annotation's would be. +expr_decl_ref(e, c) :- expr_referenced("client", "PARAMETER", p, e), + lambda_param_ref(p, c). // ── and type the uses inside the body ─────────────────────────────────────── // The arrow's parameter has no annotation, so param_type is empty for it and this adds diff --git a/graph/typescript/engine/resolution/type-resolution.dl b/graph/typescript/engine/resolution/type-resolution.dl index aa113693c..2c8e5ef20 100644 --- a/graph/typescript/engine/resolution/type-resolution.dl +++ b/graph/typescript/engine/resolution/type-resolution.dl @@ -685,8 +685,7 @@ ref_name_is_container("IteratorObject"). // `(await client.get()).body` — the receiver is the AWAITED type, not the Promise. // `async` functions make this ubiquitous, and typing the receiver as Promise instead // resolves every such call to `then`/`catch` or to nothing at all. -ref_awaited_target(ref, prov, t) :- ref_promise_value(ref, prov, t). -ref_promise_value(ref, prov, t) :- type_ref(rp, "TYPE_REFERENCE", _, tn, _, _, _, ref), +ref_awaited_target(ref, prov, t) :- type_ref(rp, "TYPE_REFERENCE", _, tn, _, _, _, ref), ref_name_is_promise(tn), type_ref_parent(rp, ref, "0", child), ref_type_target(child, prov, t). diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl index 19d553728..208215187 100644 --- a/graph/typescript/souffle/decls_all.dl +++ b/graph/typescript/souffle/decls_all.dl @@ -54,6 +54,15 @@ .decl callee_fn_param_ref(c0:symbol,c1:symbol,c2:symbol) .decl fn_type_under(c0:symbol,c1:symbol) .decl builtin_element_callback(c0:symbol,c1:symbol) +.decl fn_type_member(c0:symbol) +.decl union_other_fn_member(c0:symbol,c1:symbol) +.decl builtin_container_ref(c0:symbol) +.decl builtin_elem_ref(c0:symbol,c1:symbol) +.decl builtin_map_ref(c0:symbol,c1:symbol,c2:symbol) +.decl ref_name_is_map(c0:symbol) +.decl map_callback_param(c0:symbol,c1:symbol) +.decl builtin_promise_ref(c0:symbol,c1:symbol) +.decl lambda_param_ref(c0:symbol,c1:symbol) .decl fn_value_kind(c0:symbol) .decl lambda_param_shape(c0:symbol,c1:symbol) .decl lambda_param_namespace(c0:symbol,c1:symbol,c2:symbol) @@ -483,7 +492,6 @@ .decl readonly_over_array(c0:symbol) .decl receiver_kind_is_value(c0:symbol) .decl ref_awaited_target(c0:symbol,c1:symbol,c2:symbol) -.decl ref_promise_value(c0:symbol,c1:symbol,c2:symbol) .decl ref_base_name(c0:symbol,c1:symbol) .decl ref_context_types_a_value(c0:symbol) .decl ref_element_shape(c0:symbol,c1:symbol)