diff --git a/gemini-extension.json b/gemini-extension.json index ae6411e45..61a14197f 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "axiomcode", - "version": "0.2.2", + "version": "0.2.3", "description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.", "contextFileName": "plugins/axiomcode/AGENTS.md", "mcpServers": { diff --git a/graph/csharp/engine/expression-resolution/expr-type.dl b/graph/csharp/engine/expression-resolution/expr-type.dl index 72c20b462..2258c0e0b 100644 --- a/graph/csharp/engine/expression-resolution/expr-type.dl +++ b/graph/csharp/engine/expression-resolution/expr-type.dl @@ -281,7 +281,8 @@ expr_new_type(prov, e, gk) :- !expr_new_type_ref(prov, e, _), call_callee_name(prov, e, n), expr_ultimate_module(prov, e, mod), call_type_argc(prov, e, ar), - type_name_resolves(prov, mod, n, ar, gk). + type_name_resolves(prov, mod, n, ar, gk), + !expr_callee_name_dropped(prov, e, gk). // AN INVOCATION has its callee's return type. expr_type(prov, e, gk) :- diff --git a/graph/csharp/engine/resolution/local-binding.dl b/graph/csharp/engine/resolution/local-binding.dl index e7bc391fa..2c24e832a 100644 --- a/graph/csharp/engine/resolution/local-binding.dl +++ b/graph/csharp/engine/resolution/local-binding.dl @@ -239,7 +239,8 @@ ref_names_type(prov, e, gk) :- expr_written_name(prov, e, n), expr_type_argc(prov, e, ar), expr_ultimate_module(prov, e, mod), !ref_binds_value(prov, e), - type_name_resolves(prov, mod, n, ar, gk). + type_name_resolves(prov, mod, n, ar, gk), + !expr_name_dropped(prov, e, gk). // ── IMPLICIT `this` ───────────────────────────────────────────────────────── // An unqualified instance member reference has an implicit `this` receiver whose diff --git a/graph/csharp/engine/resolution/type-hierarchy.dl b/graph/csharp/engine/resolution/type-hierarchy.dl index 7c285be12..b329d00dd 100644 --- a/graph/csharp/engine/resolution/type-hierarchy.dl +++ b/graph/csharp/engine/resolution/type-hierarchy.dl @@ -73,6 +73,7 @@ heritage_resolves(prov, gk, pos, bgk) :- heritage_of_entity(prov, gk, pos, bn, ba, ""), type_group(prov, t, gk), type_module(prov, t, mod), type_name_resolves(prov, mod, bn, ba, bgk), + !heritage_name_dropped(prov, t, bn, bgk), bgk != gk. // ── THE SPLIT ─────────────────────────────────────────────────────────────── diff --git a/graph/csharp/engine/resolution/type-resolution.dl b/graph/csharp/engine/resolution/type-resolution.dl index f4d1b8031..75492a1d5 100644 --- a/graph/csharp/engine/resolution/type-resolution.dl +++ b/graph/csharp/engine/resolution/type-resolution.dl @@ -109,6 +109,107 @@ type_name_cand(prov, mod, s, ar, gk, 1) :- type_qn_tail(prov, qn, p, s), module_ns_chain(prov, mod, p), type_decl(prov, _, qn, ar, _, t), type_group(prov, t, gk). +// ── ...FROM THE SITE'S NAMESPACE, NOT THE FILE'S ──────────────────────────── +// The candidate above is per FILE: module_ns_chain holds every namespace the file +// declares a type in. A file declaring P.Models, P.Other.Models and a class in P +// therefore offered `Models.Report` written in P both P.Models.Report and +// P.Other.Models.Report -- and P.Other is a child of P, not in scope there. +// +// C# reads the first segment from the namespaces enclosing the SITE, innermost +// outward, and the innermost level that has a match wins: it does not merge +// levels, and a name found at a level hides the same name read in full from the +// global namespace. So per (site namespace, written name), the candidates are +// ranked by the length of the prefix they were found under and only the longest +// survives; a full match is the outermost level and survives only when no +// enclosing namespace matched. +// +// PRUNE-ONLY. The file-level answer is kept as it is and a site drops from it only +// what its own namespace rules out; the site's own answer is always one of the +// file-level ones, so nothing correct is lost. A site whose enclosing type is not +// known drops nothing. +type_name_rel_at(prov, n, ar, gk, p) :- + type_qn_tail(prov, qn, p, n), + type_decl(prov, _, qn, ar, _, t), type_group(prov, t, gk). +type_name_rel_src(prov, n, ar, gk) :- type_name_rel_at(prov, n, ar, gk, _). + +// The namespaces a name written in `ns` is read from, with the length of each. +ns_read_level(prov, ns, ns, l) :- ns_exists(prov, ns), l = strlen(ns). +ns_read_level(prov, ns, p, l) :- ns_prefix(prov, ns, p), l = strlen(p). + +// A dotted name some site in this file actually writes. type_name_resolves answers +// every tail of every type in every file whose chain holds its prefix, written or +// not, so the scoping below is keyed on what is written. Keyed by FILE as well, so +// the probe into type_name_resolves binds (module, name) and reuses the index its +// consumers already need rather than building one on the name alone. +type_name_written_dotted(prov, mod, n) :- + type_ref_name(prov, tr, n), contains(".", n), type_ref_module(prov, tr, mod). +type_name_written_dotted(prov, mod, n) :- + expr_written_name(prov, e, n), contains(".", n), expr_ultimate_module(prov, e, mod). +type_name_written_dotted(prov, mod, n) :- + call_callee_name(prov, e, n), contains(".", n), call_module(prov, e, mod). +type_name_written_dotted(prov, mod, n) :- + heritage_slot(prov, t, _, n, _, ""), contains(".", n), type_module(prov, t, mod). + +type_name_scope_demand(prov, mod, ns, n, ar) :- + type_name_written_dotted(prov, mod, n), + type_name_resolves(prov, mod, n, ar, _), + module_namespace_own(prov, mod, ns). +type_name_scope_level(prov, ns, n, ar, gk, l) :- + type_name_scope_demand(prov, _, ns, n, ar), + ns_read_level(prov, ns, p, l), + type_name_rel_at(prov, n, ar, gk, p). +type_name_scope_best(prov, ns, n, ar, b) :- + type_name_scope_level(prov, ns, n, ar, _, _), + b = max l : { type_name_scope_level(prov, ns, n, ar, _, l) }. +type_name_scope_keep(prov, ns, n, ar, gk) :- + type_name_scope_level(prov, ns, n, ar, gk, b), + type_name_scope_best(prov, ns, n, ar, b). + +// type_name_out_of_scope(Prov, Namespace, Name, Arity, GroupKey) -- a file-level +// answer that a site in `ns` does not see: a relative candidate not at the +// innermost matching level (a child or sibling namespace, or a shallower level), +// or a full match hidden by a relative one. +type_name_out_of_scope(prov, ns, n, ar, gk) :- + type_name_scope_demand(prov, mod, ns, n, ar), + type_name_resolves(prov, mod, n, ar, gk), + type_name_rel_src(prov, n, ar, gk), + !type_name_scope_keep(prov, ns, n, ar, gk). +type_name_out_of_scope(prov, ns, n, ar, gk) :- + type_name_scope_demand(prov, mod, ns, n, ar), + type_name_resolves(prov, mod, n, ar, gk), + !type_name_rel_src(prov, n, ar, gk), + type_name_scope_best(prov, ns, n, ar, _). + +// The site side, one per consumer below: the written name, the site's enclosing +// type, and that type's namespace. Joined from the (small) out-of-scope set, so only +// sites that write a pruned name are visited. +type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "TYPE"), t = o. +type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "METHOD"), method_owner(prov, t, o). +type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "FIELD"), field_decl(prov, _, t, o). +type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "PROPERTY"), property_decl(prov, _, t, o). +type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "EVENT"), event_decl(prov, _, t, o). +type_ref_scope_type(prov, tr, t) :- + type_ref_owner(prov, tr, o, "METHOD_PARAMETER"), param_of(prov, o, m), method_owner(prov, t, m). +type_ref_scope_type(prov, tr, t) :- + type_ref_owner(prov, tr, o, "VARIABLE"), var_method(prov, o, m), method_owner(prov, t, m). +type_ref_scope_type(prov, tr, t) :- type_ref_owner(prov, tr, o, "EXPRESSION"), expr_ultimate_type(prov, o, t). + +type_ref_name_dropped(prov, tr, gk) :- + type_name_out_of_scope(prov, ns, n, ar, gk), + type_ref_name(prov, tr, n), type_ref_argc(prov, tr, ar), + type_ref_scope_type(prov, tr, t), type_namespace(prov, t, ns). +expr_name_dropped(prov, e, gk) :- + type_name_out_of_scope(prov, ns, n, ar, gk), + expr_written_name(prov, e, n), expr_type_argc(prov, e, ar), + expr_ultimate_type(prov, e, t), type_namespace(prov, t, ns). +expr_callee_name_dropped(prov, e, gk) :- + type_name_out_of_scope(prov, ns, n, ar, gk), + call_callee_name(prov, e, n), call_type_argc(prov, e, ar), + expr_ultimate_type(prov, e, t), type_namespace(prov, t, ns). +heritage_name_dropped(prov, t, n, gk) :- + type_name_out_of_scope(prov, ns, n, ar, gk), + heritage_slot(prov, t, _, n, ar, ""), type_namespace(prov, t, ns). + // ── A CLIENT FILE NAMING A STAGED LIBRARY TYPE ────────────────────────────── // Every clause above is SINGLE-PROVENANCE: it joins a module and a declaration // under one `prov`, so a client module and a library namespace are never brought @@ -266,7 +367,8 @@ type_ref_resolves(prov, tr, gk) :- !type_ref_type_param(prov, tr, _), type_ref_argc(prov, tr, ar), type_ref_module(prov, tr, mod), - type_name_resolves(prov, mod, n, ar, gk). + type_name_resolves(prov, mod, n, ar, gk), + !type_ref_name_dropped(prov, tr, gk). // The module a type reference is written in, by owner kind. Every owner kind that // can carry a reference is listed; one that is missing makes every reference in that diff --git a/graph/csharp/souffle/decls_all.dl b/graph/csharp/souffle/decls_all.dl index 882cc9b7d..182506dd0 100644 --- a/graph/csharp/souffle/decls_all.dl +++ b/graph/csharp/souffle/decls_all.dl @@ -337,6 +337,7 @@ .decl expr_argument(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl expr_arity(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl expr_call_candidate(c0:symbol,c1:symbol,c2:symbol) +.decl expr_callee_name_dropped(c0:symbol,c1:symbol,c2:symbol) .decl expr_cast_ref(c0:symbol,c1:symbol,c2:symbol) .decl expr_child(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl expr_dynamic_typed(c0:symbol,c1:symbol) @@ -360,6 +361,7 @@ .decl expr_method_name_child(c0:symbol,c1:symbol,c2:symbol) .decl expr_method_ref(c0:symbol,c1:symbol,c2:symbol) .decl expr_module(c0:symbol,c1:symbol,c2:symbol) +.decl expr_name_dropped(c0:symbol,c1:symbol,c2:symbol) .decl expr_new_type(c0:symbol,c1:symbol,c2:symbol) .decl expr_new_type_ref(c0:symbol,c1:symbol,c2:symbol) .decl expr_node(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) @@ -481,6 +483,7 @@ .decl grpc_static_base(c0:symbol,c1:symbol,c2:symbol) .decl grpc_written_base(c0:symbol,c1:symbol) .decl heritage_has_ctor_args(c0:symbol,c1:symbol,c2:symbol) +.decl heritage_name_dropped(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl heritage_of_entity(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol) .decl heritage_resolves(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl heritage_slot(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol) @@ -745,6 +748,7 @@ .decl ns_encloses(c0:symbol,c1:symbol,c2:symbol) .decl ns_exists(c0:symbol,c1:symbol) .decl ns_prefix(c0:symbol,c1:symbol,c2:symbol) +.decl ns_read_level(c0:symbol,c1:symbol,c2:symbol,c3:number) .decl override_cand(c0:symbol,c1:symbol,c2:symbol,c3:number) .decl override_depth(c0:symbol,c1:symbol,c2:symbol,c3:number) .decl override_pair(c0:symbol,c1:symbol) @@ -980,10 +984,18 @@ .decl type_name_best_rank(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:number) .decl type_name_cand(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:number) .decl type_name_count(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:number) +.decl type_name_out_of_scope(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl type_name_predefined_unstaged(c0:symbol,c1:symbol,c2:symbol) +.decl type_name_rel_at(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) +.decl type_name_rel_src(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl type_name_resolves(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) +.decl type_name_scope_best(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:number) +.decl type_name_scope_demand(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) +.decl type_name_scope_keep(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) +.decl type_name_scope_level(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:number) .decl type_name_unresolved(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl type_name_visible(c0:symbol,c1:symbol,c2:symbol) +.decl type_name_written_dotted(c0:symbol,c1:symbol,c2:symbol) .decl type_namespace(c0:symbol,c1:symbol,c2:symbol) .decl type_nest_ancestor(c0:symbol,c1:symbol,c2:symbol) .decl type_ns(c0:symbol,c1:symbol,c2:symbol) @@ -1009,11 +1021,13 @@ .decl type_ref_is_tuple(c0:symbol,c1:symbol,c2:symbol) .decl type_ref_module(c0:symbol,c1:symbol,c2:symbol) .decl type_ref_name(c0:symbol,c1:symbol,c2:symbol) +.decl type_ref_name_dropped(c0:symbol,c1:symbol,c2:symbol) .decl type_ref_nullable(c0:symbol,c1:symbol) .decl type_ref_owner(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl type_ref_parent(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl type_ref_resolves(c0:symbol,c1:symbol,c2:symbol) .decl type_ref_root_position(c0:symbol,c1:symbol,c2:symbol) +.decl type_ref_scope_type(c0:symbol,c1:symbol,c2:symbol) .decl type_ref_type_param(c0:symbol,c1:symbol,c2:symbol) .decl type_satisfies(c0:symbol,c1:symbol,c2:symbol) .decl type_scope_key(c0:symbol,c1:symbol,c2:symbol) diff --git a/graph/test/typescript/cases/100-destructure-from-type-literal/src/globals.d.ts b/graph/test/typescript/cases/100-destructure-from-type-literal/src/globals.d.ts new file mode 100644 index 000000000..dd5843cc1 --- /dev/null +++ b/graph/test/typescript/cases/100-destructure-from-type-literal/src/globals.d.ts @@ -0,0 +1,11 @@ +// The case's own standard library under `noLib`. +interface Object {} +interface Boolean {} +interface Number {} +interface String {} +interface Array { length: number; } +interface Function {} +interface CallableFunction extends Function {} +interface NewableFunction extends Function {} +interface IArguments {} +interface RegExp {} diff --git a/graph/test/typescript/cases/100-destructure-from-type-literal/src/tsconfig.json b/graph/test/typescript/cases/100-destructure-from-type-literal/src/tsconfig.json new file mode 100644 index 000000000..dcbfb4f8d --- /dev/null +++ b/graph/test/typescript/cases/100-destructure-from-type-literal/src/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "noLib": true, "target": "es2022" } } diff --git a/graph/test/typescript/cases/100-destructure-from-type-literal/src/use.ts b/graph/test/typescript/cases/100-destructure-from-type-literal/src/use.ts new file mode 100644 index 000000000..1012b8536 --- /dev/null +++ b/graph/test/typescript/cases/100-destructure-from-type-literal/src/use.ts @@ -0,0 +1,47 @@ +// A name destructured out of a value whose declared type is an object TYPE LITERAL +// (inline, or through an alias) takes that member's type, as `opts.port.execute()` does. +export interface Port { execute(x: number): number } +export interface Clock { now(): number } + +export function inlineVar(opts: { port: Port; n: number }) { + const { port } = opts; + return port.execute(1); // -> Port.execute +} +export function memberAccess(opts: { port: Port }) { + return opts.port.execute(2); // -> Port.execute (already resolved: the reference) +} +type Opts = { port: Port; clock: Clock }; +export function aliasParam({ port, clock }: Opts) { + clock.now(); // -> Clock.now + return port.execute(3); // -> Port.execute +} +export function inlineParam({ port }: { port: Port }) { + return port.execute(4); // -> Port.execute +} +export function aliasVar(o: Opts) { + const { clock } = o; + return clock.now(); // -> Clock.now +} +export function nestedShape(opts: { deps: { port: Port } }) { + const { deps } = opts; + const { port } = deps; + return port.execute(5); // -> Port.execute +} +export function shapeMethod(opts: { run(x: number): number }) { + const { run } = opts; + return run(6); // -> the type literal's run +} + +// controls: no declared type, and a name the shape does not have +export function untyped(opts: any) { + const { port } = opts; + return port.execute(7); // stays unresolved +} +export function absent(opts: { clock: Clock }) { + const { port } = opts as { clock: Clock; port?: never }; + return port; // no call +} +export function otherShape(opts: { port: { execute(x: number): number } }) { + const { port } = opts; + return port.execute(8); // -> its own literal's execute, NOT Port.execute +} diff --git a/graph/test/typescript/cases/91-construct-through-shape/src/x.ts b/graph/test/typescript/cases/91-construct-through-shape/src/x.ts new file mode 100644 index 000000000..5eeddcc76 --- /dev/null +++ b/graph/test/typescript/cases/91-construct-through-shape/src/x.ts @@ -0,0 +1,16 @@ +// `new C(spec)` where C's type is a construct signature written as a type literal, an alias +// of one, a constructor type, or an interface. A factory that takes "the class to build" +// is written this way, and only the interface form resolved before. +interface WidgetDef { kind: string } +class Button { constructor(public spec: WidgetDef) {} } + +type WidgetClass = { new (spec: WidgetDef): T }; +function viaAlias(C: WidgetClass): T { return new C({ kind: "a" }); } +function viaLiteral(C: { new (spec: WidgetDef): Button }): Button { return new C({ kind: "b" }); } + +// controls: a constructor type and an interface with a construct signature +function viaCtorType(C: new (spec: WidgetDef) => Button): Button { return new C({ kind: "c" }); } +interface ButtonCtor { new (spec: WidgetDef): Button } +function viaInterface(C: ButtonCtor): Button { return new C({ kind: "d" }); } +const made = viaAlias(Button); +export { viaAlias, viaLiteral, viaCtorType, viaInterface, made }; diff --git a/graph/test/typescript/cases/92-namespace-typed-const/src/scorers.ts b/graph/test/typescript/cases/92-namespace-typed-const/src/scorers.ts new file mode 100644 index 000000000..75a9137c0 --- /dev/null +++ b/graph/test/typescript/cases/92-namespace-typed-const/src/scorers.ts @@ -0,0 +1,9 @@ +export type Scorer = (input: string) => number; +export function makeScorer(base: number): Scorer { + const fn: Scorer = (input) => input.length + base; + return fn; +} +// a function-typed const whose value is a factory's result: no literal to read +export const score: Scorer = makeScorer(1); +export const scoreArrow: Scorer = (input) => input.length; +export function plain(input: string): number { return input.length; } diff --git a/graph/test/typescript/cases/92-namespace-typed-const/src/x.ts b/graph/test/typescript/cases/92-namespace-typed-const/src/x.ts new file mode 100644 index 000000000..0517c80a5 --- /dev/null +++ b/graph/test/typescript/cases/92-namespace-typed-const/src/x.ts @@ -0,0 +1,9 @@ +// `ns.score(x)` reads the const off a namespace import. The named import of the same const +// resolved and the namespace read did not. +import * as scorers from "./scorers"; +import { score } from "./scorers"; +export function viaNamespace(s: string): number { return scorers.score(s); } +// controls: the named import, a const holding an arrow, and an exported function +export function viaNamed(s: string): number { return score(s); } +export function viaArrow(s: string): number { return scorers.scoreArrow(s); } +export function viaPlain(s: string): number { return scorers.plain(s); } diff --git a/graph/test/typescript/cases/93-literal-member-chain/src/x.ts b/graph/test/typescript/cases/93-literal-member-chain/src/x.ts new file mode 100644 index 000000000..ef3d4fa5e --- /dev/null +++ b/graph/test/typescript/cases/93-literal-member-chain/src/x.ts @@ -0,0 +1,17 @@ +// A member chain through nested inline object types. Each level reads a shape-typed +// property off a shape, and the chain stopped after the first. +class Logger { write(msg: string): void {} } +interface Order { + meta: { audit: { log: Logger; nested: { deep: { log: Logger } } } }; +} +function record(o: Order): void { + o.meta.audit.log.write("a"); + o.meta.audit.nested.deep.log.write("b"); +} +// controls: one level of literal, and a named type at every level +interface Flat { meta: { log: Logger } } +interface Named { meta: Meta } +interface Meta { log: Logger } +function flat(f: Flat): void { f.meta.log.write("c"); } +function named(n: Named): void { n.meta.log.write("d"); } +export { record, flat, named }; diff --git a/graph/test/typescript/cases/94-thistype-object-literal/src/x.ts b/graph/test/typescript/cases/94-thistype-object-literal/src/x.ts new file mode 100644 index 000000000..a6c0a6042 --- /dev/null +++ b/graph/test/typescript/cases/94-thistype-object-literal/src/x.ts @@ -0,0 +1,31 @@ +// `this` in the methods of an object literal written as `X & ThisType`: the compiler +// types it as T. A prototype table or a mixin is written this way, and every `this.m()` in +// one resolved to nothing. Covered: the marker through a generic alias bound by an explicit +// type argument, through an annotation, and written inline. +interface Greeter { name: string; greet(): string; shout(): string; hail(): string; wave(): string } +type Members = { [K in keyof T]?: T[K] } & ThisType; +interface Mixin { make(): T } +declare function mixin(name: string, members?: Members): Mixin; + +const viaTypeArgument = mixin("Greeter", { + greet() { return this.shout(); }, + shout: function shout() { return this.greet(); }, +}); +const viaAnnotation: Members = { + hail() { return this.greet(); }, +}; +interface Waver { wave?(): string } +const inline: Waver & ThisType = { + wave() { return this.shout(); }, +}; + +// controls: without the marker `this` is the literal; an arrow property keeps the class's +const plainLiteral = { + a() { return 1; }, + b() { return this.a(); }, +}; +class Host { + run(): string { return "r"; } + table = mixin("Host", { greet: () => this.run() }); +} +export { viaTypeArgument, viaAnnotation, inline, plainLiteral, Host }; diff --git a/graph/test/typescript/cases/95-bind-on-method-value/src/globals.d.ts b/graph/test/typescript/cases/95-bind-on-method-value/src/globals.d.ts new file mode 100644 index 000000000..befc3b74c --- /dev/null +++ b/graph/test/typescript/cases/95-bind-on-method-value/src/globals.d.ts @@ -0,0 +1,66 @@ +// ============================================================================ +// The case's OWN standard library: the minimum the checker needs under `noLib`. +// ============================================================================ +// WHY noLib. This mechanism's targets are declared in the REAL `lib.es5.d.ts`, and a +// case never has it: the case oracle only records a target inside the case's own files, +// and the engine is handed only `--library `. So with the real library +// neither side can name the target and the case measures nothing. Declaring `Function` +// and `CallableFunction` HERE puts them on both sides of the comparison. +// +// This is not a mock of the behaviour. The checker resolves `CallableFunction` by NAME +// from whatever is in scope and then applies `strictBindCallApply` itself, so these +// declarations exercise the real decision. Verified: the same two declarations answer +// CallableFunction under `strict: true` and Function under +// `strict: false, strictBindCallApply: false`. +// +// Everything other than Function/CallableFunction is here only because `noLib` removes +// it and the checker requires it. Keep this file minimal: an interface added here is a +// declaration the engine can resolve to, and an accidental one is an accidental answer. + +interface Object { toString(): string; } +interface Boolean {} +interface Number {} +interface String { padStart(width: number): string; padEnd(width: number): string; trim(): string; } +interface Array { length: number; } +interface ReadonlyArray { length: number; } +interface IArguments {} +interface RegExp {} + +// ── the two declarations the flag chooses between ─────────────────────────── +// Mirrors lib.es5.d.ts: `Function` declares all three, and `CallableFunction extends +// Function` REDECLARES the same three with precise generic signatures. Both are in +// scope at every site; only the flag decides. +// NO explicit `this` parameter on these. lib.es5.d.ts declares them with one, but the +// case oracle's edge label includes a `this` parameter in the signature while the engine +// (correctly) does not treat it as a parameter. So `this: Function` renders as +// `Function#apply(Function,any,any)` on one side and `Function#apply(any,any)` on the +// other, and every site scores as a disagreement the engine did not actually make. +interface Function { + apply(thisArg: any, argArray?: any): any; + call(thisArg: any, ...argArray: any[]): any; + bind(thisArg: any, ...argArray: any[]): any; + readonly name: string; + toString(): string; +} + +interface CallableFunction extends Function { + // Simply typed on purpose. lib.es5.d.ts gives these precise generic signatures + // (`this: (this: T, ...args: A) => R`), and this fixture does not need them: what it + // tests is that TWO declarations of the same member names exist and that + // `strictBindCallApply` picks between them, which the checker decides from the flag + // and not from how well a signature fits. A function-typed parameter also renders + // badly in the case oracle's edge labels: the label splits on the commas inside + // `(this: T, ...args: A) => R` and emits `apply(args: A) =,T,T)`, which can never + // match the engine's label, so every site scored as a disagreement. + // + // Arguments are optional and returns are `any` so every call form in the case source + // typechecks. The two declarations stay distinguishable without help: an edge label + // carries the OWNER, so `CallableFunction#call` and `Function#call` never collide, and + // that owner is exactly what the flag decides. + call(thisArg: any, arg0?: any, arg1?: any): any; + apply(thisArg: any, args?: any): any; + bind(thisArg: any, arg0?: any): any; +} + + +interface NewableFunction extends Function {} diff --git a/graph/test/typescript/cases/95-bind-on-method-value/src/tsconfig.json b/graph/test/typescript/cases/95-bind-on-method-value/src/tsconfig.json new file mode 100644 index 000000000..a19ccad92 --- /dev/null +++ b/graph/test/typescript/cases/95-bind-on-method-value/src/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "strict": true, + "noLib": true + } +} diff --git a/graph/test/typescript/cases/95-bind-on-method-value/src/x.ts b/graph/test/typescript/cases/95-bind-on-method-value/src/x.ts new file mode 100644 index 000000000..13b4539d2 --- /dev/null +++ b/graph/test/typescript/cases/95-bind-on-method-value/src/x.ts @@ -0,0 +1,21 @@ +// `call` / `apply` / `bind` on a METHOD read as a value, `this.render.bind(this)`. The +// receiver names a method, so it had no type for the Function members to attach to. +// src/globals.d.ts and noLib, as in case 21, put the targets inside the case. +class View { + render(): string { return "v"; } + get label(): () => string { return () => "l"; } + handler: (x: number) => number = (x) => x; + wire(): void { + this.render = this.render.bind(this); + const r = this.render.call(this); + const s = this.render.apply(this, []); + // the bound result runs the method + const bound = this.render.bind(this); + const b = bound(); + // controls: a function-typed field already resolved; a getter's value is what it returns + const h = this.handler.bind(this); + const l = this.label.bind(this); + } +} +function runOn(v: View): string { return v.render.call(v); } +export { View, runOn }; diff --git a/graph/test/typescript/cases/96-arithmetic-result-receiver/src/arith.ts b/graph/test/typescript/cases/96-arithmetic-result-receiver/src/arith.ts new file mode 100644 index 000000000..aced3df06 --- /dev/null +++ b/graph/test/typescript/cases/96-arithmetic-result-receiver/src/arith.ts @@ -0,0 +1,41 @@ +// A call on the RESULT of an arithmetic operator. The operators below yield a number +// (or a bigint when the operands are bigints), so the receiver is Number's or BigInt's. +export function elapsed(start: number, end: number): string { + return (end - start).toFixed(1); +} +export function ratio(a: number, b: number): string { + return (a / b).toFixed(2); +} +export function scaled(a: number): string { + return (a * 3 % 7).toString(16); +} +export function power(a: number): string { + return (a ** 2).toFixed(0); +} +export function masked(a: number): string { + return (a & 0xff).toString(2); +} +export function negated(a: number): string { + return (-a).toFixed(0); +} +export function parsed(s: string): string { + return (+s).toFixed(0); +} +export function total(a: number, b: number): string { + return (a + b).toFixed(1); +} +export function bigDiff(a: bigint, b: bigint): string { + return (a - b).toString(); +} +export function bigNeg(a: bigint): string { + return (-a).toString(); +} + +// controls: a `+` with a string operand is a concatenation and stays String's alone, +// and a bigint result never names Number's declaration. +export function joined(s: string, n: number): string { + return (s + n).toString(); +} +export function joinedTrim(s: string, n: number): string { + return (n + s).trim(); +} diff --git a/graph/test/typescript/cases/96-arithmetic-result-receiver/src/globals.d.ts b/graph/test/typescript/cases/96-arithmetic-result-receiver/src/globals.d.ts new file mode 100644 index 000000000..86c7a3bfe --- /dev/null +++ b/graph/test/typescript/cases/96-arithmetic-result-receiver/src/globals.d.ts @@ -0,0 +1,14 @@ +// The case's own standard library under `noLib`. Number, BigInt and String all declare +// `toString`, so a receiver typed as the wrong one of the three names a different +// declaration rather than the same one by luck. +interface Object { valueOf(): Object; } +interface Boolean {} +interface Number { toFixed(digits?: number): string; toString(radix?: number): string; } +interface BigInt { toString(radix?: number): string; } +interface String { trim(): string; toString(): string; } +interface Array { length: number; } +interface Function {} +interface CallableFunction extends Function {} +interface NewableFunction extends Function {} +interface IArguments {} +interface RegExp {} diff --git a/graph/test/typescript/cases/96-arithmetic-result-receiver/src/tsconfig.json b/graph/test/typescript/cases/96-arithmetic-result-receiver/src/tsconfig.json new file mode 100644 index 000000000..dcbfb4f8d --- /dev/null +++ b/graph/test/typescript/cases/96-arithmetic-result-receiver/src/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "noLib": true, "target": "es2022" } } diff --git a/graph/test/typescript/cases/97-nullish-default-receiver/src/bus.ts b/graph/test/typescript/cases/97-nullish-default-receiver/src/bus.ts new file mode 100644 index 000000000..8714caed5 --- /dev/null +++ b/graph/test/typescript/cases/97-nullish-default-receiver/src/bus.ts @@ -0,0 +1,37 @@ +// `a ?? b` and `a || b` carry the left operand's type. A defaulted lookup is the +// ordinary way to read-or-create an entry, and the receiver after the default had no +// type. +type Listener = (payload: unknown) => void; + +export class Bus { + private readonly listeners = new Map(); + private readonly labels = new Map(); + + on(event: string, l: Listener): void { + const list = this.listeners.get(event) ?? []; + list.push(l); + this.listeners.set(event, list); + } + direct(event: string, l: Listener): number { + return (this.listeners.get(event) ?? []).push(l); + } + label(event: string): string { + return (this.labels.get(event) || "none").trim(); + } + + // A left side that is never nullish carries its type through the default too. + nonNullable(xs: Listener[], l: Listener): number { + return (xs ?? []).push(l); + } + + // controls: an annotated default resolved before and must not change, and an untyped + // left side does not borrow the right side's type. + annotated(event: string, l: Listener): number { + const list: Listener[] = this.listeners.get(event) ?? []; + return list.push(l); + } + untyped(x: any, l: Listener): void { + const list = x ?? []; + list.push(l); + } +} diff --git a/graph/test/typescript/cases/97-nullish-default-receiver/src/globals.d.ts b/graph/test/typescript/cases/97-nullish-default-receiver/src/globals.d.ts new file mode 100644 index 000000000..a93966091 --- /dev/null +++ b/graph/test/typescript/cases/97-nullish-default-receiver/src/globals.d.ts @@ -0,0 +1,16 @@ +// The case's own standard library under `noLib`: a Map whose `get` may return +// undefined, and an Array to default to. +interface Object {} +interface Boolean {} +interface Number {} +interface String { trim(): string; } +interface Array { length: number; push(...items: T[]): number; } +interface ReadonlyArray { length: number; } +interface Map { get(key: K): V | undefined; set(key: K, value: V): this; } +interface MapConstructor { new (): Map; } +declare var Map: MapConstructor; +interface Function {} +interface CallableFunction extends Function {} +interface NewableFunction extends Function {} +interface IArguments {} +interface RegExp {} diff --git a/graph/test/typescript/cases/97-nullish-default-receiver/src/tsconfig.json b/graph/test/typescript/cases/97-nullish-default-receiver/src/tsconfig.json new file mode 100644 index 000000000..dcbfb4f8d --- /dev/null +++ b/graph/test/typescript/cases/97-nullish-default-receiver/src/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "noLib": true, "target": "es2022" } } diff --git a/graph/test/typescript/cases/98-namespace-const-callable/lib/runtime.d.ts b/graph/test/typescript/cases/98-namespace-const-callable/lib/runtime.d.ts new file mode 100644 index 000000000..bbcdaac96 --- /dev/null +++ b/graph/test/typescript/cases/98-namespace-const-callable/lib/runtime.d.ts @@ -0,0 +1,14 @@ +// A runtime's global namespace, staged as a library: a const typed by an interface with +// a call signature, a const typed by a plain interface, and a function. +declare namespace Rt { + export interface TestFn { + (name: string, fn: () => void): void; + only(name: string, fn: () => void): void; + } + export interface Env { + get(key: string): string | undefined; + } + export const test: TestFn; + export const env: Env; + export function serve(port: number): void; +} diff --git a/graph/test/typescript/cases/98-namespace-const-callable/src/globals.d.ts b/graph/test/typescript/cases/98-namespace-const-callable/src/globals.d.ts new file mode 100644 index 000000000..dd5843cc1 --- /dev/null +++ b/graph/test/typescript/cases/98-namespace-const-callable/src/globals.d.ts @@ -0,0 +1,11 @@ +// The case's own standard library under `noLib`. +interface Object {} +interface Boolean {} +interface Number {} +interface String {} +interface Array { length: number; } +interface Function {} +interface CallableFunction extends Function {} +interface NewableFunction extends Function {} +interface IArguments {} +interface RegExp {} diff --git a/graph/test/typescript/cases/98-namespace-const-callable/src/tsconfig.json b/graph/test/typescript/cases/98-namespace-const-callable/src/tsconfig.json new file mode 100644 index 000000000..dcbfb4f8d --- /dev/null +++ b/graph/test/typescript/cases/98-namespace-const-callable/src/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true, "noLib": true, "target": "es2022" } } diff --git a/graph/test/typescript/cases/98-namespace-const-callable/src/use.ts b/graph/test/typescript/cases/98-namespace-const-callable/src/use.ts new file mode 100644 index 000000000..6d7e8cbae --- /dev/null +++ b/graph/test/typescript/cases/98-namespace-const-callable/src/use.ts @@ -0,0 +1,36 @@ +/// +// (The reference is for the compiler alone: the parser reads this directory only, so +// `Rt` reaches the engine as an unbound global name and the namespace as library IR.) +// A call through a const declared in a namespace. The const's type carries the call +// signature, and a member call one hop further needs the const's type as its receiver. +export function suite(): void { + Rt.test("adds", () => {}); + Rt.test.only("focused", () => {}); + Rt.env.get("HOME"); +} + +// The same through a namespace declared in the project. +interface Runner { (n: number): number; describe(): string; } +interface Step { (n: number): number; } +declare function makeRunner(): Runner; +namespace Local { + export const run: Runner = makeRunner(); + export const arrow: Step = (n) => n + 1; + export function helper(): void {} +} +export function local(): void { + Local.run(1); + Local.run.describe(); +} + +// controls: a namespace function keeps its one edge; a const holding an arrow keeps its +// body as the target with no bodiless signature beside it; a parameter that shadows the +// namespace's name resolves against its own type. +export function controls(): void { + Rt.serve(8000); + Local.helper(); + Local.arrow(2); +} +export function shadowed(Rt: { test(name: string): void }): void { + Rt.test("shadowed"); +} 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/08-function-values-and-callbacks.edges b/graph/test/typescript/expected/08-function-values-and-callbacks.edges index c0aeef314..f5d5e2e68 100644 --- a/graph/test/typescript/expected/08-function-values-and-callbacks.edges +++ b/graph/test/typescript/expected/08-function-values-and-callbacks.edges @@ -3,7 +3,8 @@ ambiguous_unknown METHOD_CALL values#drive() @L29 -> - callback_registered FUNCTION_CALL values#useLibrary() @L40 -> values#local(?) known_edge CONSTRUCTOR_CALL values#drive() @L27 -> Pipeline#() known_edge FUNCTION_CALL values#apply(Transform,string) @L12 -> values#(string) -known_edge FUNCTION_CALL values#drive() @L30 -> values#(string) known_edge FUNCTION_CALL values#drive() @L30 -> values#apply(Transform,string) known_edge METHOD_CALL Pipeline#run(string) @L19 -> values#(string) known_edge METHOD_CALL values#drive() @L30 -> Pipeline#run(string) +multi_inferred FUNCTION_CALL values#drive() @L30 -> Pipeline#tag(string) +multi_inferred FUNCTION_CALL values#drive() @L30 -> values#(string) diff --git a/graph/test/typescript/expected/08-function-values-and-callbacks.envelope b/graph/test/typescript/expected/08-function-values-and-callbacks.envelope index b629f2f6b..95c6c001d 100644 --- a/graph/test/typescript/expected/08-function-values-and-callbacks.envelope +++ b/graph/test/typescript/expected/08-function-values-and-callbacks.envelope @@ -4,4 +4,3 @@ value values#@4:25 -> values#Pipeline.tag value_call values#Pipeline.run -> values#@6 value_call values#apply -> values#@6 value_call values#apply -> values#@7 -value_call values#drive -> values#Pipeline.tag diff --git a/graph/test/typescript/expected/08-function-values-and-callbacks.lib.edges b/graph/test/typescript/expected/08-function-values-and-callbacks.lib.edges index 20caf154d..03047edcb 100644 --- a/graph/test/typescript/expected/08-function-values-and-callbacks.lib.edges +++ b/graph/test/typescript/expected/08-function-values-and-callbacks.lib.edges @@ -5,7 +5,8 @@ boundary_lib FUNCTION_CALL values#useLibrary() @L40 -> hooks#withHook(Hook,strin callback_registered FUNCTION_CALL values#useLibrary() @L40 -> values#local(?) known_edge CONSTRUCTOR_CALL values#drive() @L27 -> Pipeline#() known_edge FUNCTION_CALL values#apply(Transform,string) @L12 -> values#(string) -known_edge FUNCTION_CALL values#drive() @L30 -> values#(string) known_edge FUNCTION_CALL values#drive() @L30 -> values#apply(Transform,string) known_edge METHOD_CALL Pipeline#run(string) @L19 -> values#(string) known_edge METHOD_CALL values#drive() @L30 -> Pipeline#run(string) +multi_inferred FUNCTION_CALL values#drive() @L30 -> Pipeline#tag(string) +multi_inferred FUNCTION_CALL values#drive() @L30 -> values#(string) diff --git a/graph/test/typescript/expected/08-function-values-and-callbacks.lib.envelope b/graph/test/typescript/expected/08-function-values-and-callbacks.lib.envelope index c3b500e6a..2df0528fc 100644 --- a/graph/test/typescript/expected/08-function-values-and-callbacks.lib.envelope +++ b/graph/test/typescript/expected/08-function-values-and-callbacks.lib.envelope @@ -5,4 +5,3 @@ value values#@4:25 -> values#Pipeline.tag value_call values#Pipeline.run -> values#@6 value_call values#apply -> values#@6 value_call values#apply -> values#@7 -value_call values#drive -> values#Pipeline.tag diff --git a/graph/test/typescript/expected/08-function-values-and-callbacks.lib.oracle b/graph/test/typescript/expected/08-function-values-and-callbacks.lib.oracle index 0b8515211..068febee7 100644 --- a/graph/test/typescript/expected/08-function-values-and-callbacks.lib.oracle +++ b/graph/test/typescript/expected/08-function-values-and-callbacks.lib.oracle @@ -1,2 +1,3 @@ -oracle=8 engine=9 agree=8 missing=0 (known 0, NEW 0) extra=1 +oracle=8 engine=10 agree=8 missing=0 (known 0, NEW 0) extra=2 + extra values#drive() -> Pipeline#tag(string) extra values#useLibrary() -> hooks#identity(?) diff --git a/graph/test/typescript/expected/08-function-values-and-callbacks.oracle b/graph/test/typescript/expected/08-function-values-and-callbacks.oracle index 4794eb8a3..84fbe5b0d 100644 --- a/graph/test/typescript/expected/08-function-values-and-callbacks.oracle +++ b/graph/test/typescript/expected/08-function-values-and-callbacks.oracle @@ -1 +1,2 @@ -oracle=6 engine=6 agree=6 missing=0 (known 0, NEW 0) extra=0 +oracle=6 engine=7 agree=6 missing=0 (known 0, NEW 0) extra=1 + extra values#drive() -> Pipeline#tag(string) diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.edges b/graph/test/typescript/expected/100-destructure-from-type-literal.edges new file mode 100644 index 000000000..6b4bede19 --- /dev/null +++ b/graph/test/typescript/expected/100-destructure-from-type-literal.edges @@ -0,0 +1,10 @@ +ambiguous_unknown METHOD_CALL use#untyped(any) @L38 -> - +known_edge FUNCTION_CALL use#shapeMethod({ run(x: number): number }) @L32 -> use#run(number) +known_edge METHOD_CALL use#aliasParam(Opts) @L15 -> Clock#now() +known_edge METHOD_CALL use#aliasParam(Opts) @L16 -> Port#execute(number) +known_edge METHOD_CALL use#aliasVar(Opts) @L23 -> Clock#now() +known_edge METHOD_CALL use#inlineParam({ port: Port }) @L19 -> Port#execute(number) +known_edge METHOD_CALL use#inlineVar({ port: Port; n: number }) @L8 -> Port#execute(number) +known_edge METHOD_CALL use#memberAccess({ port: Port }) @L11 -> Port#execute(number) +known_edge METHOD_CALL use#nestedShape({ deps: { port: Port } }) @L28 -> Port#execute(number) +known_edge METHOD_CALL use#otherShape({ port: { execute(x: number): number } }) @L46 -> use#execute(number) diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.entries b/graph/test/typescript/expected/100-destructure-from-type-literal.entries new file mode 100644 index 000000000..17fbd316e --- /dev/null +++ b/graph/test/typescript/expected/100-destructure-from-type-literal.entries @@ -0,0 +1,13 @@ +── entry_point (12) ── + exported_from_entry_module use#absent use.ts:40 + exported_from_entry_module use#aliasParam use.ts:14 + exported_from_entry_module use#aliasVar use.ts:21 + exported_from_entry_module use#inlineParam use.ts:18 + exported_from_entry_module use#inlineVar use.ts:6 + exported_from_entry_module use#memberAccess use.ts:10 + exported_from_entry_module use#nestedShape use.ts:25 + exported_from_entry_module use#otherShape use.ts:44 + exported_from_entry_module use#shapeMethod use.ts:30 + exported_from_entry_module use#untyped use.ts:36 + unimported_module globals# globals.d.ts:1 + unimported_module use# use.ts:1 diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.fields b/graph/test/typescript/expected/100-destructure-from-type-literal.fields new file mode 100644 index 000000000..33a761ca6 --- /dev/null +++ b/graph/test/typescript/expected/100-destructure-from-type-literal.fields @@ -0,0 +1 @@ +known_edge read use#memberAccess({ port: Port }) -> { port: Port }#port diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.fields-oracle b/graph/test/typescript/expected/100-destructure-from-type-literal.fields-oracle new file mode 100644 index 000000000..503afa2c0 --- /dev/null +++ b/graph/test/typescript/expected/100-destructure-from-type-literal.fields-oracle @@ -0,0 +1,9 @@ +100-destructure-from-type-literal [fields] + precision 0.0000 (0 correct, 1 wrong) + recall 0.0000 (0 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 + WRONG use#memberAccess({ port: Port }) READ { port: Port }#port + MISSING use#memberAccess({ port: Port }) READ use#port diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.oracle b/graph/test/typescript/expected/100-destructure-from-type-literal.oracle new file mode 100644 index 000000000..dbaf45eaa --- /dev/null +++ b/graph/test/typescript/expected/100-destructure-from-type-literal.oracle @@ -0,0 +1 @@ +oracle=9 engine=9 agree=9 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.type-use b/graph/test/typescript/expected/100-destructure-from-type-literal.type-use new file mode 100644 index 000000000..07d5aaba0 --- /dev/null +++ b/graph/test/typescript/expected/100-destructure-from-type-literal.type-use @@ -0,0 +1,9 @@ +known_edge FIELD_TYPE 1 Opts [TYPE] -> Clock +known_edge FIELD_TYPE 1 Opts [TYPE] -> Port +known_edge FIELD_TYPE 1 use [EXPRESSION] -> Clock +known_edge FIELD_TYPE 1 use [METHOD_PARAM] -> Clock +known_edge FIELD_TYPE 1 use [METHOD_PARAM] -> Port +known_edge FIELD_TYPE 2 use [METHOD_PARAM] -> Port +known_edge METHOD_PARAM 0 use [METHOD_PARAM] -> Opts +known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function +known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function diff --git a/graph/test/typescript/expected/100-destructure-from-type-literal.types-oracle b/graph/test/typescript/expected/100-destructure-from-type-literal.types-oracle new file mode 100644 index 000000000..feb77f3a8 --- /dev/null +++ b/graph/test/typescript/expected/100-destructure-from-type-literal.types-oracle @@ -0,0 +1,7 @@ +100-destructure-from-type-literal [types] + precision 1.0000 (7 correct, 0 wrong) + recall 1.0000 (7 of 7 the compiler resolved) + sites 12 resolved 12 (100.0%) + tiers known_edge=12 + contexts FIELD_TYPE=8 METHOD_PARAM=2 SUPER_TYPE=2 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/75-jsx-wrapper-guards.edges b/graph/test/typescript/expected/75-jsx-wrapper-guards.edges index 78ebdfe74..562520af9 100644 --- a/graph/test/typescript/expected/75-jsx-wrapper-guards.edges +++ b/graph/test/typescript/expected/75-jsx-wrapper-guards.edges @@ -7,14 +7,9 @@ ambient_terminal FUNCTION_CALL app#() @L22 -> parts#dynamic(() =Fc ambient_terminal FUNCTION_CALL app#() @L29 -> parts#memo(Fc) ambient_terminal FUNCTION_CALL app#() @L30 -> parts#observer(Fc) ambiguous_unknown FUNCTION_CALL app#MemoInner() @L29 -> - -ambiguous_unknown FUNCTION_CALL app#RenameCall({ run: () =,{ run: () =) @L67 -> - -ambiguous_unknown FUNCTION_CALL app#ShorthandCall({ Button: () =,{ Button: () =) @L64 -> - ambiguous_unknown FUNCTION_CALL heavy#Heavy() @L11 -> - ambiguous_unknown FUNCTION_CALL heavy#NamedHeavy() @L15 -> - -ambiguous_unknown JSX_COMPONENT_CALL app#(?,?) @L57 -> - ambiguous_unknown JSX_COMPONENT_CALL app#Page() @L42 -> - -ambiguous_unknown JSX_COMPONENT_CALL app#RenameTag({ icon: Fc },{ icon: Fc }) @L54 -> - -ambiguous_unknown JSX_COMPONENT_CALL app#ShorthandTag({ Button: Fc },{ Button: Fc }) @L51 -> - ambiguous_unknown METHOD_CALL app#() @L15 -> - ambiguous_unknown METHOD_CALL app#() @L16 -> - ambiguous_unknown METHOD_CALL app#() @L18 -> - @@ -30,7 +25,7 @@ callback_registered FUNCTION_CALL app#() @L30 -> app#ObservedImpl() callback_registered METHOD_CALL app#() @L15 -> app#(?) callback_registered METHOD_CALL app#() @L16 -> app#(?) callback_registered METHOD_CALL app#() @L18 -> app#(?) -callback_registered METHOD_CALL app#ArrowRenameTag({ icon: Fc }[]) @L57 -> app#(?,?) +callback_registered METHOD_CALL app#ArrowRenameTag({ icon: Fc }[]) @L57 -> app#(?) intrinsic_terminal DYNAMIC_IMPORT_CALL app#() @L15 -> - intrinsic_terminal DYNAMIC_IMPORT_CALL app#() @L16 -> - intrinsic_terminal DYNAMIC_IMPORT_CALL app#() @L18 -> - @@ -41,10 +36,13 @@ known_edge FUNCTION_CALL app#() @L25 -> app#factoryArgBody() known_edge FUNCTION_CALL app#() @L25 -> parts#makeThing(() =) known_edge FUNCTION_CALL app#() @L26 -> parts#useMemoLike(() =,unknown[]) known_edge FUNCTION_CALL app#MemoInner() @L29 -> app#memoBody() +known_edge FUNCTION_CALL app#RenameCall({ run: () =) @L67 -> app#() +known_edge FUNCTION_CALL app#ShorthandCall({ Button: () =) @L64 -> app#() known_edge FUNCTION_CALL heavy#Heavy() @L11 -> heavy#heavyWork() known_edge FUNCTION_CALL heavy#NamedHeavy() @L15 -> heavy#namedWork() known_edge FUNCTION_CALL parts#makeThing(() =) @L23 -> parts#() known_edge FUNCTION_CALL parts#useMemoLike(() =,unknown[]) @L27 -> parts#() +known_edge JSX_COMPONENT_CALL app#(?) @L57 -> parts#(T) known_edge JSX_COMPONENT_CALL app#Page() @L35 -> heavy#NamedHeavy() known_edge JSX_COMPONENT_CALL app#Page() @L36 -> heavy#NamedHeavy() known_edge JSX_COMPONENT_CALL app#Page() @L37 -> heavy#NamedHeavy() @@ -55,4 +53,6 @@ known_edge JSX_COMPONENT_CALL app#Page() @L41 -> parts#(T) known_edge JSX_COMPONENT_CALL app#Page() @L43 -> app#MemoInner() known_edge JSX_COMPONENT_CALL app#Page() @L44 -> app#ObservedImpl() known_edge JSX_COMPONENT_CALL app#PlainParamTag(Fc) @L60 -> parts#(T) +known_edge JSX_COMPONENT_CALL app#RenameTag({ icon: Fc }) @L54 -> parts#(T) +known_edge JSX_COMPONENT_CALL app#ShorthandTag({ Button: Fc }) @L51 -> parts#(T) known_edge JSX_COMPONENT_CALL app#UsesButton() @L72 -> ui/Button#Button({ label: string }) diff --git a/graph/test/typescript/expected/75-jsx-wrapper-guards.known-missing b/graph/test/typescript/expected/75-jsx-wrapper-guards.known-missing index 4b3ae9b66..cb34e31cb 100644 --- a/graph/test/typescript/expected/75-jsx-wrapper-guards.known-missing +++ b/graph/test/typescript/expected/75-jsx-wrapper-guards.known-missing @@ -2,15 +2,6 @@ # 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. # -# A name bound by a DESTRUCTURED parameter carries no declared type the engine reads, so -# the call through it stays unresolved. The compiler names the prop's type signature. -# Before this case these sites resolved to the same-named IMPORT (a wrong edge); unresolved -# is the honest answer until destructured parameters are typed. -app#(?) -> parts#(T) -app#RenameTag({ icon: Fc }) -> parts#(T) -app#ShorthandTag({ Button: Fc }) -> parts#(T) -app#RenameCall({ run: () =) -> app#() -app#ShorthandCall({ Button: () =) -> app#() # `useMemoLike(() => function MadeInner…)` returns what its argument RETURNS, through a # generic T. Argument 0 is not a component wrapper, so the tag no longer lands on the # argument arrow; following the generic return to MadeInner is value flow the engine diff --git a/graph/test/typescript/expected/75-jsx-wrapper-guards.oracle b/graph/test/typescript/expected/75-jsx-wrapper-guards.oracle index ff8858f30..94c50e44c 100644 --- a/graph/test/typescript/expected/75-jsx-wrapper-guards.oracle +++ b/graph/test/typescript/expected/75-jsx-wrapper-guards.oracle @@ -1,10 +1,5 @@ -oracle=21 engine=19 agree=15 missing=6 (known 6, NEW 0) extra=4 - known app#(?) -> parts#(T) +oracle=21 engine=24 agree=20 missing=1 (known 1, NEW 0) extra=4 known app#Page() -> app#MadeInner() - known app#RenameCall({ run: () =) -> app#() - known app#RenameTag({ icon: Fc }) -> parts#(T) - known app#ShorthandCall({ Button: () =) -> app#() - known app#ShorthandTag({ Button: Fc }) -> parts#(T) extra app#Page() -> app#MemoInner() extra app#Page() -> app#ObservedImpl() extra app#Page() -> heavy#Heavy() diff --git a/graph/test/typescript/expected/80-method-passed-by-reference.edges b/graph/test/typescript/expected/80-method-passed-by-reference.edges index 3d1be41b8..0e5d76362 100644 --- a/graph/test/typescript/expected/80-method-passed-by-reference.edges +++ b/graph/test/typescript/expected/80-method-passed-by-reference.edges @@ -1,8 +1,5 @@ ambient_terminal FUNCTION_CALL Widget#now(number[]) @L40 -> globals#setTimeout(() =,number) ambient_terminal FUNCTION_CALL Widget#wire(Target) @L24 -> globals#setTimeout(() =,number) -ambiguous_unknown METHOD_CALL Widget#now(number[]) @L40 -> - -ambiguous_unknown METHOD_CALL Widget#start(number[]) @L18 -> - -ambiguous_unknown METHOD_CALL Widget#wire(Target) @L24 -> - callback_registered FUNCTION_CALL Widget#wire(Target) @L24 -> Widget#onHover() callback_registered METHOD_CALL Widget#relay(Widget,number[]) @L29 -> Widget#handle(number) callback_registered METHOD_CALL Widget#start(number[]) @L18 -> Widget#handle(number) @@ -10,11 +7,14 @@ callback_registered METHOD_CALL Widget#start(number[]) @L19 -> Widget#handle(num callback_registered METHOD_CALL Widget#tap(Target) @L34 -> Widget#() callback_registered METHOD_CALL Widget#wire(Target) @L23 -> Widget#onClick() known_edge METHOD_CALL Widget#() @L34 -> Widget#onTap() +known_edge METHOD_CALL Widget#now(number[]) @L40 -> Function#call(unknown,unknown[]) known_edge METHOD_CALL Widget#now(number[]) @L41 -> Array#forEach((v: T) =,unknown) known_edge METHOD_CALL Widget#now(number[]) @L42 -> Array#forEach((v: T) =,unknown) known_edge METHOD_CALL Widget#relay(Widget,number[]) @L29 -> Array#forEach((v: T) =,unknown) known_edge METHOD_CALL Widget#start(number[]) @L18 -> Array#forEach((v: T) =,unknown) +known_edge METHOD_CALL Widget#start(number[]) @L18 -> Function#bind(unknown,unknown[]) known_edge METHOD_CALL Widget#start(number[]) @L19 -> Array#map((v: T) =,unknown) known_edge METHOD_CALL Widget#tap(Target) @L34 -> Target#addEventListener(string,() =) known_edge METHOD_CALL Widget#wire(Target) @L23 -> Target#addEventListener(string,() =) +known_edge METHOD_CALL Widget#wire(Target) @L24 -> Function#bind(unknown,unknown[]) known_edge PROPERTY_READ Widget#now(number[]) @L42 -> Widget#current() diff --git a/graph/test/typescript/expected/80-method-passed-by-reference.oracle b/graph/test/typescript/expected/80-method-passed-by-reference.oracle index 196060769..f8e70220b 100644 --- a/graph/test/typescript/expected/80-method-passed-by-reference.oracle +++ b/graph/test/typescript/expected/80-method-passed-by-reference.oracle @@ -1,3 +1,6 @@ -oracle=8 engine=10 agree=8 missing=0 (known 0, NEW 0) extra=2 +oracle=8 engine=13 agree=8 missing=0 (known 0, NEW 0) extra=5 + extra Widget#now(number[]) -> Function#call(unknown,unknown[]) extra Widget#now(number[]) -> globals#setTimeout(() =,number) + extra Widget#start(number[]) -> Function#bind(unknown,unknown[]) + extra Widget#wire(Target) -> Function#bind(unknown,unknown[]) extra Widget#wire(Target) -> globals#setTimeout(() =,number) diff --git a/graph/test/typescript/expected/83-closed-world-dispatch.edges b/graph/test/typescript/expected/83-closed-world-dispatch.edges index 3e35361f4..ad9e18770 100644 --- a/graph/test/typescript/expected/83-closed-world-dispatch.edges +++ b/graph/test/typescript/expected/83-closed-world-dispatch.edges @@ -1,7 +1,7 @@ -ambiguous_unknown CONSTRUCTOR_CALL handlers#main() @L52 -> - ambiguous_unknown METHOD_CALL handlers#register(HandlerClass) @L38 -> - known_edge CONSTRUCTOR_CALL handlers#main() @L46 -> Built#() known_edge CONSTRUCTOR_CALL handlers#main() @L47 -> Base#() +known_edge CONSTRUCTOR_CALL handlers#main() @L52 -> handlers#() known_edge FUNCTION_CALL handlers#() @L39 -> handlers#register(HandlerClass) known_edge FUNCTION_CALL handlers#main() @L50 -> handlers#dispatch(Handler,string) known_edge FUNCTION_CALL handlers#main() @L51 -> handlers#dispatch(Handler,string) diff --git a/graph/test/typescript/expected/83-closed-world-dispatch.known-missing b/graph/test/typescript/expected/83-closed-world-dispatch.known-missing deleted file mode 100644 index 15bb2695f..000000000 --- a/graph/test/typescript/expected/83-closed-world-dispatch.known-missing +++ /dev/null @@ -1,7 +0,0 @@ -# Accepted gaps for 83-closed-world-dispatch — 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. -# `new C()` where C is typed by a construct-signature alias: the compiler names the -# alias's signature, the engine leaves the site ambiguous_unknown. Unrelated to the -# closed-world fan this case is about. -handlers#main() -> handlers#() diff --git a/graph/test/typescript/expected/83-closed-world-dispatch.oracle b/graph/test/typescript/expected/83-closed-world-dispatch.oracle index 7f00d858a..35561d9c3 100644 --- a/graph/test/typescript/expected/83-closed-world-dispatch.oracle +++ b/graph/test/typescript/expected/83-closed-world-dispatch.oracle @@ -1,5 +1,4 @@ -oracle=6 engine=12 agree=5 missing=1 (known 1, NEW 0) extra=7 - known handlers#main() -> handlers#() +oracle=6 engine=13 agree=6 missing=0 (known 0, NEW 0) extra=7 extra handlers#dispatch(Handler,string) -> Base#handle(string) extra handlers#dispatch(Handler,string) -> Built#handle(string) extra handlers#dispatch(Handler,string) -> Orphan#handle(string) diff --git a/graph/test/typescript/expected/91-construct-through-shape.edges b/graph/test/typescript/expected/91-construct-through-shape.edges new file mode 100644 index 000000000..d04b2b5ad --- /dev/null +++ b/graph/test/typescript/expected/91-construct-through-shape.edges @@ -0,0 +1,5 @@ +known_edge CONSTRUCTOR_CALL x#viaAlias(WidgetClass) @L8 -> x#(WidgetDef) +known_edge CONSTRUCTOR_CALL x#viaCtorType(new (spec: WidgetDef) =) @L12 -> x#(WidgetDef) +known_edge CONSTRUCTOR_CALL x#viaInterface(ButtonCtor) @L14 -> ButtonCtor#(WidgetDef) +known_edge CONSTRUCTOR_CALL x#viaLiteral({ new (spec: WidgetDef): Button }) @L9 -> x#(WidgetDef) +known_edge FUNCTION_CALL x#() @L15 -> x#viaAlias(WidgetClass) diff --git a/graph/test/typescript/expected/91-construct-through-shape.entries b/graph/test/typescript/expected/91-construct-through-shape.entries new file mode 100644 index 000000000..71704302a --- /dev/null +++ b/graph/test/typescript/expected/91-construct-through-shape.entries @@ -0,0 +1,6 @@ +── entry_point (5) ── + exported_from_entry_module x#viaAlias x.ts:8 + exported_from_entry_module x#viaCtorType x.ts:12 + exported_from_entry_module x#viaInterface x.ts:14 + exported_from_entry_module x#viaLiteral x.ts:9 + unimported_module x# x.ts:1 diff --git a/graph/test/typescript/expected/91-construct-through-shape.fields b/graph/test/typescript/expected/91-construct-through-shape.fields new file mode 100644 index 000000000..e69de29bb diff --git a/graph/test/typescript/expected/91-construct-through-shape.fields-oracle b/graph/test/typescript/expected/91-construct-through-shape.fields-oracle new file mode 100644 index 000000000..2a4ef20c1 --- /dev/null +++ b/graph/test/typescript/expected/91-construct-through-shape.fields-oracle @@ -0,0 +1,7 @@ +91-construct-through-shape [fields] + precision 0.0000 (0 correct, 0 wrong) + recall 0.0000 (0 of 0 the compiler resolved) + sites 0 resolved 0 + tiers + access + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/91-construct-through-shape.oracle b/graph/test/typescript/expected/91-construct-through-shape.oracle new file mode 100644 index 000000000..6c4bb5cbb --- /dev/null +++ b/graph/test/typescript/expected/91-construct-through-shape.oracle @@ -0,0 +1 @@ +oracle=5 engine=5 agree=5 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/91-construct-through-shape.type-use b/graph/test/typescript/expected/91-construct-through-shape.type-use new file mode 100644 index 000000000..746e05578 --- /dev/null +++ b/graph/test/typescript/expected/91-construct-through-shape.type-use @@ -0,0 +1,10 @@ +ambiguous_unknown OBJECT_CREATION_TYPE 0 x [EXPRESSION] -> - +known_edge METHOD_PARAM 0 Button [METHOD_PARAM] -> WidgetDef +known_edge METHOD_PARAM 0 ButtonCtor [METHOD_PARAM] -> WidgetDef +known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> ButtonCtor +known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> WidgetClass +known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> WidgetDef +known_edge METHOD_PARAM 1 x [METHOD_PARAM] -> WidgetDef +known_edge METHOD_RETURN 0 ButtonCtor [METHOD] -> Button +known_edge METHOD_RETURN 0 x [METHOD] -> Button +known_edge METHOD_RETURN 1 x [METHOD_PARAM] -> Button diff --git a/graph/test/typescript/expected/91-construct-through-shape.types-oracle b/graph/test/typescript/expected/91-construct-through-shape.types-oracle new file mode 100644 index 000000000..4be183f12 --- /dev/null +++ b/graph/test/typescript/expected/91-construct-through-shape.types-oracle @@ -0,0 +1,8 @@ +91-construct-through-shape [types] + precision 1.0000 (7 correct, 0 wrong) + recall 0.8750 (7 of 8 the compiler resolved) + sites 17 resolved 13 (76.5%) + tiers ambiguous_unknown=4 known_edge=13 + contexts METHOD_PARAM=7 METHOD_RETURN=6 OBJECT_CREATION_TYPE=4 + not scored: 4 rows whose target is not a client declaration + MISSING WidgetClass USES WidgetDef diff --git a/graph/test/typescript/expected/92-namespace-typed-const.edges b/graph/test/typescript/expected/92-namespace-typed-const.edges new file mode 100644 index 000000000..9205e5e9c --- /dev/null +++ b/graph/test/typescript/expected/92-namespace-typed-const.edges @@ -0,0 +1,5 @@ +known_edge FUNCTION_CALL scorers#() @L7 -> scorers#makeScorer(number) +known_edge FUNCTION_CALL x#viaNamed(string) @L7 -> scorers#(string) +known_edge METHOD_CALL x#viaArrow(string) @L8 -> scorers#scoreArrow(?) +known_edge METHOD_CALL x#viaNamespace(string) @L5 -> scorers#(string) +known_edge METHOD_CALL x#viaPlain(string) @L9 -> scorers#plain(string) diff --git a/graph/test/typescript/expected/92-namespace-typed-const.entries b/graph/test/typescript/expected/92-namespace-typed-const.entries new file mode 100644 index 000000000..637ea1410 --- /dev/null +++ b/graph/test/typescript/expected/92-namespace-typed-const.entries @@ -0,0 +1,6 @@ +── entry_point (5) ── + exported_from_entry_module x#viaArrow x.ts:8 + exported_from_entry_module x#viaNamed x.ts:7 + exported_from_entry_module x#viaNamespace x.ts:5 + exported_from_entry_module x#viaPlain x.ts:9 + unimported_module x# x.ts:1 diff --git a/graph/test/typescript/expected/92-namespace-typed-const.envelope b/graph/test/typescript/expected/92-namespace-typed-const.envelope new file mode 100644 index 000000000..5781aa1ce --- /dev/null +++ b/graph/test/typescript/expected/92-namespace-typed-const.envelope @@ -0,0 +1,4 @@ +value scorers#@1:22 -> scorers#@3 +value scorers#@1:22 -> scorers#@8 +value_call x#viaNamed -> scorers#@3 +value_call x#viaNamespace -> scorers#@3 diff --git a/graph/test/typescript/expected/92-namespace-typed-const.fields b/graph/test/typescript/expected/92-namespace-typed-const.fields new file mode 100644 index 000000000..e9b9013e7 --- /dev/null +++ b/graph/test/typescript/expected/92-namespace-typed-const.fields @@ -0,0 +1,4 @@ +ambiguous_unknown read scorers#fn(?) -> - +ambiguous_unknown read scorers#plain(string) -> - +ambiguous_unknown read scorers#scoreArrow(?) -> - +known_edge read x#viaNamed(string) -> scorers#score diff --git a/graph/test/typescript/expected/92-namespace-typed-const.fields-oracle b/graph/test/typescript/expected/92-namespace-typed-const.fields-oracle new file mode 100644 index 000000000..da0990d4c --- /dev/null +++ b/graph/test/typescript/expected/92-namespace-typed-const.fields-oracle @@ -0,0 +1,7 @@ +92-namespace-typed-const [fields] + precision 0.0000 (0 correct, 0 wrong) + recall 0.0000 (0 of 0 the compiler resolved) + sites 4 resolved 1 (25.0%) + tiers ambiguous_unknown=3 known_edge=1 + access read=4 + not scored: 4 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/92-namespace-typed-const.known-missing b/graph/test/typescript/expected/92-namespace-typed-const.known-missing new file mode 100644 index 000000000..9dacefbf2 --- /dev/null +++ b/graph/test/typescript/expected/92-namespace-typed-const.known-missing @@ -0,0 +1,7 @@ +# Accepted gaps for 92-namespace-typed-const: 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. +# A const that HOLDS an arrow: the compiler names the annotation's signature, the engine names +# the arrow's body, as it does for the same const read by bare name. Kept on purpose: adding the +# bodiless signature beside the body would hedge a site whose implementation is known. +x#viaArrow(string) -> scorers#(string) diff --git a/graph/test/typescript/expected/92-namespace-typed-const.oracle b/graph/test/typescript/expected/92-namespace-typed-const.oracle new file mode 100644 index 000000000..aaf82eab3 --- /dev/null +++ b/graph/test/typescript/expected/92-namespace-typed-const.oracle @@ -0,0 +1,3 @@ +oracle=5 engine=5 agree=4 missing=1 (known 1, NEW 0) extra=1 + known x#viaArrow(string) -> scorers#(string) + extra x#viaArrow(string) -> scorers#scoreArrow(?) diff --git a/graph/test/typescript/expected/92-namespace-typed-const.type-use b/graph/test/typescript/expected/92-namespace-typed-const.type-use new file mode 100644 index 000000000..fca5ea869 --- /dev/null +++ b/graph/test/typescript/expected/92-namespace-typed-const.type-use @@ -0,0 +1,2 @@ +known_edge METHOD_RETURN 0 scorers [METHOD] -> Scorer +known_edge VARIABLE_TYPE 0 scorers [VARIABLE] -> Scorer diff --git a/graph/test/typescript/expected/92-namespace-typed-const.types-oracle b/graph/test/typescript/expected/92-namespace-typed-const.types-oracle new file mode 100644 index 000000000..1318ffdaa --- /dev/null +++ b/graph/test/typescript/expected/92-namespace-typed-const.types-oracle @@ -0,0 +1,7 @@ +92-namespace-typed-const [types] + precision 1.0000 (1 correct, 0 wrong) + recall 1.0000 (1 of 1 the compiler resolved) + sites 4 resolved 4 (100.0%) + tiers known_edge=4 + contexts METHOD_RETURN=1 VARIABLE_TYPE=3 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/93-literal-member-chain.edges b/graph/test/typescript/expected/93-literal-member-chain.edges new file mode 100644 index 000000000..9e9c71b98 --- /dev/null +++ b/graph/test/typescript/expected/93-literal-member-chain.edges @@ -0,0 +1,4 @@ +known_edge METHOD_CALL x#flat(Flat) @L15 -> Logger#write(string) +known_edge METHOD_CALL x#named(Named) @L16 -> Logger#write(string) +known_edge METHOD_CALL x#record(Order) @L8 -> Logger#write(string) +known_edge METHOD_CALL x#record(Order) @L9 -> Logger#write(string) diff --git a/graph/test/typescript/expected/93-literal-member-chain.entries b/graph/test/typescript/expected/93-literal-member-chain.entries new file mode 100644 index 000000000..ffd59efd0 --- /dev/null +++ b/graph/test/typescript/expected/93-literal-member-chain.entries @@ -0,0 +1,5 @@ +── entry_point (4) ── + exported_from_entry_module x#flat x.ts:15 + exported_from_entry_module x#named x.ts:16 + exported_from_entry_module x#record x.ts:7 + unimported_module x# x.ts:1 diff --git a/graph/test/typescript/expected/93-literal-member-chain.fields b/graph/test/typescript/expected/93-literal-member-chain.fields new file mode 100644 index 000000000..a14893e8a --- /dev/null +++ b/graph/test/typescript/expected/93-literal-member-chain.fields @@ -0,0 +1,10 @@ +known_edge read x#flat(Flat) -> Flat#meta +known_edge read x#flat(Flat) -> { log: Logger }#log +known_edge read x#named(Named) -> Meta#log +known_edge read x#named(Named) -> Named#meta +known_edge read x#record(Order) -> Order#meta +known_edge read x#record(Order) -> { audit: { log: Logger; nested: { deep: { log: Logger } } } }#audit +known_edge read x#record(Order) -> { deep: { log: Logger } }#deep +known_edge read x#record(Order) -> { log: Logger }#log +known_edge read x#record(Order) -> { log: Logger; nested: { deep: { log: Logger } } }#log +known_edge read x#record(Order) -> { log: Logger; nested: { deep: { log: Logger } } }#nested diff --git a/graph/test/typescript/expected/93-literal-member-chain.fields-oracle b/graph/test/typescript/expected/93-literal-member-chain.fields-oracle new file mode 100644 index 000000000..27fc7aac8 --- /dev/null +++ b/graph/test/typescript/expected/93-literal-member-chain.fields-oracle @@ -0,0 +1,18 @@ +93-literal-member-chain [fields] + precision 0.4000 (4 correct, 6 wrong) + recall 0.4444 (4 of 9 the compiler resolved) + sites 12 resolved 12 (100.0%) + tiers known_edge=12 + access read=12 + not scored: 0 rows whose target is not a client declaration + WRONG x#flat(Flat) READ { log: Logger }#log + WRONG x#record(Order) READ { audit: { log: Logger; nested: { deep: { log: Logger } } } }#audit + WRONG x#record(Order) READ { deep: { log: Logger } }#deep + WRONG x#record(Order) READ { log: Logger }#log + WRONG x#record(Order) READ { log: Logger; nested: { deep: { log: Logger } } }#log + WRONG x#record(Order) READ { log: Logger; nested: { deep: { log: Logger } } }#nested + MISSING x#flat(Flat) READ Flat#log + MISSING x#record(Order) READ Order#audit + MISSING x#record(Order) READ Order#deep + MISSING x#record(Order) READ Order#log + MISSING x#record(Order) READ Order#nested diff --git a/graph/test/typescript/expected/93-literal-member-chain.oracle b/graph/test/typescript/expected/93-literal-member-chain.oracle new file mode 100644 index 000000000..a43ea4f88 --- /dev/null +++ b/graph/test/typescript/expected/93-literal-member-chain.oracle @@ -0,0 +1 @@ +oracle=3 engine=3 agree=3 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/93-literal-member-chain.type-use b/graph/test/typescript/expected/93-literal-member-chain.type-use new file mode 100644 index 000000000..cecb1bce8 --- /dev/null +++ b/graph/test/typescript/expected/93-literal-member-chain.type-use @@ -0,0 +1,8 @@ +known_edge FIELD_TYPE 0 Meta [FIELD] -> Logger +known_edge FIELD_TYPE 0 Named [FIELD] -> Meta +known_edge FIELD_TYPE 1 Flat [FIELD] -> Logger +known_edge FIELD_TYPE 2 Order [FIELD] -> Logger +known_edge FIELD_TYPE 4 Order [FIELD] -> Logger +known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> Flat +known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> Named +known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> Order diff --git a/graph/test/typescript/expected/93-literal-member-chain.types-oracle b/graph/test/typescript/expected/93-literal-member-chain.types-oracle new file mode 100644 index 000000000..53acc6dcd --- /dev/null +++ b/graph/test/typescript/expected/93-literal-member-chain.types-oracle @@ -0,0 +1,7 @@ +93-literal-member-chain [types] + precision 1.0000 (7 correct, 0 wrong) + recall 1.0000 (7 of 7 the compiler resolved) + sites 8 resolved 8 (100.0%) + tiers known_edge=8 + contexts FIELD_TYPE=5 METHOD_PARAM=3 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/94-thistype-object-literal.edges b/graph/test/typescript/expected/94-thistype-object-literal.edges new file mode 100644 index 000000000..ebda1c8aa --- /dev/null +++ b/graph/test/typescript/expected/94-thistype-object-literal.edges @@ -0,0 +1,8 @@ +ambient_terminal FUNCTION_CALL x#() @L10 -> x#mixin(string,Members) +ambient_terminal FUNCTION_CALL x#() @L29 -> x#mixin(string,Members) +ambiguous_unknown METHOD_CALL x#b() @L25 -> - +known_edge METHOD_CALL Host#() @L29 -> Host#run() +known_edge METHOD_CALL x#greet() @L11 -> Greeter#shout() +known_edge METHOD_CALL x#hail() @L15 -> Greeter#greet() +known_edge METHOD_CALL x#shout() @L12 -> Greeter#greet() +known_edge METHOD_CALL x#wave() @L19 -> Greeter#shout() diff --git a/graph/test/typescript/expected/94-thistype-object-literal.entries b/graph/test/typescript/expected/94-thistype-object-literal.entries new file mode 100644 index 000000000..6f5cfa1ff --- /dev/null +++ b/graph/test/typescript/expected/94-thistype-object-literal.entries @@ -0,0 +1,2 @@ +── entry_point (1) ── + unimported_module x# x.ts:1 diff --git a/graph/test/typescript/expected/94-thistype-object-literal.envelope b/graph/test/typescript/expected/94-thistype-object-literal.envelope new file mode 100644 index 000000000..6ef842dad --- /dev/null +++ b/graph/test/typescript/expected/94-thistype-object-literal.envelope @@ -0,0 +1,2 @@ +value x#Greeter.hail -> x#hail +value x#Waver.wave -> x#wave diff --git a/graph/test/typescript/expected/94-thistype-object-literal.fields b/graph/test/typescript/expected/94-thistype-object-literal.fields new file mode 100644 index 000000000..e69de29bb diff --git a/graph/test/typescript/expected/94-thistype-object-literal.fields-oracle b/graph/test/typescript/expected/94-thistype-object-literal.fields-oracle new file mode 100644 index 000000000..36c4e6506 --- /dev/null +++ b/graph/test/typescript/expected/94-thistype-object-literal.fields-oracle @@ -0,0 +1,7 @@ +94-thistype-object-literal [fields] + precision 0.0000 (0 correct, 0 wrong) + recall 0.0000 (0 of 0 the compiler resolved) + sites 0 resolved 0 + tiers + access + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/94-thistype-object-literal.known-missing b/graph/test/typescript/expected/94-thistype-object-literal.known-missing new file mode 100644 index 000000000..c34d8fcf5 --- /dev/null +++ b/graph/test/typescript/expected/94-thistype-object-literal.known-missing @@ -0,0 +1,6 @@ +# Accepted gaps for 94-thistype-object-literal: 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 no-marker control: with no contextual type, `this` in a literal's method is the literal +# itself. The engine does not type it, before this change and after; the marker cases are the fix. +x#b() -> x#a() diff --git a/graph/test/typescript/expected/94-thistype-object-literal.oracle b/graph/test/typescript/expected/94-thistype-object-literal.oracle new file mode 100644 index 000000000..5c112c0a4 --- /dev/null +++ b/graph/test/typescript/expected/94-thistype-object-literal.oracle @@ -0,0 +1,2 @@ +oracle=7 engine=6 agree=6 missing=1 (known 1, NEW 0) extra=0 + known x#b() -> x#a() diff --git a/graph/test/typescript/expected/94-thistype-object-literal.type-use b/graph/test/typescript/expected/94-thistype-object-literal.type-use new file mode 100644 index 000000000..9d88dab9d --- /dev/null +++ b/graph/test/typescript/expected/94-thistype-object-literal.type-use @@ -0,0 +1,10 @@ +ambiguous_unknown TYPE_ELEMENT 1 Members [TYPE] -> - +ambiguous_unknown TYPE_ELEMENT 1 x [VARIABLE] -> - +known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> Members +known_edge METHOD_RETURN 0 x [METHOD] -> Mixin +known_edge METHOD_TYPE_ARGUMENT 0 Host [EXPRESSION] -> Greeter +known_edge METHOD_TYPE_ARGUMENT 0 x [EXPRESSION] -> Greeter +known_edge TYPE_ARGUMENT 1 x [VARIABLE] -> Greeter +known_edge TYPE_ARGUMENT 2 x [VARIABLE] -> Greeter +known_edge TYPE_ELEMENT 1 x [VARIABLE] -> Waver +known_edge VARIABLE_TYPE 0 x [VARIABLE] -> Members diff --git a/graph/test/typescript/expected/94-thistype-object-literal.types-oracle b/graph/test/typescript/expected/94-thistype-object-literal.types-oracle new file mode 100644 index 000000000..cd697dd3d --- /dev/null +++ b/graph/test/typescript/expected/94-thistype-object-literal.types-oracle @@ -0,0 +1,7 @@ +94-thistype-object-literal [types] + precision 1.0000 (5 correct, 0 wrong) + recall 1.0000 (5 of 5 the compiler resolved) + sites 10 resolved 8 (80.0%) + tiers ambiguous_unknown=2 known_edge=8 + contexts METHOD_PARAM=1 METHOD_RETURN=1 METHOD_TYPE_ARGUMENT=2 TYPE_ARGUMENT=2 TYPE_ELEMENT=3 VARIABLE_TYPE=1 + not scored: 2 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/95-bind-on-method-value.edges b/graph/test/typescript/expected/95-bind-on-method-value.edges new file mode 100644 index 000000000..44e5302ec --- /dev/null +++ b/graph/test/typescript/expected/95-bind-on-method-value.edges @@ -0,0 +1,9 @@ +known_edge FUNCTION_CALL View#wire() @L14 -> View#render() +known_edge METHOD_CALL View#wire() @L10 -> CallableFunction#call(any,any,any) +known_edge METHOD_CALL View#wire() @L11 -> CallableFunction#apply(any,any) +known_edge METHOD_CALL View#wire() @L13 -> CallableFunction#bind(any,any) +known_edge METHOD_CALL View#wire() @L16 -> CallableFunction#bind(any,any) +known_edge METHOD_CALL View#wire() @L17 -> CallableFunction#bind(any,any) +known_edge METHOD_CALL View#wire() @L9 -> CallableFunction#bind(any,any) +known_edge METHOD_CALL x#runOn(View) @L20 -> CallableFunction#call(any,any,any) +known_edge PROPERTY_READ View#wire() @L17 -> View#label() diff --git a/graph/test/typescript/expected/95-bind-on-method-value.entries b/graph/test/typescript/expected/95-bind-on-method-value.entries new file mode 100644 index 000000000..6997e79ce --- /dev/null +++ b/graph/test/typescript/expected/95-bind-on-method-value.entries @@ -0,0 +1,4 @@ +── entry_point (3) ── + exported_from_entry_module x#runOn x.ts:20 + unimported_module globals# globals.d.ts:1 + unimported_module x# x.ts:1 diff --git a/graph/test/typescript/expected/95-bind-on-method-value.envelope b/graph/test/typescript/expected/95-bind-on-method-value.envelope new file mode 100644 index 000000000..46f9e8590 --- /dev/null +++ b/graph/test/typescript/expected/95-bind-on-method-value.envelope @@ -0,0 +1,5 @@ +nominal globals#Function.apply -> globals#CallableFunction.apply +nominal globals#Function.bind -> globals#CallableFunction.bind +nominal globals#Function.call -> globals#CallableFunction.call +value x#@6:16 -> x#View.@6 +value x#@7:12 -> x#View.@7 diff --git a/graph/test/typescript/expected/95-bind-on-method-value.fields b/graph/test/typescript/expected/95-bind-on-method-value.fields new file mode 100644 index 000000000..88df97815 --- /dev/null +++ b/graph/test/typescript/expected/95-bind-on-method-value.fields @@ -0,0 +1 @@ +known_edge read View#wire() -> View#handler diff --git a/graph/test/typescript/expected/95-bind-on-method-value.fields-oracle b/graph/test/typescript/expected/95-bind-on-method-value.fields-oracle new file mode 100644 index 000000000..72328250a --- /dev/null +++ b/graph/test/typescript/expected/95-bind-on-method-value.fields-oracle @@ -0,0 +1,7 @@ +95-bind-on-method-value [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/95-bind-on-method-value.oracle b/graph/test/typescript/expected/95-bind-on-method-value.oracle new file mode 100644 index 000000000..131c80216 --- /dev/null +++ b/graph/test/typescript/expected/95-bind-on-method-value.oracle @@ -0,0 +1,2 @@ +oracle=5 engine=6 agree=5 missing=0 (known 0, NEW 0) extra=1 + extra View#wire() -> View#render() diff --git a/graph/test/typescript/expected/95-bind-on-method-value.type-use b/graph/test/typescript/expected/95-bind-on-method-value.type-use new file mode 100644 index 000000000..e6dc01bae --- /dev/null +++ b/graph/test/typescript/expected/95-bind-on-method-value.type-use @@ -0,0 +1,3 @@ +known_edge METHOD_PARAM 0 x [METHOD_PARAM] -> View +known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function +known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function diff --git a/graph/test/typescript/expected/95-bind-on-method-value.types-oracle b/graph/test/typescript/expected/95-bind-on-method-value.types-oracle new file mode 100644 index 000000000..3cce60dfc --- /dev/null +++ b/graph/test/typescript/expected/95-bind-on-method-value.types-oracle @@ -0,0 +1,7 @@ +95-bind-on-method-value [types] + 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 + contexts METHOD_PARAM=1 SUPER_TYPE=2 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/96-arithmetic-result-receiver.edges b/graph/test/typescript/expected/96-arithmetic-result-receiver.edges new file mode 100644 index 000000000..84ba005a3 --- /dev/null +++ b/graph/test/typescript/expected/96-arithmetic-result-receiver.edges @@ -0,0 +1,12 @@ +known_edge METHOD_CALL arith#bigDiff(bigint,bigint) @L28 -> BigInt#toString(number) +known_edge METHOD_CALL arith#bigNeg(bigint) @L31 -> BigInt#toString(number) +known_edge METHOD_CALL arith#elapsed(number,number) @L4 -> Number#toFixed(number) +known_edge METHOD_CALL arith#joined(string,number) @L37 -> String#toString() +known_edge METHOD_CALL arith#joinedTrim(string,number) @L40 -> String#trim() +known_edge METHOD_CALL arith#masked(number) @L16 -> Number#toString(number) +known_edge METHOD_CALL arith#negated(number) @L19 -> Number#toFixed(number) +known_edge METHOD_CALL arith#parsed(string) @L22 -> Number#toFixed(number) +known_edge METHOD_CALL arith#power(number) @L13 -> Number#toFixed(number) +known_edge METHOD_CALL arith#ratio(number,number) @L7 -> Number#toFixed(number) +known_edge METHOD_CALL arith#scaled(number) @L10 -> Number#toString(number) +known_edge METHOD_CALL arith#total(number,number) @L25 -> Number#toFixed(number) diff --git a/graph/test/typescript/expected/96-arithmetic-result-receiver.entries b/graph/test/typescript/expected/96-arithmetic-result-receiver.entries new file mode 100644 index 000000000..ab7f81f10 --- /dev/null +++ b/graph/test/typescript/expected/96-arithmetic-result-receiver.entries @@ -0,0 +1,15 @@ +── entry_point (14) ── + exported_from_entry_module arith#bigDiff arith.ts:27 + exported_from_entry_module arith#bigNeg arith.ts:30 + exported_from_entry_module arith#elapsed arith.ts:3 + exported_from_entry_module arith#joined arith.ts:36 + exported_from_entry_module arith#joinedTrim arith.ts:39 + exported_from_entry_module arith#masked arith.ts:15 + exported_from_entry_module arith#negated arith.ts:18 + exported_from_entry_module arith#parsed arith.ts:21 + exported_from_entry_module arith#power arith.ts:12 + exported_from_entry_module arith#ratio arith.ts:6 + exported_from_entry_module arith#scaled arith.ts:9 + exported_from_entry_module arith#total arith.ts:24 + unimported_module arith# arith.ts:1 + unimported_module globals# globals.d.ts:1 diff --git a/graph/test/typescript/expected/96-arithmetic-result-receiver.fields b/graph/test/typescript/expected/96-arithmetic-result-receiver.fields new file mode 100644 index 000000000..e69de29bb diff --git a/graph/test/typescript/expected/96-arithmetic-result-receiver.fields-oracle b/graph/test/typescript/expected/96-arithmetic-result-receiver.fields-oracle new file mode 100644 index 000000000..18021fe45 --- /dev/null +++ b/graph/test/typescript/expected/96-arithmetic-result-receiver.fields-oracle @@ -0,0 +1,7 @@ +96-arithmetic-result-receiver [fields] + precision 0.0000 (0 correct, 0 wrong) + recall 0.0000 (0 of 0 the compiler resolved) + sites 0 resolved 0 + tiers + access + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/96-arithmetic-result-receiver.oracle b/graph/test/typescript/expected/96-arithmetic-result-receiver.oracle new file mode 100644 index 000000000..8dfb8bec2 --- /dev/null +++ b/graph/test/typescript/expected/96-arithmetic-result-receiver.oracle @@ -0,0 +1 @@ +oracle=12 engine=12 agree=12 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/96-arithmetic-result-receiver.type-use b/graph/test/typescript/expected/96-arithmetic-result-receiver.type-use new file mode 100644 index 000000000..07e338d22 --- /dev/null +++ b/graph/test/typescript/expected/96-arithmetic-result-receiver.type-use @@ -0,0 +1,3 @@ +known_edge METHOD_RETURN 0 Object [METHOD] -> Object +known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function +known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function diff --git a/graph/test/typescript/expected/96-arithmetic-result-receiver.types-oracle b/graph/test/typescript/expected/96-arithmetic-result-receiver.types-oracle new file mode 100644 index 000000000..0f875e309 --- /dev/null +++ b/graph/test/typescript/expected/96-arithmetic-result-receiver.types-oracle @@ -0,0 +1,7 @@ +96-arithmetic-result-receiver [types] + 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 + contexts METHOD_RETURN=1 SUPER_TYPE=2 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/97-nullish-default-receiver.edges b/graph/test/typescript/expected/97-nullish-default-receiver.edges new file mode 100644 index 000000000..5cba14cc0 --- /dev/null +++ b/graph/test/typescript/expected/97-nullish-default-receiver.edges @@ -0,0 +1,13 @@ +ambiguous_unknown METHOD_CALL Bus#untyped(any,Listener) @L35 -> - +known_edge CONSTRUCTOR_CALL bus#() @L7 -> MapConstructor#() +known_edge CONSTRUCTOR_CALL bus#() @L8 -> MapConstructor#() +known_edge METHOD_CALL Bus#annotated(string,Listener) @L30 -> Map#get(T) +known_edge METHOD_CALL Bus#annotated(string,Listener) @L31 -> Array#push(T[]) +known_edge METHOD_CALL Bus#direct(string,Listener) @L16 -> Array#push(T[]) +known_edge METHOD_CALL Bus#direct(string,Listener) @L16 -> Map#get(T) +known_edge METHOD_CALL Bus#label(string) @L19 -> Map#get(T) +known_edge METHOD_CALL Bus#label(string) @L19 -> String#trim() +known_edge METHOD_CALL Bus#nonNullable(Listener[],Listener) @L24 -> Array#push(T[]) +known_edge METHOD_CALL Bus#on(string,Listener) @L11 -> Map#get(T) +known_edge METHOD_CALL Bus#on(string,Listener) @L12 -> Array#push(T[]) +known_edge METHOD_CALL Bus#on(string,Listener) @L13 -> Map#set(T,T) diff --git a/graph/test/typescript/expected/97-nullish-default-receiver.entries b/graph/test/typescript/expected/97-nullish-default-receiver.entries new file mode 100644 index 000000000..8fa4a6c6d --- /dev/null +++ b/graph/test/typescript/expected/97-nullish-default-receiver.entries @@ -0,0 +1,3 @@ +── entry_point (2) ── + unimported_module bus# bus.ts:1 + unimported_module globals# globals.d.ts:1 diff --git a/graph/test/typescript/expected/97-nullish-default-receiver.fields b/graph/test/typescript/expected/97-nullish-default-receiver.fields new file mode 100644 index 000000000..8b0eabc4f --- /dev/null +++ b/graph/test/typescript/expected/97-nullish-default-receiver.fields @@ -0,0 +1,4 @@ +known_edge read Bus#annotated(string,Listener) -> Bus#listeners +known_edge read Bus#direct(string,Listener) -> Bus#listeners +known_edge read Bus#label(string) -> Bus#labels +known_edge read Bus#on(string,Listener) -> Bus#listeners diff --git a/graph/test/typescript/expected/97-nullish-default-receiver.fields-oracle b/graph/test/typescript/expected/97-nullish-default-receiver.fields-oracle new file mode 100644 index 000000000..ff08c2b5b --- /dev/null +++ b/graph/test/typescript/expected/97-nullish-default-receiver.fields-oracle @@ -0,0 +1,7 @@ +97-nullish-default-receiver [fields] + precision 1.0000 (4 correct, 0 wrong) + recall 1.0000 (4 of 4 the compiler resolved) + sites 5 resolved 5 (100.0%) + tiers known_edge=5 + access read=5 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/97-nullish-default-receiver.oracle b/graph/test/typescript/expected/97-nullish-default-receiver.oracle new file mode 100644 index 000000000..373cdb593 --- /dev/null +++ b/graph/test/typescript/expected/97-nullish-default-receiver.oracle @@ -0,0 +1 @@ +oracle=11 engine=11 agree=11 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/97-nullish-default-receiver.type-use b/graph/test/typescript/expected/97-nullish-default-receiver.type-use new file mode 100644 index 000000000..60abe7649 --- /dev/null +++ b/graph/test/typescript/expected/97-nullish-default-receiver.type-use @@ -0,0 +1,9 @@ +known_edge METHOD_PARAM 0 Bus [METHOD_PARAM] -> Listener +known_edge METHOD_RETURN 0 MapConstructor [METHOD] -> Map +known_edge OBJECT_CREATION_TYPE 0 Bus [EXPRESSION] -> Map +known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function +known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function +known_edge TYPE_ELEMENT 1 Bus [EXPRESSION] -> Listener +known_edge TYPE_ELEMENT 1 Bus [METHOD_PARAM] -> Listener +known_edge TYPE_ELEMENT 1 Bus [VARIABLE] -> Listener +known_edge VARIABLE_TYPE 0 globals [VARIABLE] -> MapConstructor diff --git a/graph/test/typescript/expected/97-nullish-default-receiver.types-oracle b/graph/test/typescript/expected/97-nullish-default-receiver.types-oracle new file mode 100644 index 000000000..a1ed2e12d --- /dev/null +++ b/graph/test/typescript/expected/97-nullish-default-receiver.types-oracle @@ -0,0 +1,9 @@ +97-nullish-default-receiver [types] + precision 0.8333 (5 correct, 1 wrong) + recall 0.8333 (5 of 6 the compiler resolved) + sites 14 resolved 14 (100.0%) + tiers known_edge=14 + contexts METHOD_PARAM=5 METHOD_RETURN=1 OBJECT_CREATION_TYPE=2 SUPER_TYPE=2 TYPE_ELEMENT=3 VARIABLE_TYPE=1 + not scored: 0 rows whose target is not a client declaration + WRONG globals USES MapConstructor + MISSING globals.d USES MapConstructor diff --git a/graph/test/typescript/expected/98-namespace-const-callable.edges b/graph/test/typescript/expected/98-namespace-const-callable.edges new file mode 100644 index 000000000..c29154ff4 --- /dev/null +++ b/graph/test/typescript/expected/98-namespace-const-callable.edges @@ -0,0 +1,12 @@ +ambient_terminal FUNCTION_CALL use#() @L17 -> use#makeRunner() +ambiguous_unknown METHOD_CALL use#controls() @L30 -> - +ambiguous_unknown METHOD_CALL use#suite() @L7 -> - +ambiguous_unknown METHOD_CALL use#suite() @L8 -> - +ambiguous_unknown METHOD_CALL use#suite() @L9 -> - +callback_registered METHOD_CALL use#suite() @L7 -> use#() +callback_registered METHOD_CALL use#suite() @L8 -> use#() +known_edge METHOD_CALL use#controls() @L31 -> Local#helper() +known_edge METHOD_CALL use#controls() @L32 -> Local#arrow(?) +known_edge METHOD_CALL use#local() @L22 -> Runner#(number) +known_edge METHOD_CALL use#local() @L23 -> Runner#describe() +known_edge METHOD_CALL use#shadowed({ test(name: string): void }) @L35 -> use#test(string) diff --git a/graph/test/typescript/expected/98-namespace-const-callable.entries b/graph/test/typescript/expected/98-namespace-const-callable.entries new file mode 100644 index 000000000..e4446c5aa --- /dev/null +++ b/graph/test/typescript/expected/98-namespace-const-callable.entries @@ -0,0 +1,8 @@ +── entry_point (7) ── + exported_from_entry_module use#controls use.ts:29 + exported_from_entry_module use#local use.ts:21 + exported_from_entry_module use#shadowed use.ts:34 + exported_from_entry_module use#suite use.ts:6 + test use# use.ts:7 + unimported_module globals# globals.d.ts:1 + unimported_module use# use.ts:1 diff --git a/graph/test/typescript/expected/98-namespace-const-callable.envelope b/graph/test/typescript/expected/98-namespace-const-callable.envelope new file mode 100644 index 000000000..61c29593d --- /dev/null +++ b/graph/test/typescript/expected/98-namespace-const-callable.envelope @@ -0,0 +1 @@ +value use#Step. -> use#Local. diff --git a/graph/test/typescript/expected/98-namespace-const-callable.fields b/graph/test/typescript/expected/98-namespace-const-callable.fields new file mode 100644 index 000000000..7f7ac1a4f --- /dev/null +++ b/graph/test/typescript/expected/98-namespace-const-callable.fields @@ -0,0 +1,2 @@ +ambiguous_unknown read use#local() -> - +ambiguous_unknown read use#suite() -> - diff --git a/graph/test/typescript/expected/98-namespace-const-callable.fields-oracle b/graph/test/typescript/expected/98-namespace-const-callable.fields-oracle new file mode 100644 index 000000000..db1ae7a2a --- /dev/null +++ b/graph/test/typescript/expected/98-namespace-const-callable.fields-oracle @@ -0,0 +1,7 @@ +98-namespace-const-callable [fields] + precision 0.0000 (0 correct, 0 wrong) + recall 0.0000 (0 of 0 the compiler resolved) + sites 3 resolved 0 (0.0%) + tiers ambiguous_unknown=3 + access read=3 + not scored: 3 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/98-namespace-const-callable.known-missing b/graph/test/typescript/expected/98-namespace-const-callable.known-missing new file mode 100644 index 000000000..9a6bcdf71 --- /dev/null +++ b/graph/test/typescript/expected/98-namespace-const-callable.known-missing @@ -0,0 +1,7 @@ +# Accepted gaps for 98-namespace-const-callable: 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 control const that HOLDS an arrow: the compiler names the annotation's signature, the +# engine names the arrow's body, the same accepted shape as 92-namespace-typed-const. Kept on +# purpose: the namespace-const rule must not add the bodiless signature beside the body. +use#controls() -> Step#(number) diff --git a/graph/test/typescript/expected/98-namespace-const-callable.lib.edges b/graph/test/typescript/expected/98-namespace-const-callable.lib.edges new file mode 100644 index 000000000..c3df497ab --- /dev/null +++ b/graph/test/typescript/expected/98-namespace-const-callable.lib.edges @@ -0,0 +1,12 @@ +ambient_terminal FUNCTION_CALL use#() @L17 -> use#makeRunner() +boundary_lib METHOD_CALL use#controls() @L30 -> Rt#serve(number) +boundary_lib METHOD_CALL use#suite() @L7 -> TestFn#(string,() =) +boundary_lib METHOD_CALL use#suite() @L8 -> TestFn#only(string,() =) +boundary_lib METHOD_CALL use#suite() @L9 -> Env#get(string) +callback_registered METHOD_CALL use#suite() @L7 -> use#() +callback_registered METHOD_CALL use#suite() @L8 -> use#() +known_edge METHOD_CALL use#controls() @L31 -> Local#helper() +known_edge METHOD_CALL use#controls() @L32 -> Local#arrow(?) +known_edge METHOD_CALL use#local() @L22 -> Runner#(number) +known_edge METHOD_CALL use#local() @L23 -> Runner#describe() +known_edge METHOD_CALL use#shadowed({ test(name: string): void }) @L35 -> use#test(string) diff --git a/graph/test/typescript/expected/98-namespace-const-callable.lib.envelope b/graph/test/typescript/expected/98-namespace-const-callable.lib.envelope new file mode 100644 index 000000000..61c29593d --- /dev/null +++ b/graph/test/typescript/expected/98-namespace-const-callable.lib.envelope @@ -0,0 +1 @@ +value use#Step. -> use#Local. diff --git a/graph/test/typescript/expected/98-namespace-const-callable.lib.known-missing b/graph/test/typescript/expected/98-namespace-const-callable.lib.known-missing new file mode 100644 index 000000000..9a6bcdf71 --- /dev/null +++ b/graph/test/typescript/expected/98-namespace-const-callable.lib.known-missing @@ -0,0 +1,7 @@ +# Accepted gaps for 98-namespace-const-callable: 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 control const that HOLDS an arrow: the compiler names the annotation's signature, the +# engine names the arrow's body, the same accepted shape as 92-namespace-typed-const. Kept on +# purpose: the namespace-const rule must not add the bodiless signature beside the body. +use#controls() -> Step#(number) diff --git a/graph/test/typescript/expected/98-namespace-const-callable.lib.oracle b/graph/test/typescript/expected/98-namespace-const-callable.lib.oracle new file mode 100644 index 000000000..c256eb244 --- /dev/null +++ b/graph/test/typescript/expected/98-namespace-const-callable.lib.oracle @@ -0,0 +1,3 @@ +oracle=10 engine=10 agree=9 missing=1 (known 1, NEW 0) extra=1 + known use#controls() -> Step#(number) + extra use#controls() -> Local#arrow(?) diff --git a/graph/test/typescript/expected/98-namespace-const-callable.oracle b/graph/test/typescript/expected/98-namespace-const-callable.oracle new file mode 100644 index 000000000..a5054e035 --- /dev/null +++ b/graph/test/typescript/expected/98-namespace-const-callable.oracle @@ -0,0 +1,3 @@ +oracle=6 engine=6 agree=5 missing=1 (known 1, NEW 0) extra=1 + known use#controls() -> Step#(number) + extra use#controls() -> Local#arrow(?) diff --git a/graph/test/typescript/expected/98-namespace-const-callable.type-use b/graph/test/typescript/expected/98-namespace-const-callable.type-use new file mode 100644 index 000000000..680d17cce --- /dev/null +++ b/graph/test/typescript/expected/98-namespace-const-callable.type-use @@ -0,0 +1,5 @@ +known_edge METHOD_RETURN 0 use [METHOD] -> Runner +known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function +known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function +known_edge VARIABLE_TYPE 0 Local [VARIABLE] -> Runner +known_edge VARIABLE_TYPE 0 Local [VARIABLE] -> Step diff --git a/graph/test/typescript/expected/98-namespace-const-callable.types-oracle b/graph/test/typescript/expected/98-namespace-const-callable.types-oracle new file mode 100644 index 000000000..b5b65c7d9 --- /dev/null +++ b/graph/test/typescript/expected/98-namespace-const-callable.types-oracle @@ -0,0 +1,10 @@ +98-namespace-const-callable [types] + precision 0.6000 (3 correct, 2 wrong) + recall 0.7500 (3 of 4 the compiler resolved) + sites 5 resolved 5 (100.0%) + tiers known_edge=5 + contexts METHOD_RETURN=1 SUPER_TYPE=2 VARIABLE_TYPE=2 + not scored: 0 rows whose target is not a client declaration + WRONG Local USES Runner + WRONG Local USES Step + MISSING use USES Step 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..63c4b74cb --- /dev/null +++ b/graph/test/typescript/expected/99-contextual-destructured-param.edges @@ -0,0 +1,9 @@ +ambiguous_unknown METHOD_CALL use#untyped(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) @L23 -> Utils#send(string) +known_edge METHOD_CALL use#otherConst(?) @L34 -> Other#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.oracle b/graph/test/typescript/expected/99-contextual-destructured-param.oracle new file mode 100644 index 000000000..fa4000b6a --- /dev/null +++ b/graph/test/typescript/expected/99-contextual-destructured-param.oracle @@ -0,0 +1 @@ +oracle=7 engine=7 agree=7 missing=0 (known 0, NEW 0) extra=0 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/test/typescript/tools/normalize_edges.py b/graph/test/typescript/tools/normalize_edges.py index 9807d7574..ad8513cbd 100644 --- a/graph/test/typescript/tools/normalize_edges.py +++ b/graph/test/typescript/tools/normalize_edges.py @@ -198,7 +198,15 @@ def enclosing_owner(type_hash): for r in rows(f'{ir}/all-typescript-methods.csv'): h = r['tsMethodUniqueHash'] ps = [] - for p in sorted(params.get(h, []), key=lambda x: int(x.get('position') or 0)): + # A destructured parameter has a row for the pattern and one per name it binds, all at the pattern's + # position (bindingSourceKind PROPERTY/INDEX): one parameter, labelled once, as the compiler prints it. + by_pos = {} + for p in params.get(h, []): + pos = int(p.get('position') or 0) + bound = (p.get('bindingSourceKind') or 'NONE') != 'NONE' + if pos not in by_pos or (by_pos[pos][0] and not bound): + by_pos[pos] = (bound, p) + for _, p in sorted((pos, bp[1]) for pos, bp in by_pos.items()): if p.get('isReceiverParameter') == 'true': continue t = simple(p.get('parameterTypeName') or p.get('parameterBaseType')) diff --git a/graph/typescript/engine/expression-resolution/callee-resolution.dl b/graph/typescript/engine/expression-resolution/callee-resolution.dl index c42570448..d05fc58a1 100644 --- a/graph/typescript/engine/expression-resolution/callee-resolution.dl +++ b/graph/typescript/engine/expression-resolution/callee-resolution.dl @@ -190,6 +190,45 @@ expr_call_candidate(ce, m) :- call_site("client", _, cn, rk, recv, ce, _), expr_shape(recv, s), method_in_scope(s, cn, _, m). +// (6b) `call` / `apply` / `bind` ON A METHOD READ AS A VALUE: `this.render.bind(this)`, +// `proto.check.call(inst, x)`. The receiver is an access that names a METHOD, so it has no +// type or shape of its own for rules (4) to (6) to search, and the Function prototype carve-out +// in member-lookup.dl attaches only to a scope with a call signature. The compiler types the +// receiver as the method's function type and answers with its `bind`. method_value +// (value-flow.dl) recognises the same receiver but runs after this resolver, so the lookup is +// repeated here exactly as rules (4) and (6) make it. A getter is excluded: reading it runs it, +// and the value handed over is whatever it returns. +expr_call_candidate(ce, fm) :- call_site("client", _, cn, rk, recv, ce, _), + receiver_kind_is_value(rk), + function_prototype_member(cn, fm), + property_access_recv(recv, n, q), + expr_type(q, _, qt), + method_in_scope(qt, n, "false", m), + !method_kind(_, "GETTER", _, m), + !method_kind(_, "SETTER", _, m). +expr_call_candidate(ce, fm) :- call_site("client", _, cn, rk, recv, ce, _), + receiver_kind_is_value(rk), + function_prototype_member(cn, fm), + property_access_recv(recv, n, q), + expr_shape(q, s), + method_in_scope(s, n, _, m), + !method_kind(_, "GETTER", _, m), + !method_kind(_, "SETTER", _, m). +// …and what the bound result runs. `const write = sink.write.bind(sink); write(x)` calls +// `write`, which is what the compiler names. Typed only through `bind`'s generic return, the +// call reached the bodiless function types inside that return in the standard library and +// nothing else, a wrong answer as soon as (6b) gave the `bind` site its target. +expr_call_candidate(ce, m) :- call_callee_expr(_, ce, ne), + expr_referenced("client", "VARIABLE", v, ne), + var_initializer("client", _, bc, v), + call_site("client", _, "bind", _, recv, bc, _), + recv != "", + property_access_recv(recv, n, q), + expr_type(q, _, qt), + method_in_scope(qt, n, "false", m), + !method_kind(_, "GETTER", _, m), + !method_kind(_, "SETTER", _, m). + // (5b) A MEMBER OF AN OBJECT LITERAL BOUND TO A NAME — `handlers.viaShorthand(n)`. // An OBJECT_LITERAL_METHOD's owner is the literal's EXPRESSION, not a type, so it is in // no member table at all: `method_in_scope` is keyed on a scope and there is no scope @@ -484,6 +523,21 @@ expr_call_candidate(ce, ctor) :- call_site("client", "CONSTRUCTOR_CALL", _, _, _ call_callee_expr(_, ce, ne), expr_type(ne, _, ctorType), constructor_in_scope(ctorType, ctor). +// …and where the callee's type is a SHAPE rather than a declared type: a parameter typed +// `{ new (def: D): T }`, or an alias of one, as a factory taking "the class to build" is +// written. Rule (3) reads expr_shape for a plain call through a callable shape; construction +// read only expr_type, so the construct signature on the literal was never consulted and +// `new C(def)` resolved to nothing. Same lookup, construct side. +// +// Not where the callee is a variable known to HOLD a class (`const Ctor: new () => Dog = Dog`): +// the class's own constructor already answers there, and the annotation's bodiless signature +// beside it would hedge a site that was right. +expr_call_candidate(ce, ctor) :- call_site("client", "CONSTRUCTOR_CALL", _, _, _, ce, _), + call_callee_expr(_, ce, ne), + expr_shape(ne, s), + !callee_holds_class(ne), + constructor_in_scope(s, ctor). +callee_holds_class(ne) :- expr_referenced("client", "VARIABLE", v, ne), class_valued_var(v, _). // (10) `super(...)` — the enclosing class's `extends` parent's constructor. Never // an implements edge: `implements` inherits nothing, so it has no constructor to run. diff --git a/graph/typescript/engine/expression-resolution/expr-type.dl b/graph/typescript/engine/expression-resolution/expr-type.dl index 06396ef12..92f65cfca 100644 --- a/graph/typescript/engine/expression-resolution/expr-type.dl +++ b/graph/typescript/engine/expression-resolution/expr-type.dl @@ -162,6 +162,57 @@ expr_type(e, prov, t) :- property_access_recv(e, n, q), expr_type(q, _, qt), namespace_member(prov, qt, n, "TYPE", t). +// ── A CONST DECLARED IN A NAMESPACE — `N.run(x)`, `N.env.get(k)` ──────────── +// `declare namespace N { export const run: Runner; }` with `interface Runner { (x): void }`. +// The const is a VARIABLE whose enclosing type is the namespace, so namespace_member +// (types, methods, fields) never lists it and the access `N.run` had no type: the call +// through it found no call signature, and a member call one hop further found no +// receiver. This is how a runtime or a standard-library namespace publishes a callable +// object or a constructor (`var Format: FormatConstructor` inside a namespace). +// +// The namespace receiver comes three ways: a namespace typed as a value (a local +// namespace, a nested one), one typed as a static receiver (a global the binder knew +// as ambient), and a bare global name the binder could not bind at all, which resolves +// by name to a global NAMESPACE_TYPE exactly as rule (2d) in callee-resolution does +// for a namespace's functions. The name lookup is only for a reference the binder +// left unbound or ambient, so a local of the same name keeps its own binding. +// +// A const that holds a function literal is left to the method lookup that already +// answers with its body; its annotation beside it would add a bodiless signature as a +// second answer. A const whose type has no call signature stays uncallable: the call +// rules ask the type for a call signature, and a plain interface has none. +namespace_value_member(prov, ns, name, v) :- var_enclosing_type(prov, ns, v), + type_decl(prov, _, _, _, "NAMESPACE_TYPE", _, ns), + var_decl(prov, name, _, sk, _, _, v), + namespace_value_scope(sk), + name != "", + !var_is_function(v, _). +namespace_value_scope("NAMESPACE_SCOPE"). +namespace_value_scope("AMBIENT_SCOPE"). +expr_namespace_decl(q, ns) :- expr_type(q, prov, ns), + type_decl(prov, _, _, _, "NAMESPACE_TYPE", _, ns). +expr_namespace_decl(q, ns) :- expr_static_type(q, prov, ns), + type_decl(prov, _, _, _, "NAMESPACE_TYPE", _, ns). +expr_namespace_decl(q, ns) :- expr_kind("client", "IDENTIFIER_REFERENCE", _, q), + expr_referenced("client", rk, _, q), + unbound_global_ref_kind(rk), + expr_name("client", n, q), + n != "", + global_decl_wide(n, "TYPE", prov, ns), + type_decl(prov, _, _, _, "NAMESPACE_TYPE", _, ns). +unbound_global_ref_kind("UNKNOWN"). +unbound_global_ref_kind("AMBIENT_GLOBAL"). +expr_type(e, prov, t) :- property_access_recv(e, n, q), + expr_namespace_decl(q, ns), + scope_sibling(ns, sib), + namespace_value_member(_, sib, n, v), + var_type(v, prov, t). +expr_shape(e, s) :- property_access_recv(e, n, q), + expr_namespace_decl(q, ns), + scope_sibling(ns, sib), + namespace_value_member(_, sib, n, v), + var_shape(v, s). + // ── `this` ────────────────────────────────────────────────────────────────── expr_type(e, "client", t) :- expr_referenced("client", "THIS", _, e), expr_enclosing_type(e, t). @@ -178,6 +229,60 @@ this_expr(e) :- expr_referenced("client", "THIS", _, e). this_expr(e) :- expr_kind("client", "THIS_REFERENCE", _, e). method_declared_this(m, p) :- param_shape("client", "THIS", _, _, p), param_decl("client", _, _, _, m, p). +// ── `this` IN AN OBJECT LITERAL WRITTEN AS `X & ThisType` ──────────────── +// `defineProto({ inc() {…}, twice() { return this.inc() } })` where the parameter is +// `ProtoFor = { … } & ThisType`: the compiler types `this` in the literal's methods as +// T, which is the whole point of the marker. It is how a prototype table, a mixin or a store +// definition is written when its methods run with the instance as receiver. Nothing typed +// `this` there, so every `this.m()` in such a table resolved to nothing. +// +// thistype_arg(Ref, ArgRef): the contextual reference carries ThisType, with ArgRef as its +// argument written in Ref's own scope. Found through an intersection, a parenthesised type, +// and an alias, where the alias's own parameter is replaced by Ref's argument for it. That +// argument is then either a written type or the CALLEE's type variable, bound by the call +// (call_tvar_arg_binding, which covers an explicit type argument). +// +// Function-valued members only: an arrow property does not bind `this`, so it keeps the +// enclosing one. +thistype_arg(r, a) :- type_ref(prov, "TYPE_REFERENCE", _, "ThisType", _, _, _, r), + type_ref_parent(prov, r, "0", a). +thistype_arg(r, a) :- type_ref(prov, k, _, _, _, _, _, r), + thistype_carrier_kind(k), + type_ref_parent(prov, r, _, c), + thistype_arg(c, a). +thistype_arg(r, a) :- type_ref_resolves(_, r, t), + type_alias_target(_, ar, t), + thistype_arg(ar, v), + type_ref_type_param(_, tp, v), + type_param(_, pname, _, _, t, tp), + type_arg_binding(r, pname, a). +thistype_arg(r, a) :- type_ref_resolves(_, r, t), + type_alias_target(_, ar, t), + thistype_arg(ar, a), + !type_ref_type_param(_, _, a). +thistype_carrier_kind("INTERSECTION"). +thistype_carrier_kind("PARENTHESIZED"). + +objlit_this_type(l, prov, t) :- objlit_ctx_ref(l, r), + thistype_arg(r, a), + !type_ref_type_param(_, _, a), + ref_type_target(a, prov, t). +objlit_this_type(l, prov, t) :- expr_child("client", ce, "ARGUMENT", pos, l), + expr_kind("client", "OBJECT_LITERAL", _, l), + expr_call_candidate(ce, m), + param_decl(_, _, pos, _, m, p), + param_type_ref(_, r, p), + thistype_arg(r, a), + type_ref_type_param(_, tp, a), + type_param(_, _, _, _, m, tp), + call_tvar_arg_binding(ce, tp, prov, t). + +expr_type(e, prov, t) :- this_expr(e), + expr_enclosing_method(e, lm), + objlit_member_fn(l, _, lm), + !method_kind(_, "ARROW_FUNCTION", _, lm), + objlit_this_type(l, prov, t). + // `this` INSIDE A STATIC METHOD IS THE CLASS, so its members are the STATICS. A helper // class that calls its own statics through `this` is ordinary TypeScript — // `private static parse(...)` reached as `this.parse(...)` from another static — and the @@ -307,6 +412,20 @@ expr_shape(e, s) :- expr_kind("client", "BINARY_EXPRESSION", _, e), binary_op_names_operand("||", "LEFT_OPERAND"). binary_op_names_operand("??", "LEFT_OPERAND"). binary_op_names_operand("&&", "RIGHT_OPERAND"). +// …and that same operand's named TYPE: `const list = map.get(k) ?? []; list.push(x)`. +// The compiler types `a ?? b` as `NonNullable | B`, so the left operand's type is a +// member of the result whatever the right one is; dropping the nullish part is free here +// because `undefined` and `null` have no member table to look anything up in. The SHAPE +// rule above already flowed; the named type did not, so a defaulted collection or a +// defaulted service object lost its receiver type at the default. +// +// Still only the operand the compiler names, for the reason above: the right operand's +// type beside it puts a second, wrong declaration into a site the left one decides. +expr_type(e, prov, t) :- expr_kind("client", "BINARY_EXPRESSION", _, e), + expr_operator("client", op, e), + binary_op_names_operand(op, r), + expr_child("client", e, r, _, side), + expr_type(side, prov, t). // ── an ARROW / FUNCTION expression — the value IS the function ────────────── // `anonymousDeclarationHash` gives the ts_method the expression introduces. Java @@ -353,6 +472,84 @@ expr_type(e, prov, t) :- expr_kind("client", "BINARY_EXPRESSION", _, e), edge_role_is_binary_operand("LEFT_OPERAND"). edge_role_is_binary_operand("RIGHT_OPERAND"). +// ── ARITHMETIC — `(end - start).toFixed(1)`, `(-x).toString()` ────────────── +// `-`, `*`, `/`, `%`, `**`, the bitwise and shift operators, and unary `-`, `~`, `++`, +// `--` yield a number, or a bigint when the operands are bigints: the compiler rejects +// every other operand type for these operators (an object with a numeric valueOf +// included), so an operand typed anything but BigInt means the result is a number. +// Unary `+` is a number unconditionally. The receiver had no type at all before, so a +// call on a computed number — formatting a duration, rounding a ratio — resolved to +// nothing. +// +// Keyed on what the operand IS typed, never on what it is not: "a number unless an +// operand is a bigint" would negate expr_type inside its own fixpoint. An operand with +// no type (an untyped value) therefore gives the result no type either. +// +// `+` is a number only when BOTH operands are numbers. One string operand makes it a +// concatenation (the rule above), and that must stay a string and nothing else. +operand_typed_named(x, n) :- expr_type(x, prov, t), + type_decl(prov, n, _, _, _, _, t). +operand_not_bigint(x) :- operand_typed_named(x, n), n != "BigInt". +expr_type(e, prov, t) :- expr_kind("client", "BINARY_EXPRESSION", _, e), + expr_operator("client", op, e), + numeric_only_binary_op(op), + expr_child("client", e, r, _, operand), + edge_role_is_binary_operand(r), + operand_not_bigint(operand), + expr_module("client", mod, e), + name_binds_any(mod, "Number", prov, "TYPE", t). +expr_type(e, prov, t) :- expr_kind("client", "BINARY_EXPRESSION", _, e), + expr_operator("client", op, e), + numeric_only_binary_op(op), + expr_child("client", e, r, _, operand), + edge_role_is_binary_operand(r), + operand_typed_named(operand, "BigInt"), + expr_module("client", mod, e), + name_binds_any(mod, "BigInt", prov, "TYPE", t). +expr_type(e, prov, t) :- expr_kind("client", "BINARY_EXPRESSION", _, e), + expr_operator("client", "+", e), + expr_child("client", e, "LEFT_OPERAND", _, l), + operand_typed_named(l, "Number"), + expr_child("client", e, "RIGHT_OPERAND", _, r), + operand_typed_named(r, "Number"), + expr_module("client", mod, e), + name_binds_any(mod, "Number", prov, "TYPE", t). +expr_type(e, prov, t) :- expr_kind("client", "UNARY_EXPRESSION", _, e), + expr_operator("client", op, e), + numeric_unary_op(op), + expr_child("client", e, "UNARY_OPERAND", _, operand), + operand_not_bigint(operand), + expr_module("client", mod, e), + name_binds_any(mod, "Number", prov, "TYPE", t). +expr_type(e, prov, t) :- expr_kind("client", "UNARY_EXPRESSION", _, e), + expr_operator("client", op, e), + numeric_unary_op(op), + op != "+", + expr_child("client", e, "UNARY_OPERAND", _, operand), + operand_typed_named(operand, "BigInt"), + expr_module("client", mod, e), + name_binds_any(mod, "BigInt", prov, "TYPE", t). +expr_type(e, prov, t) :- expr_kind("client", "UNARY_EXPRESSION", _, e), + expr_operator("client", "+", e), + expr_module("client", mod, e), + name_binds_any(mod, "Number", prov, "TYPE", t). +numeric_only_binary_op("-"). +numeric_only_binary_op("*"). +numeric_only_binary_op("/"). +numeric_only_binary_op("%"). +numeric_only_binary_op("**"). +numeric_only_binary_op("&"). +numeric_only_binary_op("|"). +numeric_only_binary_op("^"). +numeric_only_binary_op("<<"). +numeric_only_binary_op(">>"). +numeric_only_binary_op(">>>"). +numeric_unary_op("-"). +numeric_unary_op("+"). +numeric_unary_op("~"). +numeric_unary_op("++"). +numeric_unary_op("--"). + // ── an ARRAY literal receiver — `[1,2].map(f)` ───────────────────────────── expr_type(e, prov, t) :- expr_kind("client", "ARRAY_LITERAL", _, e), expr_module("client", mod, e), @@ -426,6 +623,15 @@ expr_type(e, prov, t) :- property_access_recv(e, n, q), expr_shape(q, qs), field_in_scope(qs, n, _, f), field_type(f, prov, t). +// …and the member's SHAPE, off a shape qualifier. The typed-qualifier clause for +// field_shape is below; without this one a chain of inline object types, `ctx.common.issues` +// with `ctx: { common: { issues: Issue[] } }`, went one level deep and stopped: `ctx` had a +// shape, `ctx.common` read a shape-typed member off it and got nothing, and every call on +// the rest of the chain was unresolved. +expr_shape(e, s) :- property_access_recv(e, n, q), + expr_shape(q, qs), + field_in_scope(qs, n, _, f), + field_shape(f, s). // ── A PROPERTY ACCESS WHOSE MEMBER IS A GETTER ───────────────────────────── // `get entityToSyncMetadatas(): EntityMetadata[]`, read as // `this.entityToSyncMetadatas.map(...)`. Every rule above looks the member up in @@ -471,6 +677,21 @@ expr_type(e, prov, t) :- property_access_recv(e, n, q), expr_namespace(q, mprov, m), module_export(mprov, m, n, "VARIABLE", v), var_type(v, prov, t). +// …and its SHAPE, which is the half a function-typed export has: `export const parse: +// Parser = makeParser(1)` where `Parser` aliases a function type. The named import of the +// same const already reads var_shape (the IMPORT_BINDING clause above); read off the +// namespace, `ns.parse(x)` had a type-less, shape-less callee and resolved to nothing, while +// `import { parse }` beside it resolved. The value is a factory's result, so var_is_function +// has no literal to find and the declared shape is the only answer there is. +// +// Only there. Where the const DOES hold a function literal, rule (8) already answers with its +// body, and adding the annotation's bodiless signature beside it turned an implementation the +// site was right to name into a two-way hedge. +expr_shape(e, s) :- property_access_recv(e, n, q), + expr_namespace(q, _, m), + module_export(_, m, n, "VARIABLE", v), + !var_is_function(v, _), + var_shape(v, s). expr_static_type(e, prov, t) :- property_access_recv(e, n, q), expr_namespace(q, prov, m), module_export(prov, m, n, "TYPE", t). 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/engine/resolution/generics.dl b/graph/typescript/engine/resolution/generics.dl index 77f3f5197..5ea648375 100644 --- a/graph/typescript/engine/resolution/generics.dl +++ b/graph/typescript/engine/resolution/generics.dl @@ -52,6 +52,17 @@ expr_decl_ref(e, r) :- property_access_recv(e, n, q), expr_type(q, _, qt), field_in_scope(qt, n, "false", f), field_type_ref(_, r, f). +// An un-annotated field takes its initializer's reference, the field twin of the +// variable rule below: `private readonly byKey = new Map()` then +// `this.byKey.get(k)` substitutes V exactly as an annotated field does. The field's +// TYPE already came from the initializer (reference-types.dl); its type arguments did +// not, so the call resolved and its result had no type. +expr_decl_ref(e, r) :- property_access_recv(e, n, q), + expr_type(q, _, qt), + field_in_scope(qt, n, "false", f), + !field_has_declared_type(f), + field_initializer("client", init, f), + expr_decl_ref(init, r). // A chained receiver: `getItems().find(p)` — the reference is the callee's declared // RETURN type, which carries its own arguments. expr_decl_ref(e, r) :- expr_call_candidate(e, callee), diff --git a/graph/typescript/engine/resolution/reference-types.dl b/graph/typescript/engine/resolution/reference-types.dl index 04b5345bb..831f1f27a 100644 --- a/graph/typescript/engine/resolution/reference-types.dl +++ b/graph/typescript/engine/resolution/reference-types.dl @@ -494,6 +494,28 @@ param_binding_method(bh, m2) :- param_binding(_, _, _, "PROPERTY", src, _, bh), scope_sibling(t, sib), declared_method(sib, src, _, m2). +// (c) the pattern is typed by an OBJECT TYPE LITERAL — `({ port }: { port: Port })`, or +// `({ port }: Opts)` with `type Opts = { port: Port }`. A type literal has no declaration +// to give param_type; it is a SHAPE (param_shape_target), and a shape is its own scope +// (scope_sibling(s, s)), so the field and method lookups above read off it unchanged. +binding_pattern_shape(bh, sh) :- param_binding(_, _, pos, "PROPERTY", _, m, bh), + param_decl(_, _, pos, _, m, ph), + param_shape_target(ph, sh). +param_type(bh, prov, ft) :- param_binding(_, _, _, "PROPERTY", src, _, bh), + binding_pattern_shape(bh, sh), + scope_sibling(sh, sib), + declared_field(sib, src, _, f), + field_type(f, prov, ft). +param_shape_target(bh, fs) :- param_binding(_, _, _, "PROPERTY", src, _, bh), + binding_pattern_shape(bh, sh), + scope_sibling(sh, sib), + declared_field(sib, src, _, f), + field_shape(f, fs). +param_binding_method(bh, m2) :- param_binding(_, _, _, "PROPERTY", src, _, bh), + binding_pattern_shape(bh, sh), + scope_sibling(sh, sib), + declared_method(sib, src, _, m2). + // ── a DESTRUCTURED VARIABLE takes the type of the member it names ─────────── // `const { helper, nested } = ctx` — the mirror of the parameter case, and the more @@ -527,6 +549,27 @@ var_binding_method(v, m) :- var_binding("client", _, "PROPERTY", src, _, v), scope_sibling(t, sib), declared_method(sib, src, _, m). +// (c) the initializer has a SHAPE, not a type — `const { port } = opts` with +// `opts: { port: Port }`. `opts.port.execute()` already resolved through the shape +// (expr_shape off a shape qualifier); the destructured name beside it did not. +var_binding_shape(v, sh) :- var_binding("client", _, "PROPERTY", _, e, v), + e != "", + expr_shape(e, sh). +var_type(v, prov, ft) :- var_binding("client", _, "PROPERTY", src, _, v), + var_binding_shape(v, sh), + scope_sibling(sh, sib), + declared_field(sib, src, _, f), + field_type(f, prov, ft). +var_shape(v, fs) :- var_binding("client", _, "PROPERTY", src, _, v), + var_binding_shape(v, sh), + scope_sibling(sh, sib), + declared_field(sib, src, _, f), + field_shape(f, fs). +var_binding_method(v, m) :- var_binding("client", _, "PROPERTY", src, _, v), + var_binding_shape(v, sh), + scope_sibling(sh, sib), + declared_method(sib, src, _, m). + // ── an INDEX-DESTRUCTURED VARIABLE takes the ELEMENT type of its source ───── // `const [first] = ops` and `const [a, b] = makePair()`. The parser has emitted these diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl index be835f2c9..45af545cb 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) @@ -201,6 +204,7 @@ .decl callee_kind_is_expression(c0:symbol) .decl objlit_ctx_ref(c0:symbol,c1:symbol) .decl objlit_ctx_scope(c0:symbol,c1:symbol) +.decl objlit_this_type(c0:symbol,c1:symbol,c2:symbol) .decl objlit_prop(c0:symbol,c1:symbol,c2:symbol) .decl objlit_member_fn(c0:symbol,c1:symbol,c2:symbol) .decl fn_written_as_sig(c0:symbol,c1:symbol) @@ -393,6 +397,8 @@ .decl method_signature_role_of(c0:symbol,c1:symbol) .decl method_this_param(c0:symbol) .decl this_expr(c0:symbol) +.decl thistype_arg(c0:symbol,c1:symbol) +.decl thistype_carrier_kind(c0:symbol) .decl method_declared_this(c0:symbol,c1:symbol) .decl module_ambient_specifier(c0:symbol,c1:symbol,c2:symbol) .decl module_decl(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol) @@ -461,6 +467,7 @@ .decl owner_kind_is_module_level(c0:symbol) .decl owner_name_has_overload_signature(c0:symbol,c1:symbol) .decl param_call_signature(c0:symbol,c1:symbol) +.decl binding_pattern_shape(c0:symbol,c1:symbol) .decl binding_pattern_type(c0:symbol,c1:symbol,c2:symbol) .decl param_binding_method(c0:symbol,c1:symbol) .decl param_binding(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol,c6:symbol) @@ -556,6 +563,7 @@ .decl return_is_type_param(c0:symbol,c1:symbol) .decl call_type_argument(c0:symbol,c1:symbol,c2:symbol) .decl call_tvar_arg_binding(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl callee_holds_class(c0:symbol) .decl array_of_type_param(c0:symbol,c1:symbol) .decl param_array_wrapper_kind(c0:symbol) .decl return_type_variable(c0:symbol,c1:symbol) @@ -575,6 +583,14 @@ .decl specifier_global_target(c0:symbol,c1:symbol,c2:symbol) .decl specifier_is_relative(c0:symbol) .decl string_typed(c0:symbol) +.decl operand_typed_named(c0:symbol,c1:symbol) +.decl operand_not_bigint(c0:symbol) +.decl numeric_only_binary_op(c0:symbol) +.decl numeric_unary_op(c0:symbol) +.decl namespace_value_member(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl namespace_value_scope(c0:symbol) +.decl expr_namespace_decl(c0:symbol,c1:symbol) +.decl unbound_global_ref_kind(c0:symbol) .decl structural_implementor(c0:symbol,c1:symbol) .decl taint_gating(c0:symbol) .decl target_has_nominal_implementor(c0:symbol) @@ -629,6 +645,7 @@ .decl library_receiver(c0:symbol,c1:symbol) .decl unresolved_receiver(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl var_binding_method(c0:symbol,c1:symbol) +.decl var_binding_shape(c0:symbol,c1:symbol) .decl var_binding_target(c0:symbol,c1:symbol,c2:symbol) .decl var_binding(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol) .decl var_is_destructured(c0:symbol) diff --git a/package.json b/package.json index 0d7c265c3..c1fd43e95 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@axiomcode/code-graph", - "version": "0.2.2", + "version": "0.2.3", "description": "AxiomCode Graph: a resolved call graph of your codebase grounded in formal methods, so you and your coding agents can see who calls what, what a change breaks, and which tests it reaches.", "repository": { "type": "git", @@ -31,11 +31,11 @@ ], "license": "FSL-1.1-Apache-2.0", "optionalDependencies": { - "@axiomcode/engine-darwin-arm64": "0.2.2", - "@axiomcode/engine-darwin-x64": "0.2.2", - "@axiomcode/engine-linux-x64": "0.2.2", - "@axiomcode/engine-linux-arm64": "0.2.2", - "@axiomcode/engine-win32-x64": "0.2.2" + "@axiomcode/engine-darwin-arm64": "0.2.3", + "@axiomcode/engine-darwin-x64": "0.2.3", + "@axiomcode/engine-linux-x64": "0.2.3", + "@axiomcode/engine-linux-arm64": "0.2.3", + "@axiomcode/engine-win32-x64": "0.2.3" }, "bin": { "axiomcode": "bin/axiomcode.js" diff --git a/plugins/axiomcode/.claude-plugin/plugin.json b/plugins/axiomcode/.claude-plugin/plugin.json index 6e2eae13a..a45810eee 100644 --- a/plugins/axiomcode/.claude-plugin/plugin.json +++ b/plugins/axiomcode/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "axiomcode", "description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.", - "version": "0.2.2", + "version": "0.2.3", "author": { "name": "AxiomCode" } diff --git a/plugins/axiomcode/.codex-plugin/plugin.json b/plugins/axiomcode/.codex-plugin/plugin.json index 0348a6a37..29c2ba521 100644 --- a/plugins/axiomcode/.codex-plugin/plugin.json +++ b/plugins/axiomcode/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "axiomcode", - "version": "0.2.2", + "version": "0.2.3", "description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.", "author": { "name": "AxiomCode", "url": "https://github.com/AxiomCodeAI/axiomcodegraph" }, "homepage": "https://github.com/AxiomCodeAI/axiomcodegraph", diff --git a/plugins/axiomcode/.cursor-plugin/plugin.json b/plugins/axiomcode/.cursor-plugin/plugin.json index 74ce345ef..fadd636d0 100644 --- a/plugins/axiomcode/.cursor-plugin/plugin.json +++ b/plugins/axiomcode/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "axiomcode", - "version": "0.2.2", + "version": "0.2.3", "description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed. Java, TypeScript, Python, JavaScript.", "author": { "name": "AxiomCode" }, "homepage": "https://github.com/AxiomCodeAI/axiomcodegraph", diff --git a/plugins/axiomcode/mcp/_fallback.py b/plugins/axiomcode/mcp/_fallback.py index 8eb322681..a00b95b61 100644 --- a/plugins/axiomcode/mcp/_fallback.py +++ b/plugins/axiomcode/mcp/_fallback.py @@ -18,7 +18,14 @@ """ import inspect, json, sys, typing -__all__ = ["MCPServer"] +__all__ = ["MCPServer", "ToolError"] + + +class ToolError(Exception): + """A failure the tool anticipated and put into words: the words are the result's text and the result is marked + isError, as the SDK's own ToolError is. Any other exception is marked the same way, but with its type in front, + because its words were never meant for the caller.""" + _JSON_TYPE = {str: "string", int: "integer", float: "number", bool: "boolean", list: "array", dict: "object"} @@ -132,6 +139,8 @@ def _handle(self, msg): try: out = t["fn"](**args) return {"content": [{"type": "text", "text": "" if out is None else str(out)}]} + except ToolError as e: + return {"content": [{"type": "text", "text": str(e)}], "isError": True} except Exception as e: return {"content": [{"type": "text", "text": f"{type(e).__name__}: {e}"}], "isError": True} if m == "ping": diff --git a/plugins/axiomcode/mcp/server.py b/plugins/axiomcode/mcp/server.py index b1ebc5e62..cf1c3bda4 100755 --- a/plugins/axiomcode/mcp/server.py +++ b/plugins/axiomcode/mcp/server.py @@ -126,11 +126,17 @@ def unknown_arguments(name, arguments): said = [f"{k}: unexpected argument" for k in extra] return f"invalid arguments for {name}: " + '; '.join(said) + f". {name} takes: {', '.join(params) or 'no arguments'}" -try: - import importlib - ToolError = importlib.import_module(MCPServer.__module__.rsplit('.', 1)[0] + '.exceptions').ToolError -except Exception: - ToolError = ValueError +# A FAILURE IS RAISED, NOT RETURNED. A client marks a result isError only when the tool raised, and only the server's +# own ToolError keeps the words: the SDK (mcp ≥ 2.3) replaces any other exception's message with a bare "Error +# executing tool", and the fallback puts the type in front. So run() raises this one class, from whichever half serves. +if MCPServer.__module__ == '_fallback': + from _fallback import ToolError +else: + try: + import importlib + ToolError = importlib.import_module(MCPServer.__module__.rsplit('.', 1)[0] + '.exceptions').ToolError + except Exception: + ToolError = ValueError # an SDK with no exceptions module: the result is still isError, wordless class Server(MCPServer): def tool(self, *a, **k): @@ -152,6 +158,9 @@ async def call_tool(self, name, arguments, *a, **k): # the SDK: refused as a # launch.js hands over the bash it chose, because on Windows a bare `bash` is WSL's or nothing (#1233). BASH = os.environ.get('AXIOMCODE_BASH') or 'bash' +# how long a call may take before it is given up as a failure (seconds; a first build is minutes, so the default is +# generous); AXIOMCODE_MCP_TIMEOUT exists for a test that must see the timeout in seconds, not in minutes +TIMEOUT = float(os.environ.get('AXIOMCODE_MCP_TIMEOUT') or 900) # THE TIMER (#1305). The hooks refresh the graph while an agent works; an edit made in an editor or a terminal while the # session sits idle is caught only at the next prompt. The server lives as long as the session, so once @@ -190,12 +199,36 @@ def _timer(interval): if time.time() - fresh.last_update(repo) >= interval: fresh.hook_kick(repo, 'the timer') except Exception: pass -def run(args, cwd=None, timeout=900): +def repository(args, cwd=None): + """the repository a call asks about: the last positional argument that is a directory (a flag's value, `--in `, + is not one), else the working directory""" + pos = [a for i, a in enumerate(args[1:], 1) if args[i - 1] not in VALUED] + return next((a for a in reversed(pos) if os.path.isdir(a)), cwd or os.getcwd()) + +# WHAT A FAILURE IS. The CLI's exits: 0 is an answer, however empty; 1 is `path` saying in words that no chain connects +# the two; 2 is a refusal with its correction (nothing named X and the nearest names, no graph, no such directory), +# and ALSO the note that the graph is still being built. A refusal is a failure of the call, as an argument the tool +# does not take already is (#1567): the client marks the result isError and the model still reads the words. The +# building note is not: it is told apart by the build lock, which a build holds for as long as it runs. A crash, a +# kill, an exit no verb uses, or silence is a failure whatever the code says. +def failed(r, repo): + """whether a finished call is a failure the client must see as one, not an answer""" + if r.returncode == 0: return False + if r.returncode < 0 or r.returncode not in (1, 2): return True + if 'Traceback (most recent call last)' in (r.stderr or ''): return True + if r.returncode == 1: return not ((r.stdout or '').strip() or (r.stderr or '').strip()) + try: return not scripts_module('ax_fresh').building(os.path.realpath(repo)) + except Exception: return True + +def run(args, cwd=None, timeout=None): + """the CLI's words for a call; a call that failed (failed, a bash that will not start, a timeout with no build + running) raises ToolError with the words, so the client marks the result isError and the model still reads them""" + if timeout is None: timeout = TIMEOUT for a in args[1:]: if os.path.isdir(a) and os.path.isdir(os.path.join(a, '.axiomcode')): SEEN.add(os.path.realpath(a)) # A QUERY ON A REPOSITORY WITH NO GRAPH YET does not hold the call for the length of a first build (minutes; past this - # server's timeout it came back as a bare "Error executing tool"): the build is started in the background and the - # answer says what stage it is at (ax_contract.ensure_graph). `index` is asked for a build and still waits for it. + # server's timeout it is a failure saying only that): the build is started in the background and the answer says + # what stage it is at (ax_contract.ensure_graph). `index` is asked for a build and still waits for it. env = dict(os.environ, AXIOMCODE_BUILD_NOWAIT='1', AXIOMCODE_SURFACE='mcp') if args and args[0] != 'index' else None root = plugin_root() ax = os.path.join(root, 'skills', 'axiomcode', 'scripts', 'axiomcode') @@ -204,6 +237,7 @@ def run(args, cwd=None, timeout=900): if o: return f"axiomcode: {o}." note = stale_note(root) head = (note + '\n') if note else '' + repo = repository(args, cwd) try: # On Windows the arguments travel in AXIOMCODE_ARGV, not on the command line: Git Bash parses a command line by # msys rules (`'` quotes, `\"` is no escape), so a question holding an apostrophe lost every argument after it. @@ -213,23 +247,22 @@ def run(args, cwd=None, timeout=900): argv, env = [BASH, ax], dict(env or os.environ, AXIOMCODE_ARGV=''.join(f'{a}\x1f' for a in args)) r = subprocess.run(argv, cwd=cwd or None, capture_output=True, text=True, timeout=timeout, env=env) except OSError as e: - return (f"axiomcode could not start bash ({BASH}): {e}. On Windows it needs the bash that comes with " - "Git for Windows; install it, or set AXIOMCODE_BASH to its bin\\bash.exe.") + raise ToolError(f"axiomcode could not start bash ({BASH}): {e}. On Windows it needs the bash that comes with " + "Git for Windows; install it, or set AXIOMCODE_BASH to its bin\\bash.exe.") from None except subprocess.TimeoutExpired: - # a flag's value (`--in `) is not the repository - pos = [a for i, a in enumerate(args[1:], 1) if args[i - 1] not in VALUED] - repo = next((a for a in reversed(pos) if os.path.isdir(a)), cwd or os.getcwd()) try: ax_fresh = scripts_module('ax_fresh'); ax_contract = scripts_module('ax_contract') if ax_fresh.building(os.path.realpath(repo)): return head + ax_contract.building_note(os.path.realpath(repo)) except Exception: pass - return head + (f"axiomcode {args[0] if args else ''} did not answer within {timeout} s. Nothing was changed; " - f"see {os.path.join(repo, '.axiomcode', 'build.log')} if a build was running, and ask again.") + raise ToolError(head + f"axiomcode {args[0] if args else ''} did not answer within {timeout:g} s. Nothing was changed; " + f"see {os.path.join(repo, '.axiomcode', 'build.log')} if a build was running, and ask again.") from None out = (r.stdout or '') + (('\n' + r.stderr.strip()) if r.returncode and r.stderr.strip() else '') # an answer given from a graph that predates some edit says so, and names the files (#1305); one given from a graph a # fallback engine built, in place of the checkout's own, names that engine if not r.returncode: out += ''.join('\n' + l for l in (r.stderr or '').splitlines() if l.startswith(('graph refresh:', 'graph built by:'))) - return head + (out.strip() or f"(no output, exit {r.returncode})") + text = head + (out.strip() or f"(no output, exit {r.returncode})") + if failed(r, repo): raise ToolError(text + (f"\n(exit {r.returncode})" if out.strip() else '')) + return text # THE TOOLS TAKE NO OPTIONS, so an answer never tells the agent to pass one. The notes the verbs add (a stale # graph, a refresh in flight) are kept for what they say; a clause that names a flag or a parameter to set is dropped. @@ -249,6 +282,13 @@ def plain(text): if keep and ''.join(keep).strip(): out.append(' '.join(keep).rstrip(' ;,')) return '\n'.join(out) +def answer(args): + """a tool's answer: the CLI's words with the option-naming clauses dropped. A failure run() raises goes the same + way — its words through plain(), then on as the ToolError the client marks isError — so the model reads the + correction (the nearest names, the fix) either way, and a flag it cannot pass is named in neither.""" + try: return plain(run(args)) + except ToolError as e: raise ToolError(plain(str(e))) from None + # THE SMALL SURFACE. Search is grep's job; the graph answers what grep cannot. Three questions, each answered as # numbered places with the code of the function each sits in, so a place is understood without opening its file, @@ -259,20 +299,20 @@ def context(task: str, source: bool = False) -> str: """How something works, from a task in words: the files and callables the task touches, and for a "how does X work" question the call FLOW — every step in the order the calls are written, with ⚠ where the graph lost a call. source=True asks for the flow with each step's code, so it is read without opening files.""" - return plain(run(['context', task] + (['--source'] if source else []) + [os.getcwd()])) + return answer(['context', task] + (['--source'] if source else []) + [os.getcwd()]) @srv.tool() def impact(name: str = '') -> str: """What a change reaches. With a name (as written in the code: Owner.method, function, Type, or file.py:123): who calls it, what depends on it further out, and which tests exercise it, each with its code. With no name: the same for the declarations your uncommitted edits changed.""" - return plain(run(['impact'] + ([name] if name.strip() else []) + [os.getcwd()])) + return answer(['impact'] + ([name] if name.strip() else []) + [os.getcwd()]) @srv.tool() def path(start: str, end: str) -> str: """How one declaration reaches another: every hop of the call chain with the code at the line the call is written on. start / end as written in the code (Owner.method, function, Type).""" - return plain(run(['path', start, end, os.getcwd()])) + return answer(['path', start, end, os.getcwd()]) @srv.tool() def link(site: str = '', target: str = '') -> str: @@ -282,12 +322,12 @@ def link(site: str = '', target: str = '') -> str: whether the graph took it (a link whose line changed is dropped, never trusted). target "-" removes the site's links; target "not:" rejects a lead (a by-name or one-of-a-set guess) at that site, which is then not walked. Never link a guess, and never link a candidate for its rank alone.""" - return plain(run(['link'] + ([site] if site.strip() else []) + ([target] if site.strip() and target.strip() else []) + [os.getcwd()])) + return answer(['link'] + ([site] if site.strip() else []) + ([target] if site.strip() and target.strip() else []) + [os.getcwd()]) @srv.tool() def tests() -> str: """The tests your uncommitted edits reach, each with its code, and the command that runs exactly those.""" - return plain(run(['tests', os.getcwd()])) + return answer(['tests', os.getcwd()]) if __name__ == '__main__': # catch up on whatever changed while no session was running (#1305): started, never waited on diff --git a/plugins/axiomcode/skills/axiomcode/scripts/ax_links.py b/plugins/axiomcode/skills/axiomcode/scripts/ax_links.py index 940f845a1..103edbba5 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/ax_links.py +++ b/plugins/axiomcode/skills/axiomcode/scripts/ax_links.py @@ -295,6 +295,24 @@ def stable_name(g, mid, display, tfile): return display, (f"{tfile}:{ln[0][0]}" if ln else tfile) +def same_target(a, b, g=None, mid=None): + """two links name ONE declaration: the same recorded name in the same recorded file (and line, when the file carries + one). A name alone is not a declaration: every arrow function, lambda and anonymous class shares its placeholder name + (``, ``), so one site linked to two of them is two links. With a graph and the id `b` resolved to, a + row recorded under another spelling of that declaration (the file gained a second one of its name since) is it too.""" + if a.get('target') == b.get('target') and (a.get('target_file') or '') == (b.get('target_file') or ''): return True + if g is not None and mid is not None: + try: ts = g.targets(a.get('target') or '', a.get('target_file') or '') + except sqlite3.Error: return False + return len(ts) == 1 and ts[0][0] == mid + return False + + +def target_label(target, tfile): + """a target as printed: a placeholder name (``) says nothing alone, so it is printed with its declaration""" + return f"{target} ({tfile})" if tfile and re.split(r'[.:]', target or '')[-1].startswith('<') else target + + def is_value_lib(g, sid): """a site resolved only into a library method that runs a value (Method.invoke, Function.apply)""" rows = g.q("SELECT e.tier, s.callee_name FROM call_edges e JOIN call_sites s ON s.id = e.call_site_id WHERE e.call_site_id = ?", sid) @@ -1165,11 +1183,45 @@ def list_links(repo, as_json=False): print(f"{len(rows)} asserted link(s) in {os.path.relpath(links_path(repo), repo)}:") for r in rows: where = r['site'] + (f" (now line {r['now']})" if r.get('now') and r['site'] and int(r['site'].split(':')[1]) != r['now'] else '') - tgt = f"NOT {r['target']} (a lead rejected: not walked)" if r.get('rejects') else r['target'] + tl = target_label(r['target'], r.get('target_file')) + tgt = f"NOT {tl} (a lead rejected: not walked)" if r.get('rejects') else tl print(f" {r['link']:>3}. [{r['status']}] {where} → {tgt}" + (f" — {r['reason']}" if r['reason'] else '')) return 0 +def removal_targets(repo, at_site, tgt): + """the links at one site that `--remove ` names -> [link], or a refusal (str). is a name as the link + recorded it, or a declaration's file:line (the only way to tell two ``s at one site apart). A name that covers + links to more than one declaration removes none of them: it is refused with the file:line of each.""" + text = [l for l in at_site if l['target'] == tgt or (l.get('target_file') or '') == tgt] + ids = set() + for db, _r, live in graph_dbs(repo): + if not live: continue + try: + con = sqlite3.connect(f"file:{db}?mode=ro", uri=True) + try: + g = Graph(con) + want = {t[0] for t in g.targets(tgt)} + if not want: continue + gone = set() + for l in at_site: + ts = g.targets(l['target'], l.get('target_file') or '') + if len(ts) == 1 and ts[0][0] in want: ids.add(id(l)) + if not ts: gone.add(id(l)) + if len(want) == 1: + # the declaration it names: exactly the links that resolve to it (and a stale one recorded under that name) + return [l for l in at_site if id(l) in ids or (id(l) in gone and any(l is x for x in text))] + finally: con.close() + except sqlite3.Error: continue + hit = [l for l in at_site if any(l is x for x in text) or id(l) in ids] + decls = sorted({(l['target'], l.get('target_file') or '') for l in hit}) + if len(decls) > 1: + return (f"`{tgt}` names {len(decls)} links at {at_site[0]['file']}:{at_site[0]['line']} " + f"({'; '.join(f'{t} ({tf})' if tf else t for t, tf in decls)}): name the one to remove by its declaration's " + f"file:line, or `-` for all of them") + return hit + + def main(argv): as_json = '--json' in argv argv = [a for a in argv if a != '--json'] @@ -1192,8 +1244,11 @@ def main(argv): links, bad = read_links(repo) if remove or (len(args) > 1 and args[1] == '-'): tgt = args[1] if len(args) > 1 and args[1] != '-' else None - keep = [l for l in links if not (l['file'] == f and l['line'] == line and (tgt is None or l['target'] == tgt) - and (col is None or l.get('col') in (None, col)))] + at_site = [l for l in links if l['file'] == f and l['line'] == line and (col is None or l.get('col') in (None, col))] + hit = at_site if tgt is None else removal_targets(repo, at_site, tgt) + if isinstance(hit, str): + print(f"axiomcode link: {hit}", file=sys.stderr); return 1 + keep = [l for l in links if not any(l is h for h in hit)] gone = len(links) - len(keep) write_links(repo, keep + []) # malformed lines are dropped only by hand; they stay listed until fixed _restore_bad(repo, bad) @@ -1230,24 +1285,27 @@ def main(argv): new.update(callee=r.get('callee_written') or callee, caller=g.display(r['caller']) if r.get('caller') else '', target=r['target_display'], target_file=r.get('target_file') or '', **({'not': True} if reject else {})) new['target'], new['target_file'] = stable_name(g, r['callee_id'], r['target_display'], new['target_file']) - g.con.close() # the column the link resolved to is recorded even when none was given: it is what the link names from now on if r.get('col') and not new.get('col'): new['col'] = r['col']; new['ncol'] = norm_col(L[line - 1], r['col']) - dup = [l for l in links if l['file'] == f and l['line'] == line and l['target'] == new['target'] and l.get('col') == new.get('col') - and bool(l.get('not')) == bool(new.get('not'))] + # re-linking the same site to the same DECLARATION replaces that row; the same name declared elsewhere (two arrows + # handed to one registry) is another link + dup = [l for l in links if l['file'] == f and l['line'] == line and l.get('col') == new.get('col') + and bool(l.get('not')) == bool(new.get('not')) and same_target(l, new, g, r['callee_id'])] + g.con.close() links = [l for l in links if l not in dup] + [new] t0 = time.time() write_links(repo, links); _restore_bad(repo, bad) res = apply_repo(repo) ms = (time.time() - t0) * 1000 note_ = next((x['reason'] for live, rs in res.values() if live for l_, x in rs - if l_['file'] == f and l_['line'] == line and l_['target'] == new['target'] and l_.get('col') == new.get('col') and x['status'] in ('applied', 'moved')), '') + if l_['file'] == f and l_['line'] == line and same_target(l_, new) and l_.get('col') == new.get('col') and x['status'] in ('applied', 'moved')), '') where = f"{f}:{line}" + (f":{new['col']}" if new.get('col') else '') + shown = target_label(new['target'], new['target_file']) if reject: - print(f"rejected the lead {where} → {new['target']}: no walk takes it from now on (the engine's row is kept; " + print(f"rejected the lead {where} → {shown}: no walk takes it from now on (the engine's row is kept; " f"`axiomcode link {where} -` restores it) — applied to {sum(1 for v in res.values() if v[0])} graph(s) in {ms:.0f} ms") return 0 - print(f"linked {where} `{new['callee']}` → {new['target']} [asserted]" + (" (the graph already had this edge; recorded, nothing added)" if r['status'] == 'redundant' else '') + print(f"linked {where} `{new['callee']}` → {shown} [asserted]" + (" (the graph already had this edge; recorded, nothing added)" if r['status'] == 'redundant' else '') + (f"; {note_}" if note_ else '') + f" — applied to {sum(1 for v in res.values() if v[0])} graph(s) in {ms:.0f} ms") return 0 diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-link b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-link index d8f96c6c8..d26ff9ad2 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-link +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-link @@ -2,6 +2,8 @@ """axiomcode link [] [] axiomcode link [] every asserted link, and whether the graph took it (--json) axiomcode link - [] remove the links at that site (or --remove []) + ( as a name, or its declaration's file:line: two ``s at one + site are two links, told apart only by where each is declared) Records that the call written at file:line reaches — a declaration as the graph names it (Owner.method, function) or its file:line — where the graph could not resolve it. The link is kept in axiomcode-links.tsv at the diff --git a/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/case.json b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/case.json new file mode 100644 index 000000000..9e5afadbd --- /dev/null +++ b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/case.json @@ -0,0 +1,22 @@ +{"lang": "csharp", "src": "src", + "checks": [ + {"why": "inside namespace P, Models.Report is P.Models.Report", + "run": ["impact", "P.Models.Report.Make"], + "want": ["[resolved] Calls.Partially"]}, + {"why": "control: a file that declares only namespace P keeps its one correct target", + "run": ["impact", "P.Models.Report.Make"], + "want": ["[resolved] Alone.Only"]}, + {"why": "a child namespace of the site's is not in scope: P.Other.Models.Report is not named from P, even when the same file declares P.Other", + "run": ["impact", "P.Other.Models.Report.Make"], + "avoid": ["[resolved] Calls.Partially", "[one of a set] Calls.Partially", "Alone.Only"]}, + {"why": "inside P.Other, Models is P.Other.Models: the innermost namespace with a match wins, for a nested type too", + "run": ["impact", "P.Other.Models.Report.Make"], + "want": ["[resolved] Deeper.Nearest", "[resolved] Deeper.Inner.Nested"]}, + {"why": "...and the outer P.Models.Report is not also named from P.Other", + "run": ["impact", "P.Models.Report.Make"], + "avoid": ["[resolved] Deeper.Nearest", "[one of a set] Deeper.Nearest", "[resolved] Deeper.Inner.Nested", "[one of a set] Deeper.Inner.Nested"]}, + {"why": "control: an unrelated namespace declared in the same file is never read", + "run": ["impact", "Z.Models.Report.Make"], + "avoid": ["[resolved] Calls.Partially", "[one of a set] Calls.Partially", "Alone.Only", + "[resolved] Deeper.Nearest", "[one of a set] Deeper.Nearest", + "[resolved] Deeper.Inner.Nested", "[one of a set] Deeper.Inner.Nested"]}]} diff --git a/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Mixed.cs b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Mixed.cs new file mode 100644 index 000000000..0b4cf7534 --- /dev/null +++ b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Mixed.cs @@ -0,0 +1,28 @@ +// one file declaring several namespaces: the namespaces a name is read from are the SITE's, not the file's +namespace P.Models { public sealed class Report { public static Report Make() { return new Report(); } } } +namespace P.Other.Models { public sealed class Report { public static Report Make() { return new Report(); } } } +namespace Z.Models { public sealed class Report { public static Report Make() { return new Report(); } } } + +namespace P +{ + public sealed class Calls + { + // inside P, Models is P.Models; P.Other is a child of P and not in scope here + public Models.Report Partially() => Models.Report.Make(); + } +} + +namespace P.Other +{ + public sealed class Deeper + { + // inside P.Other, Models is P.Other.Models: the innermost namespace with a match wins + public Models.Report Nearest() => Models.Report.Make(); + + // a nested type reads from the same namespace as the type around it + public sealed class Inner + { + public Models.Report Nested() => Models.Report.Make(); + } + } +} diff --git a/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Report.csproj b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Report.csproj new file mode 100644 index 000000000..d3b2f0306 --- /dev/null +++ b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Report.csproj @@ -0,0 +1 @@ +net8.0 diff --git a/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Single.cs b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Single.cs new file mode 100644 index 000000000..55e77d379 --- /dev/null +++ b/tests/cases/csharp/a-relative-name-is-read-from-the-sites-namespace/src/Single.cs @@ -0,0 +1,8 @@ +// control: a file that declares only namespace P keeps its one correct target +namespace P +{ + public sealed class Alone + { + public Models.Report Only() => Models.Report.Make(); + } +} diff --git a/tests/cases/csharp/asserted-links-anonymous/App/Bus.cs b/tests/cases/csharp/asserted-links-anonymous/App/Bus.cs new file mode 100644 index 000000000..f836e27df --- /dev/null +++ b/tests/cases/csharp/asserted-links-anonymous/App/Bus.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace App +{ + public class Bus + { + private readonly List> listeners = new List>(); + + public void On(Action listener) + { + listeners.Add(listener); + } + + public void Emit(string x) + { + foreach (var listener in listeners) + { + listener(x); + } + } + } +} diff --git a/tests/cases/csharp/asserted-links-anonymous/App/Wire.cs b/tests/cases/csharp/asserted-links-anonymous/App/Wire.cs new file mode 100644 index 000000000..c02d368f6 --- /dev/null +++ b/tests/cases/csharp/asserted-links-anonymous/App/Wire.cs @@ -0,0 +1,18 @@ +namespace App +{ + public static class Wire + { + public static void Created(string x) { } + public static void Updated(string x) { } + public static void OnCreated(string x) { Created(x); } + public static void OnUpdated(string x) { Updated(x); } + + public static void Connect(Bus bus) + { + bus.On(x => Created(x)); + bus.On(x => Updated(x)); + bus.On(OnCreated); + bus.On(OnUpdated); + } + } +} diff --git a/tests/cases/csharp/asserted-links-anonymous/case.json b/tests/cases/csharp/asserted-links-anonymous/case.json new file mode 100644 index 000000000..caf1fd3ee --- /dev/null +++ b/tests/cases/csharp/asserted-links-anonymous/case.json @@ -0,0 +1,190 @@ +{ + "lang": "csharp", + "checks": [ + { + "why": "BASE: one call site linked to an anonymous function: the link names it by its declaration, since every one of the lambdas shares the name Wire.", + "run": [ + "link", + "App/Bus.cs:19", + "App/Wire.cs:12" + ], + "want": [ + "linked App/Bus.cs:19:17 `listener` → Wire. (App/Wire.cs:12) [asserted]" + ] + }, + { + "why": "a second link from the SAME site to ANOTHER of the lambdas is a second link, not a replacement of the first (both are named Wire.)", + "run": [ + "link", + "App/Bus.cs:19", + "App/Wire.cs:13" + ], + "want": [ + "linked App/Bus.cs:19:17 `listener` → Wire. (App/Wire.cs:13) [asserted]" + ] + }, + { + "why": "both links are kept and both applied", + "run": [ + "link", + "--list" + ], + "want": [ + "2 asserted link(s)", + "[applied] App/Bus.cs:19:17 → Wire. (App/Wire.cs:12)", + "[applied] App/Bus.cs:19:17 → Wire. (App/Wire.cs:13)" + ] + }, + { + "why": "CONTROL: re-linking the same site to the same declaration replaces its row (idempotent)", + "run": [ + "link", + "App/Bus.cs:19", + "App/Wire.cs:12" + ], + "want": [ + "linked App/Bus.cs:19:17 `listener` → Wire. (App/Wire.cs:12) [asserted]" + ] + }, + { + "why": "CONTROL: still two links after the re-link", + "run": [ + "link", + "--list" + ], + "want": [ + "2 asserted link(s)", + "→ Wire. (App/Wire.cs:12)", + "→ Wire. (App/Wire.cs:13)" + ], + "avoid": [ + "3 asserted link(s)" + ] + }, + { + "why": "removing by the placeholder name would remove two different links: refused, naming each declaration", + "run": [ + "link", + "--remove", + "App/Bus.cs:19", + "Wire." + ], + "expect_error": true, + "want": [ + "`Wire.` names 2 links at App/Bus.cs:19", + "(App/Wire.cs:12)", + "(App/Wire.cs:13)", + "file:line" + ] + }, + { + "why": "nothing was removed by the refused removal", + "run": [ + "link", + "--list" + ], + "want": [ + "2 asserted link(s)" + ] + }, + { + "why": "removing by the declaration's file:line removes that link only", + "run": [ + "link", + "--remove", + "App/Bus.cs:19", + "App/Wire.cs:12" + ], + "want": [ + "removed 1 link(s) at App/Bus.cs:19" + ] + }, + { + "why": "the other anonymous target's link stays", + "run": [ + "link", + "--list" + ], + "want": [ + "1 asserted link(s)", + "→ Wire. (App/Wire.cs:13)" + ], + "avoid": [ + "→ Wire. (App/Wire.cs:12)" + ] + }, + { + "why": "CONTROL: the same site linked to two NAMED functions keeps both", + "run": [ + "link", + "App/Bus.cs:19", + "Wire.OnCreated" + ], + "want": [ + "→ Wire.OnCreated [asserted]" + ] + }, + { + "why": "CONTROL: the second named function", + "run": [ + "link", + "App/Bus.cs:19", + "Wire.OnUpdated" + ], + "want": [ + "→ Wire.OnUpdated [asserted]" + ] + }, + { + "why": "CONTROL: three links: one anonymous, two named", + "run": [ + "link", + "--list" + ], + "want": [ + "3 asserted link(s)", + "→ Wire. (App/Wire.cs:13)", + "→ Wire.OnCreated", + "→ Wire.OnUpdated" + ] + }, + { + "why": "CONTROL: --remove removes only the one named", + "run": [ + "link", + "--remove", + "App/Bus.cs:19", + "Wire.OnCreated" + ], + "want": [ + "removed 1 link(s) at App/Bus.cs:19" + ] + }, + { + "why": "CONTROL: the other named link and the anonymous one stay", + "run": [ + "link", + "--list" + ], + "want": [ + "2 asserted link(s)", + "→ Wire. (App/Wire.cs:13)", + "→ Wire.OnUpdated" + ], + "avoid": [ + "→ Wire.OnCreated " + ] + }, + { + "why": "`-` still removes every link at the site", + "run": [ + "link", + "App/Bus.cs:19", + "-" + ], + "want": [ + "removed 2 link(s) at App/Bus.cs:19" + ] + } + ] +} diff --git a/tests/cases/python/asserted-links-anonymous/app/__init__.py b/tests/cases/python/asserted-links-anonymous/app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/cases/python/asserted-links-anonymous/app/bus.py b/tests/cases/python/asserted-links-anonymous/app/bus.py new file mode 100644 index 000000000..890eda87f --- /dev/null +++ b/tests/cases/python/asserted-links-anonymous/app/bus.py @@ -0,0 +1,10 @@ +class Bus: + def __init__(self): + self.listeners = [] + + def on(self, listener): + self.listeners.append(listener) + + def emit(self, x): + for listener in self.listeners: + listener(x) diff --git a/tests/cases/python/asserted-links-anonymous/app/wire.py b/tests/cases/python/asserted-links-anonymous/app/wire.py new file mode 100644 index 000000000..d81e122a9 --- /dev/null +++ b/tests/cases/python/asserted-links-anonymous/app/wire.py @@ -0,0 +1,11 @@ +def created(x): pass +def updated(x): pass +def on_created(x): created(x) +def on_updated(x): updated(x) + + +def wire(bus): + bus.on(lambda x: created(x)) + bus.on(lambda x: updated(x)) + bus.on(on_created) + bus.on(on_updated) diff --git a/tests/cases/python/asserted-links-anonymous/case.json b/tests/cases/python/asserted-links-anonymous/case.json new file mode 100644 index 000000000..eec9c2cb1 --- /dev/null +++ b/tests/cases/python/asserted-links-anonymous/case.json @@ -0,0 +1,191 @@ +{ + "lang": "python", + "src": ".", + "checks": [ + { + "why": "BASE: one call site linked to an anonymous function: the link names it by its declaration, since every one of the lambdas shares the name ", + "run": [ + "link", + "app/bus.py:10", + "app/wire.py:8" + ], + "want": [ + "linked app/bus.py:10:13 `listener` → (app/wire.py:8) [asserted]" + ] + }, + { + "why": "a second link from the SAME site to ANOTHER of the lambdas is a second link, not a replacement of the first (both are named )", + "run": [ + "link", + "app/bus.py:10", + "app/wire.py:9" + ], + "want": [ + "linked app/bus.py:10:13 `listener` → (app/wire.py:9) [asserted]" + ] + }, + { + "why": "both links are kept and both applied", + "run": [ + "link", + "--list" + ], + "want": [ + "2 asserted link(s)", + "[applied] app/bus.py:10:13 → (app/wire.py:8)", + "[applied] app/bus.py:10:13 → (app/wire.py:9)" + ] + }, + { + "why": "CONTROL: re-linking the same site to the same declaration replaces its row (idempotent)", + "run": [ + "link", + "app/bus.py:10", + "app/wire.py:8" + ], + "want": [ + "linked app/bus.py:10:13 `listener` → (app/wire.py:8) [asserted]" + ] + }, + { + "why": "CONTROL: still two links after the re-link", + "run": [ + "link", + "--list" + ], + "want": [ + "2 asserted link(s)", + "→ (app/wire.py:8)", + "→ (app/wire.py:9)" + ], + "avoid": [ + "3 asserted link(s)" + ] + }, + { + "why": "removing by the placeholder name would remove two different links: refused, naming each declaration", + "run": [ + "link", + "--remove", + "app/bus.py:10", + "" + ], + "expect_error": true, + "want": [ + "`` names 2 links at app/bus.py:10", + "(app/wire.py:8)", + "(app/wire.py:9)", + "file:line" + ] + }, + { + "why": "nothing was removed by the refused removal", + "run": [ + "link", + "--list" + ], + "want": [ + "2 asserted link(s)" + ] + }, + { + "why": "removing by the declaration's file:line removes that link only", + "run": [ + "link", + "--remove", + "app/bus.py:10", + "app/wire.py:8" + ], + "want": [ + "removed 1 link(s) at app/bus.py:10" + ] + }, + { + "why": "the other anonymous target's link stays", + "run": [ + "link", + "--list" + ], + "want": [ + "1 asserted link(s)", + "→ (app/wire.py:9)" + ], + "avoid": [ + "→ (app/wire.py:8)" + ] + }, + { + "why": "CONTROL: the same site linked to two NAMED functions keeps both", + "run": [ + "link", + "app/bus.py:10", + "on_created" + ], + "want": [ + "→ on_created [asserted]" + ] + }, + { + "why": "CONTROL: the second named function", + "run": [ + "link", + "app/bus.py:10", + "on_updated" + ], + "want": [ + "→ on_updated [asserted]" + ] + }, + { + "why": "CONTROL: three links: one anonymous, two named", + "run": [ + "link", + "--list" + ], + "want": [ + "3 asserted link(s)", + "→ (app/wire.py:9)", + "→ on_created", + "→ on_updated" + ] + }, + { + "why": "CONTROL: --remove removes only the one named", + "run": [ + "link", + "--remove", + "app/bus.py:10", + "on_created" + ], + "want": [ + "removed 1 link(s) at app/bus.py:10" + ] + }, + { + "why": "CONTROL: the other named link and the anonymous one stay", + "run": [ + "link", + "--list" + ], + "want": [ + "2 asserted link(s)", + "→ (app/wire.py:9)", + "→ on_updated" + ], + "avoid": [ + "→ on_created " + ] + }, + { + "why": "`-` still removes every link at the site", + "run": [ + "link", + "app/bus.py:10", + "-" + ], + "want": [ + "removed 2 link(s) at app/bus.py:10" + ] + } + ] +} diff --git a/tests/cases/typescript/asserted-links-anonymous/case.json b/tests/cases/typescript/asserted-links-anonymous/case.json new file mode 100644 index 000000000..1254461e7 --- /dev/null +++ b/tests/cases/typescript/asserted-links-anonymous/case.json @@ -0,0 +1,191 @@ +{ + "lang": "typescript", + "src": "src", + "checks": [ + { + "why": "BASE: one call site linked to an anonymous function: the link names it by its declaration, since every one of the arrow functions shares the name ", + "run": [ + "link", + "src/bus.ts:10", + "src/wire.ts:9" + ], + "want": [ + "linked src/bus.ts:10:7 `listener` → (src/wire.ts:9) [asserted]" + ] + }, + { + "why": "a second link from the SAME site to ANOTHER of the arrow functions is a second link, not a replacement of the first (both are named )", + "run": [ + "link", + "src/bus.ts:10", + "src/wire.ts:10" + ], + "want": [ + "linked src/bus.ts:10:7 `listener` → (src/wire.ts:10) [asserted]" + ] + }, + { + "why": "both links are kept and both applied", + "run": [ + "link", + "--list" + ], + "want": [ + "2 asserted link(s)", + "[applied] src/bus.ts:10:7 → (src/wire.ts:9)", + "[applied] src/bus.ts:10:7 → (src/wire.ts:10)" + ] + }, + { + "why": "CONTROL: re-linking the same site to the same declaration replaces its row (idempotent)", + "run": [ + "link", + "src/bus.ts:10", + "src/wire.ts:9" + ], + "want": [ + "linked src/bus.ts:10:7 `listener` → (src/wire.ts:9) [asserted]" + ] + }, + { + "why": "CONTROL: still two links after the re-link", + "run": [ + "link", + "--list" + ], + "want": [ + "2 asserted link(s)", + "→ (src/wire.ts:9)", + "→ (src/wire.ts:10)" + ], + "avoid": [ + "3 asserted link(s)" + ] + }, + { + "why": "removing by the placeholder name would remove two different links: refused, naming each declaration", + "run": [ + "link", + "--remove", + "src/bus.ts:10", + "" + ], + "expect_error": true, + "want": [ + "`` names 2 links at src/bus.ts:10", + "(src/wire.ts:9)", + "(src/wire.ts:10)", + "file:line" + ] + }, + { + "why": "nothing was removed by the refused removal", + "run": [ + "link", + "--list" + ], + "want": [ + "2 asserted link(s)" + ] + }, + { + "why": "removing by the declaration's file:line removes that link only", + "run": [ + "link", + "--remove", + "src/bus.ts:10", + "src/wire.ts:9" + ], + "want": [ + "removed 1 link(s) at src/bus.ts:10" + ] + }, + { + "why": "the other anonymous target's link stays", + "run": [ + "link", + "--list" + ], + "want": [ + "1 asserted link(s)", + "→ (src/wire.ts:10)" + ], + "avoid": [ + "→ (src/wire.ts:9)" + ] + }, + { + "why": "CONTROL: the same site linked to two NAMED functions keeps both", + "run": [ + "link", + "src/bus.ts:10", + "onCreated" + ], + "want": [ + "→ onCreated [asserted]" + ] + }, + { + "why": "CONTROL: the second named function", + "run": [ + "link", + "src/bus.ts:10", + "onUpdated" + ], + "want": [ + "→ onUpdated [asserted]" + ] + }, + { + "why": "CONTROL: three links: one anonymous, two named", + "run": [ + "link", + "--list" + ], + "want": [ + "3 asserted link(s)", + "→ (src/wire.ts:10)", + "→ onCreated", + "→ onUpdated" + ] + }, + { + "why": "CONTROL: --remove removes only the one named", + "run": [ + "link", + "--remove", + "src/bus.ts:10", + "onCreated" + ], + "want": [ + "removed 1 link(s) at src/bus.ts:10" + ] + }, + { + "why": "CONTROL: the other named link and the anonymous one stay", + "run": [ + "link", + "--list" + ], + "want": [ + "2 asserted link(s)", + "→ (src/wire.ts:10)", + "→ onUpdated" + ], + "avoid": [ + "→ onCreated " + ] + }, + { + "why": "`-` still removes every link at the site", + "run": [ + "link", + "src/bus.ts:10", + "-" + ], + "want": [ + "removed 2 link(s) at src/bus.ts:10" + ] + } + ] +} diff --git a/tests/cases/typescript/asserted-links-anonymous/src/bus.ts b/tests/cases/typescript/asserted-links-anonymous/src/bus.ts new file mode 100644 index 000000000..ac164a341 --- /dev/null +++ b/tests/cases/typescript/asserted-links-anonymous/src/bus.ts @@ -0,0 +1,13 @@ +export class Bus { + private readonly listeners: Array<(x: string) => void> = []; + + on(listener: (x: string) => void): void { + this.listeners.push(listener); + } + + emit(x: string): void { + for (const listener of this.listeners) { + listener(x); + } + } +} diff --git a/tests/cases/typescript/asserted-links-anonymous/src/wire.ts b/tests/cases/typescript/asserted-links-anonymous/src/wire.ts new file mode 100644 index 000000000..5c8b29fea --- /dev/null +++ b/tests/cases/typescript/asserted-links-anonymous/src/wire.ts @@ -0,0 +1,13 @@ +import { Bus } from "./bus"; + +export function created(x: string): void {} +export function updated(x: string): void {} +export function onCreated(x: string): void { created(x); } +export function onUpdated(x: string): void { updated(x); } + +export function wire(bus: Bus): void { + bus.on((x) => created(x)); + bus.on((x) => updated(x)); + bus.on(onCreated); + bus.on(onUpdated); +} diff --git a/tests/corrupt_graph.py b/tests/corrupt_graph.py index b999e00d1..a37cd2e04 100644 --- a/tests/corrupt_graph.py +++ b/tests/corrupt_graph.py @@ -121,13 +121,16 @@ def check(ok, why, detail=''): open(os.path.realpath(ptr), 'wb').write(os.urandom(8192)) sys.path.insert(0, os.path.join(ROOT, 'plugins', 'axiomcode', 'mcp')); os.environ.update(AXIOMCODE_ENGINE=ROOT, AXIOMCODE_NO_REFRESH='1') import server - text = server.run(['impact', 'helper', repo]) + def words(args): # a refusal comes as the server's ToolError, words intact + try: return server.run(args) + except server.ToolError as e: return str(e) + text = words(['impact', 'helper', repo]) check('Traceback' not in text and 'corrupt' in text, 'mcp: the tool answer says the graph is corrupt, no traceback', text) sys.path.insert(0, SCRIPTS); import ax_fresh for _ in range(600): # the rebuild it started if os.path.exists(ptr) and not ax_fresh.building(repo): break time.sleep(1) - text = server.run(['impact', 'helper', repo]) + text = words(['impact', 'helper', repo]) check('caller' in text and 'corrupt' not in text, 'mcp: once rebuilt the tool answers again', text) # ── bad page ──────────────────────────────────────────────────────────────────────────────────────── diff --git a/tests/latency.py b/tests/latency.py index ad4064985..a908df6ef 100644 --- a/tests/latency.py +++ b/tests/latency.py @@ -313,9 +313,12 @@ def timeout_checks(): with contextlib.redirect_stderr(io.StringIO()): spec.loader.exec_module(m) work = tempfile.mkdtemp(prefix='axiomcode-timeout-') try: - try: out = m.run(['context', 'anything', work], timeout=0.01); raised = '' + # the words come as the server's own ToolError, which the client marks isError; a bare exception would lose them, + # and a plain return would pass the failure off as an answer + try: out = m.run(['context', 'anything', work], timeout=0.01); raised = 'returned as an answer' + except m.ToolError as e: out, raised = str(e), '' except Exception as e: out, raised = '', repr(e) - check("timeout: an MCP call past the server's timeout answers in words, not with an exception", + check("timeout: an MCP call past the server's timeout fails in words that say so, as a tool error", not raised and 'did not answer within' in out, raised or out) finally: shutil.rmtree(work, ignore_errors=True) diff --git a/tests/mcp.py b/tests/mcp.py index f85b37d37..1b4311dd6 100644 --- a/tests/mcp.py +++ b/tests/mcp.py @@ -190,11 +190,96 @@ def text(self, name, arguments): return ''.join(c.get('text', '') for c in self.ask('tools/call', {'name': name, 'arguments': arguments}).get('content', [])) def close(self): + """ends the server; returns what it wrote on stderr, which says which half served (the fallback's banner)""" self.timer.cancel() self.p.stdin.close() self.p.wait() + err = self.p.stderr.read() self.p.stdout.close() self.p.stderr.close() + return err + + +FALLBACK_BANNER = 'serving with the built-in fallback' +# holds a repository's build lock the way a build does (ax_fresh._flock: LOCK_EX on .axiomcode/build.lock), from a +# process of its own, and says so once it has it +HOLD = ("import fcntl, os, sys, time; fd = os.open(sys.argv[1], os.O_RDWR | os.O_CREAT, 0o644); " + "fcntl.flock(fd, fcntl.LOCK_EX); print('held', flush=True); time.sleep(120)") + + +def check_failures(label, cmd, cwd, env=None, workdir=None, sdk=None): + """A call the CLI could not carry out is a tool error (isError), never an answer: a bash that is not there, a verb + that crashed or was killed, an exit no verb uses, silence, a timeout with no build running, and a refusal (nothing + named X, no graph: exit 2, as an argument the tool lacks already is, #1567). The controls are what must NOT be an + error: an answer, an empty one, `path` saying "no chain" (exit 1, in words), and the note that a graph is still + being built (exit 2 while the build lock is held). The CLI is a fake bash that says what each case needs, so what is + tested is the server's own reading of it; the words must survive, and the SDK puts a prefix on them, so substrings. + `sdk` True or False asserts which half served the mode, read off the stderr banner.""" + if os.name == 'nt': + return [] + bad = [] + here = workdir or cwd # the server's cwd, which is the repository every tool asks about + fakes = tempfile.mkdtemp(prefix='ax-bash-') + base = {k: v for k, v in (env or os.environ).items() if k != 'AXIOMCODE_BASH'} + base.update(AXIOMCODE_MCP_TIMEOUT='2', AXIOMCODE_REFRESH_INTERVAL='0') + + def fake(name, body): + p = os.path.join(fakes, name) + with open(p, 'w') as f: + f.write('#!/bin/sh\n' + body + '\n') + os.chmod(p, 0o755) + return p + crash = r"""printf 'Traceback (most recent call last):\n File "x.py", line 1, in \nValueError: bad\n' >&2; exit 1""" + note = 'the graph for $PWD is being built (starting; 0m01s so far). Nothing else is needed: ask again in a minute or two' + cases = [('missing', os.path.join(fakes, 'no-bash'), True, ['could not start']), + ('boom', fake('boom', "printf 'boom\\n' >&2; exit 7"), True, ['boom', 'exit 7']), + ('slow', fake('slow', 'exec sleep 30'), True, ['did not answer within']), # exec: the kill reaches the sleeper + ('crash', fake('crash', crash), True, ['Traceback']), + ('no chain', fake('nochain', 'echo "no chain of resolved calls connects a and b in either direction"; exit 1'), False, ['no chain']), + ('empty ok', fake('empty', 'exit 0'), False, ['(no output, exit 0)']), + ('building', fake('building', f'echo "{note}"; exit 2'), False, ['being built']), + ('refusal', fake('refusal', 'echo "nothing named zz is declared in this graph. Exact names that are close: a, b"; exit 2'), True, ['close:', 'exit 2']), + ('silent 1', fake('silent', 'exit 1'), True, ['(no output, exit 1)'])] + served = None + try: + for name, bash, want, needles in cases: + s = Session(cmd, here, dict(base, AXIOMCODE_BASH=bash)) + holder = None + try: + if name == 'building': # after the start, so the server's own start-up sees no .axiomcode + os.makedirs(os.path.join(here, '.axiomcode'), exist_ok=True) + holder = subprocess.Popen([sys.executable, '-c', HOLD, os.path.join(here, '.axiomcode', 'build.lock')], + stdout=subprocess.PIPE, text=True) + holder.stdout.readline() + res = s.ask('tools/call', {'name': 'path', 'arguments': {'start': 'a', 'end': 'b'}}) + finally: + if holder: + holder.kill(); holder.wait(); holder.stdout.close() + shutil.rmtree(os.path.join(here, '.axiomcode'), ignore_errors=True) + err = s.close() + if served is None: + served = 'the fallback' if FALLBACK_BANNER in err else 'the SDK' + text = ' '.join(c.get('text', '') for c in res.get('content', [])) + if bool(res.get('isError')) != want: + bad.append(f"{label}, {name}: isError {res.get('isError')!r}, want {want}: {text[:300]!r}") + for n in needles: + if n not in text: + bad.append(f"{label}, {name}: the words lack {n!r}: {text[:300]!r}") + finally: + shutil.rmtree(fakes, ignore_errors=True) + print(f"{label}: failures checked, served by {served}") + if sdk is not None and served != ('the SDK' if sdk else 'the fallback'): + bad.append(f"{label}: served by {served}, but this mode was meant to test {'the SDK' if sdk else 'the fallback'}") + return bad + + +def sdk_python(): + """an interpreter with the MCP SDK, so the failure checks run on that half too: AXIOMCODE_SDK_PYTHON, else this + one; None when neither imports it""" + for py in filter(None, (os.environ.get('AXIOMCODE_SDK_PYTHON'), sys.executable)): + if subprocess.run([py, '-c', 'import mcp.server.mcpserver'], capture_output=True).returncode == 0: + return py + return None def check_install_move(work): @@ -309,6 +394,7 @@ def main(): bad += check('bin/axiomcode mcp', ['bash', CLI, 'mcp'], repo) # the SDK when the launcher finds one, which ignored an argument it did not know (#1567); else the fallback again bad += check_arguments('bin/axiomcode mcp', ['bash', CLI, 'mcp'], repo, lax=True) + bad += check_failures('bin/axiomcode mcp', ['bash', CLI, 'mcp'], repo) bad += check_front_door() if os.name != 'nt': bad += check_install_move(work) @@ -316,6 +402,15 @@ def main(): env_note = 'python3 -S server.py (fallback, no SDK)' bad += check(env_note, [sys.executable, '-S', SERVER], repo) bad += check_arguments(env_note, [sys.executable, '-S', SERVER], repo) + bad += check_failures(env_note, [sys.executable, '-S', SERVER], repo, sdk=False) + # the same failures through the SDK, when an interpreter has it (a venv with `pip install mcp`, named by + # AXIOMCODE_SDK_PYTHON); the skip is said out loud, so a run without the SDK is not mistaken for one with it + py = sdk_python() + if py: + bad += check_failures('server.py (SDK)', [py, SERVER], repo, sdk=True) + else: + print('SDK path skipped: mcp not importable (python3 -m venv && /bin/pip install mcp && ' + 'AXIOMCODE_SDK_PYTHON=/bin/python python3 tests/mcp.py)') # The plugin's own server entries, run as their hosts run them, from a copy of the plugin under a path # with a space, which splits an unquoted path into two words. @@ -329,6 +424,8 @@ def main(): server = json.load(f)['mcpServers']['axiomcode'] bad += check('.codex-plugin/mcp.json', [server['command'], *server['args']], repo, base, os.path.join(plugin, server.get('cwd', '.'))) + bad += check_failures('.codex-plugin/mcp.json', [server['command'], *server['args']], repo, base, + os.path.join(plugin, server.get('cwd', '.'))) # Cursor's own manifest names the server with ${CURSOR_PLUGIN_ROOT}, which Cursor replaces with the # plugin directory before it starts the command, and starts it in the user's project. @@ -338,6 +435,7 @@ def main(): cmd = [server['command'], *map(expand, server['args'])] env = dict(base, **{k: expand(v) for k, v in server.get('env', {}).items()}) bad += check('.cursor-plugin/plugin.json', cmd, repo, env, repo) + bad += check_failures('.cursor-plugin/plugin.json', cmd, repo, env, repo) with open(os.path.join(plugin, '.mcp.json')) as f: server = json.load(f)['mcpServers']['axiomcode'] @@ -345,6 +443,10 @@ def main(): cmd = [server['command'], *map(expand, server['args'])] env = dict(base, **{k: expand(v) for k, v in server.get('env', {}).items()}) bad += check('.mcp.json', cmd, repo, env, repo) + bad += check_failures('.mcp.json', cmd, repo, env, repo) + if py: # Claude Code's entry with the SDK's interpreter first in the launcher's search + sdk_env = dict(env, AXIOMCODE_PYTHON=py, PATH=os.path.dirname(py) + os.pathsep + env.get('PATH', '')) + bad += check_failures('.mcp.json (SDK python)', cmd, repo, sdk_env, repo, sdk=True) # A uv on PATH that cannot build its environment is passed over for the fallback, rather than started # and left to exit before `initialize` (#1249). python3 and python are shadowed by interpreters without