diff --git a/graph/test/typescript/cases/82-structural-conformer-across-programs/src/app/globals.d.ts b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/app/globals.d.ts new file mode 100644 index 000000000..7bd873485 --- /dev/null +++ b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/app/globals.d.ts @@ -0,0 +1,14 @@ +// The PRODUCTION program's standard library (noLib, as case 21 explains). `Array.map` here +// and `Pipeline.map` below both take one callback. +interface Array { + map(fn: (v: T) => U): U[]; +} +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/82-structural-conformer-across-programs/src/app/pipeline.ts b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/app/pipeline.ts new file mode 100644 index 000000000..6c9601e88 --- /dev/null +++ b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/app/pipeline.ts @@ -0,0 +1,16 @@ +// Production code, in its own program. `Pipeline` has a `map`, so by shape it "satisfies" +// the FIXTURE program's hand-written `Array` too — but no value of it can ever reach a +// receiver typed by that `Array`: neither program sees the other's files. So the fixture's +// `rows.map(...)` must not dispatch here, and `this.items.map(project)` must not reach the +// fixture's callback through that dispatch. +export class Pipeline { + constructor(private readonly items: T[]) {} + + map(project: (value: T) => U): Pipeline { + return new Pipeline(this.items.map(project)); + } +} + +export function build(xs: string[]): Pipeline { + return new Pipeline(xs).map((s) => s); +} diff --git a/graph/test/typescript/cases/82-structural-conformer-across-programs/src/app/tsconfig.json b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/app/tsconfig.json new file mode 100644 index 000000000..c901fced9 --- /dev/null +++ b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/app/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "noLib": true, "target": "es2022", "module": "commonjs" } } diff --git a/graph/test/typescript/cases/82-structural-conformer-across-programs/src/fixture/globals.d.ts b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/fixture/globals.d.ts new file mode 100644 index 000000000..745c7321b --- /dev/null +++ b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/fixture/globals.d.ts @@ -0,0 +1,16 @@ +// A TEST FIXTURE's own standard library, in a SIBLING program: these globals belong to +// ./tsconfig.json alone. The parameter name differs from ../app/globals.d.ts on purpose, +// so the two `Array.map` declarations are told apart in the goldens, and this header is +// one line longer so neither `Array` spans the line of the other's `map`. +interface Array { + map(each: (item: T) => U): U[]; +} +interface Object { toString(radix?: number): 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/82-structural-conformer-across-programs/src/fixture/tsconfig.json b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/fixture/tsconfig.json new file mode 100644 index 000000000..c901fced9 --- /dev/null +++ b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/fixture/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "noLib": true, "target": "es2022", "module": "commonjs" } } diff --git a/graph/test/typescript/cases/82-structural-conformer-across-programs/src/fixture/use.ts b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/fixture/use.ts new file mode 100644 index 000000000..e0b82e391 --- /dev/null +++ b/graph/test/typescript/cases/82-structural-conformer-across-programs/src/fixture/use.ts @@ -0,0 +1,16 @@ +// CONTROL: `Bag` lives in this program, has the fixture `Array`'s one member and is +// constructed, so it stays a structural conformer of this program's `Array`: `rows.map` +// keeps its dispatch to `Bag.map`. Only the other program's `Pipeline` drops out. +export class Row { + key(): string { return "row"; } +} + +export class Bag { + map(each: (item: T) => U): U[] { return []; } +} + +export function keys(rows: Row[]): string[] { + return rows.map((r) => r.key()); +} + +export const bag = new Bag(); diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.edges b/graph/test/typescript/expected/82-structural-conformer-across-programs.edges new file mode 100644 index 000000000..5d1aad278 --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.edges @@ -0,0 +1,9 @@ +known_edge CONSTRUCTOR_CALL Pipeline#map((value: T) =) @L10 -> Pipeline#(T[]) +known_edge CONSTRUCTOR_CALL app/pipeline#build(string[]) @L15 -> Pipeline#(T[]) +known_edge CONSTRUCTOR_CALL fixture/use#() @L16 -> Bag#() +known_edge METHOD_CALL app/pipeline#build(string[]) @L15 -> Pipeline#map((value: T) =) +known_edge METHOD_CALL fixture/use#(?) @L13 -> Row#key() +multi_inferred METHOD_CALL Pipeline#map((value: T) =) @L10 -> Array#map((v: T) =) +multi_inferred METHOD_CALL Pipeline#map((value: T) =) @L10 -> Pipeline#map((value: T) =) +multi_inferred METHOD_CALL fixture/use#keys(Row[]) @L13 -> Array#map((item: T) =) +multi_inferred METHOD_CALL fixture/use#keys(Row[]) @L13 -> Bag#map((item: T) =) diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.entries b/graph/test/typescript/expected/82-structural-conformer-across-programs.entries new file mode 100644 index 000000000..b002c567a --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.entries @@ -0,0 +1,7 @@ +── entry_point (6) ── + exported_from_entry_module app/pipeline#build pipeline.ts:14 + exported_from_entry_module fixture/use#keys use.ts:12 + unimported_module app/globals# globals.d.ts:1 + unimported_module app/pipeline# pipeline.ts:1 + unimported_module fixture/globals# globals.d.ts:1 + unimported_module fixture/use# use.ts:1 diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.envelope b/graph/test/typescript/expected/82-structural-conformer-across-programs.envelope new file mode 100644 index 000000000..bcd932617 --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.envelope @@ -0,0 +1,6 @@ +structural app/globals#Array.map -> app/pipeline#Pipeline.map +structural fixture/globals#Array.map -> fixture/use#Bag.map +value app/globals#@4:14 -> app/pipeline# +value app/pipeline#@9:19 -> app/pipeline# +value fixture/globals#@6:16 -> fixture/use# +value fixture/use#@9:16 -> fixture/use# diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.fields b/graph/test/typescript/expected/82-structural-conformer-across-programs.fields new file mode 100644 index 000000000..a069adc82 --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.fields @@ -0,0 +1 @@ +known_edge read Pipeline#map((value: T) =) -> Pipeline#items diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.fields-oracle b/graph/test/typescript/expected/82-structural-conformer-across-programs.fields-oracle new file mode 100644 index 000000000..ca4ac9b80 --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.fields-oracle @@ -0,0 +1,7 @@ +82-structural-conformer-across-programs [fields] + precision 1.0000 (1 correct, 0 wrong) + recall 1.0000 (1 of 1 the compiler resolved) + sites 1 resolved 1 (100.0%) + tiers known_edge=1 + access read=1 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.known-missing b/graph/test/typescript/expected/82-structural-conformer-across-programs.known-missing new file mode 100644 index 000000000..0b193f752 --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.known-missing @@ -0,0 +1,5 @@ +# The per-case oracle compiles every file under src/ as ONE program (tsc-program.mjs walks +# the directory), so the two programs' `interface Array` merge and the fixture's later +# `map` overload is the one it picks. In the real layout app/ and fixture/ are separate +# programs and `this.items.map` resolves to app/globals.d.ts, which is what the engine says. +Pipeline#map((value: T) =) -> Array#map((item: T) =) diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.oracle b/graph/test/typescript/expected/82-structural-conformer-across-programs.oracle new file mode 100644 index 000000000..f5b1660dc --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.oracle @@ -0,0 +1,5 @@ +oracle=7 engine=9 agree=6 missing=1 (known 1, NEW 0) extra=3 + known Pipeline#map((value: T) =) -> Array#map((item: T) =) + extra Pipeline#map((value: T) =) -> Array#map((v: T) =) + extra Pipeline#map((value: T) =) -> Pipeline#map((value: T) =) + extra fixture/use#keys(Row[]) -> Bag#map((item: T) =) diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.type-use b/graph/test/typescript/expected/82-structural-conformer-across-programs.type-use new file mode 100644 index 000000000..b39a992e8 --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.type-use @@ -0,0 +1,9 @@ +known_edge METHOD_RETURN 0 Pipeline [METHOD] -> Pipeline +known_edge METHOD_RETURN 0 app/pipeline [METHOD] -> Pipeline +known_edge METHOD_TYPE_ARGUMENT 0 fixture/use [EXPRESSION] -> Row +known_edge OBJECT_CREATION_TYPE 0 Pipeline [EXPRESSION] -> Pipeline +known_edge OBJECT_CREATION_TYPE 0 app/pipeline [EXPRESSION] -> Pipeline +known_edge OBJECT_CREATION_TYPE 0 fixture/use [EXPRESSION] -> Bag +known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function +known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function +known_edge TYPE_ELEMENT 1 fixture/use [METHOD_PARAM] -> Row diff --git a/graph/test/typescript/expected/82-structural-conformer-across-programs.types-oracle b/graph/test/typescript/expected/82-structural-conformer-across-programs.types-oracle new file mode 100644 index 000000000..e1b1ff228 --- /dev/null +++ b/graph/test/typescript/expected/82-structural-conformer-across-programs.types-oracle @@ -0,0 +1,7 @@ +82-structural-conformer-across-programs [types] + precision 1.0000 (6 correct, 0 wrong) + recall 1.0000 (6 of 6 the compiler resolved) + sites 11 resolved 11 (100.0%) + tiers known_edge=11 + contexts METHOD_RETURN=2 METHOD_TYPE_ARGUMENT=1 OBJECT_CREATION_TYPE=3 SUPER_TYPE=4 TYPE_ELEMENT=1 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/typescript/engine/resolution/structural-satisfaction.dl b/graph/typescript/engine/resolution/structural-satisfaction.dl index d911f7083..2753406b4 100644 --- a/graph/typescript/engine/resolution/structural-satisfaction.dl +++ b/graph/typescript/engine/resolution/structural-satisfaction.dl @@ -120,7 +120,41 @@ target_has_nominal_implementor(t) :- implementors(t, _). structural_implementor(t, s) :- type_satisfies(s, t), satisfaction_target(t), s != t, - !target_has_nominal_implementor(t). + !target_has_nominal_implementor(t), + !sat_cross_program(s, t). + +// ── sat_cross_program(Source, Target) — a shape match no value can cross (#1574) ── +// A CLIENT GLOBAL belongs to one program (module-graph.dl). A class in ANOTHER program +// that happens to have the global's members — production's `Pipeline.map` against a test +// fixture's hand-written `interface Array { map }` — was counted as its implementor, so +// every fixture `xs.map(cb)` dispatched into production code and production's own +// `this.items.map(f)` dispatched into the fixture's classes: callback reach then spanned +// every program in the repository. A value of the class can reach a receiver typed by +// that global only in code that sees both, so the pair is dropped when NEITHER program +// can see the other's file: the class's module is outside the global's program, and the +// global's file is outside the class's. Governed-by or under-the-directory-of, the same +// test program_sees makes, so a root `types/global.d.ts` keeps every nested package's +// conformers, and a nested package's global keeps a root class its code may import. +// Only pairs of two different governing tsconfigs are asked, so this is a handful of +// prefix tests, not (modules) x (programs). A module with no tsconfig is never cut. +path_key(p) :- module_tsconfig(_, p). +tsconfig_dir(p, d) :- module_tsconfig(_, p), path_dir(p, d). +sat_cross_program_pair(s, t, ms, pt, mt, ps) :- sat_pair_seed(s, t), + client_global_program(t, pt), + type_module("client", ms, s), module_tsconfig(ms, ps), ps != pt, + type_module("client", mt, t). +sat_tree_ask(ms, pt) :- sat_cross_program_pair(_, _, ms, pt, _, _). +sat_tree_ask(mt, ps) :- sat_cross_program_pair(_, _, _, _, mt, ps). +module_in_program_tree(m, p) :- sat_tree_ask(m, p), module_tsconfig(m, p). +module_in_program_tree(m, p) :- sat_tree_ask(m, p), + tsconfig_dir(p, d), + module_file("client", fp, m), + n = strlen(d), + strlen(fp) >= n, + substr(fp, 0, n) = d. +sat_cross_program(s, t) :- sat_cross_program_pair(s, t, ms, pt, mt, ps), + !module_in_program_tree(ms, pt), + !module_in_program_tree(mt, ps). // ── …AND A CONFORMER THE PROGRAM ACTUALLY CONSTRUCTS, EVEN THEN (#428) ───── // The gate above is right about EVIDENCE and wrong about what it does with it. Where a @@ -169,7 +203,8 @@ structural_implementor(t, s) :- type_satisfies(s, t), s != t, target_has_nominal_implementor(t), !implementors(t, s), - type_instantiated(s, _). + type_instantiated(s, _), + !sat_cross_program(s, t). // ── satisfaction_unmeasured(TargetTypeHash, Name) ─────────────────────────── // EVERY SUPPRESSION COUNTABLE. A required member of a tested interface that NO diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl index b046589a2..1a0523d22 100644 --- a/graph/typescript/souffle/decls_all.dl +++ b/graph/typescript/souffle/decls_all.dl @@ -692,6 +692,11 @@ .decl program_module_count(c0:symbol,c1:number) .decl program_is_wide(c0:symbol) .decl global_decl_wide(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl tsconfig_dir(c0:symbol,c1:symbol) +.decl sat_cross_program_pair(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol) +.decl sat_tree_ask(c0:symbol,c1:symbol) +.decl module_in_program_tree(c0:symbol,c1:symbol) +.decl sat_cross_program(c0:symbol,c1:symbol) .decl remote_edge(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl remote_undetermined(c0:symbol,c1:symbol,c2:symbol) .decl remote_unsent(c0:symbol,c1:symbol,c2:symbol)