From 6453aa5f99e12de900e2de46561d19bd2325bfc4 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 9 Oct 2026 08:35:33 -0700 Subject: [PATCH 1/4] typescript: a JSX attribute hands its function to the element, and a class component tag hands over render and its lifecycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A function written inside a JSX `{…}` — `onPointerMove={this.handleMove}`, `onClick={onSave}`, `renderItem={drawRow}` — was reached by nothing. The parser writes each `{…}` as an expression root of its own, linked to no element (an intrinsic tag has no call site at all), so no hand-over rule saw it, and a handler read off a field or named by reference was cut off from every test that renders the component and fires the event. The `{…}` is now the site that hands the function over, from the function it is written in (callback_registered, as for `xs.map(cb)`); what the value is read through is followed exactly as for an argument. A call there hands over what it returns and stays out. A tag that builds a class component resolved only to its constructor, so render and the lifecycle methods the renderer calls (componentDidMount, componentWillUnmount, …) had no caller, and nor did anything below them. The tag now hands them over too, for a class that has a render method. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../expected/74-jsx-component-forms.edges | 1 + .../expected/80-vue-definecomponent-jsx.edges | 1 + .../80-vue-definecomponent-jsx.lib.edges | 1 + .../engine/call-edge-generation/call_chain.dl | 8 ++ .../engine/resolution/value-flow.dl | 63 +++++++++ graph/typescript/souffle/decls_all.dl | 6 + .../typescript/jsx-handed-functions/case.json | 123 ++++++++++++++++++ .../jsx-handed-functions/src/board.tsx | 49 +++++++ .../jsx-handed-functions/src/jsx.d.ts | 13 ++ .../jsx-handed-functions/src/work.ts | 7 + .../jsx-handed-functions/tsconfig.json | 1 + 11 files changed, 273 insertions(+) create mode 100644 tests/cases/typescript/jsx-handed-functions/case.json create mode 100644 tests/cases/typescript/jsx-handed-functions/src/board.tsx create mode 100644 tests/cases/typescript/jsx-handed-functions/src/jsx.d.ts create mode 100644 tests/cases/typescript/jsx-handed-functions/src/work.ts create mode 100644 tests/cases/typescript/jsx-handed-functions/tsconfig.json diff --git a/graph/test/typescript/expected/74-jsx-component-forms.edges b/graph/test/typescript/expected/74-jsx-component-forms.edges index 8a9a7700..1215ed2b 100644 --- a/graph/test/typescript/expected/74-jsx-component-forms.edges +++ b/graph/test/typescript/expected/74-jsx-component-forms.edges @@ -10,6 +10,7 @@ callback_registered FUNCTION_CALL app#() @L4 -> app#() callback_registered FUNCTION_CALL parts#() @L33 -> parts#UserCard({ id: string }) callback_registered FUNCTION_CALL parts#() @L41 -> parts#FieldImpl({ label: string },unknown) callback_registered FUNCTION_CALL parts#() @L73 -> parts#buildDefault() +callback_registered JSX_COMPONENT_CALL app#App() @L13 -> Panel#render() intrinsic_terminal DYNAMIC_IMPORT_CALL app#() @L4 -> - known_edge FUNCTION_CALL app#useFactory() @L23 -> parts#()@L22 known_edge FUNCTION_CALL parts#FieldImpl({ label: string },unknown) @L39 -> parts#trackClick(string) diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges b/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges index 332f0630..15c58497 100644 --- a/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges @@ -14,6 +14,7 @@ ambiguous_unknown FUNCTION_CALL parts#render() @L35 -> - ambiguous_unknown FUNCTION_CALL reg#() @L18 -> - ambiguous_unknown JSX_COMPONENT_CALL app#() @L20 -> - callback_registered FUNCTION_CALL parts#() @L40 -> parts#(?) +callback_registered JSX_COMPONENT_CALL app#() @L? -> app#onPing() known_edge FUNCTION_CALL app#onPing() @L12 -> app#helper() known_edge FUNCTION_CALL button#() @L10 -> button#leafButton() known_edge FUNCTION_CALL card#render() @L12 -> card#leafCard() diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges index 40aed3c8..92c54f4d 100644 --- a/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges +++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges @@ -14,6 +14,7 @@ boundary_lib FUNCTION_CALL parts#() @L42 -> vue#defineNuxtComponent boundary_lib FUNCTION_CALL parts#() @L49 -> vue#makeStore(ComponentOptions) boundary_lib FUNCTION_CALL reg#() @L18 -> vue#defineComponent(ComponentOptions) callback_registered FUNCTION_CALL parts#() @L40 -> parts#(?) +callback_registered JSX_COMPONENT_CALL app#() @L? -> app#onPing() known_edge FUNCTION_CALL app#onPing() @L12 -> app#helper() known_edge FUNCTION_CALL button#() @L10 -> button#leafButton() known_edge FUNCTION_CALL card#render() @L12 -> card#leafCard() diff --git a/graph/typescript/engine/call-edge-generation/call_chain.dl b/graph/typescript/engine/call-edge-generation/call_chain.dl index 56518799..890a08ad 100644 --- a/graph/typescript/engine/call-edge-generation/call_chain.dl +++ b/graph/typescript/engine/call-edge-generation/call_chain.dl @@ -134,6 +134,14 @@ call_chain_edge(ce, caller, "-", m, "client", "callback_registered", kind) :- caller != m, invocation_site(ce, kind). +// …and a function written inside a JSX `{…}` (resolution/value-flow.dl, jsx_handed_value). The `{…}` is +// the site: an intrinsic tag has no call site of its own, and the expression positions the edge. +call_chain_edge(r, caller, "-", m, "client", "callback_registered", "JSX_COMPONENT_CALL") :- + jsx_handed_value(r, m), + method_prov(m, "client"), + jsx_root_caller(r, caller), + caller != m. + // ── call_runs_edge(FromMethod, ToMethod, Prov) ────────────────────────────── // The chain a consumer should WALK: overload signatures re-pointed to the body that // actually executes (resolution/overload-sets.dl). Kept separate from diff --git a/graph/typescript/engine/resolution/value-flow.dl b/graph/typescript/engine/resolution/value-flow.dl index 75e9d129..6007a400 100644 --- a/graph/typescript/engine/resolution/value-flow.dl +++ b/graph/typescript/engine/resolution/value-flow.dl @@ -294,6 +294,69 @@ proxy_trap_name("preventExtensions"). proxy_trap_name("apply"). proxy_trap_name("construct"). +// …AND A FUNCTION WRITTEN AS A JSX ATTRIBUTE OR CHILD. ``, +// ` + + ) + } +} + +export function mountBoard(): unknown { + return +} + +// a function component handing a module function and a render callback to elements +export function Toolbar(): unknown { + return ( + + ) +} + +export function List(props: { renderItem: (i: number) => string }): unknown { + return
    {props.renderItem.length}
+} + +// CONTROL: a class with a render method that is never written as a tag is not a component: building it runs no render +export class Report { + render(): number { return measure(1) } +} +export function makeReport(): Report { + return new Report() +} + +// CONTROL: an element whose attributes are data hands nothing over +export function Badge(): unknown { + return +} diff --git a/tests/cases/typescript/jsx-handed-functions/src/jsx.d.ts b/tests/cases/typescript/jsx-handed-functions/src/jsx.d.ts new file mode 100644 index 00000000..e5c8fd2d --- /dev/null +++ b/tests/cases/typescript/jsx-handed-functions/src/jsx.d.ts @@ -0,0 +1,13 @@ +// the project supplies its own JSX namespace and component base, so the case needs no framework installed +declare global { + namespace JSX { + type Element = unknown + interface ElementClass { render(): unknown } + interface ElementAttributesProperty { props: unknown } + interface IntrinsicElements { [name: string]: unknown } + } +} +export class Component

{ + props: P + constructor(props: P) { this.props = props } +} diff --git a/tests/cases/typescript/jsx-handed-functions/src/work.ts b/tests/cases/typescript/jsx-handed-functions/src/work.ts new file mode 100644 index 00000000..46ad4ace --- /dev/null +++ b/tests/cases/typescript/jsx-handed-functions/src/work.ts @@ -0,0 +1,7 @@ +export function hitTest(x: number): number { return x * 2 } +export function persist(id: string): string { return id.trim() } +export function startClock(): number { return Date.now() } +export function stopClock(): number { return 0 } +export function onSave(): string { return "saved" } +export function drawRow(i: number): string { return String(i) } +export function measure(n: number): number { return n + 1 } diff --git a/tests/cases/typescript/jsx-handed-functions/tsconfig.json b/tests/cases/typescript/jsx-handed-functions/tsconfig.json new file mode 100644 index 00000000..f75ffe5b --- /dev/null +++ b/tests/cases/typescript/jsx-handed-functions/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "jsx": "preserve", "strict": true } } From b2e7cda46558e2427335c50421fdfa98f0e1ea9d Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:09:51 -0700 Subject: [PATCH 2/4] typescript: a call through an imported const runs the function it holds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A package's public functions are often its default instance's methods exported as consts: `export const createDraft = immer.createDraft.bind(immer)`, `export const plain = engine.plain`. The holder rules already knew what such a const holds (a method read as a value, through `bind`), and a call to it in the defining module ran that method. A call from any other module names the const through an import, and only a local variable, a parameter or a field was followed — so every caller outside the defining module, its whole test suite included, ran nothing. A call whose callee is an imported const, or a namespace import's member (`api.applyPatches(…)`), now runs what that module's own variable holds. The import is followed to its module, never matched by name: the same exported name bound in another module runs that module's method. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../engine/resolution/value-flow.dl | 19 +++ graph/typescript/souffle/decls_all.dl | 1 + .../typescript/bound-method-export/case.json | 110 ++++++++++++++++++ .../bound-method-export/src/engine.ts | 25 ++++ .../bound-method-export/src/other.ts | 8 ++ .../typescript/bound-method-export/src/use.ts | 17 +++ 6 files changed, 180 insertions(+) create mode 100644 tests/cases/typescript/bound-method-export/case.json create mode 100644 tests/cases/typescript/bound-method-export/src/engine.ts create mode 100644 tests/cases/typescript/bound-method-export/src/other.ts create mode 100644 tests/cases/typescript/bound-method-export/src/use.ts diff --git a/graph/typescript/engine/resolution/value-flow.dl b/graph/typescript/engine/resolution/value-flow.dl index 6007a400..284c6e88 100644 --- a/graph/typescript/engine/resolution/value-flow.dl +++ b/graph/typescript/engine/resolution/value-flow.dl @@ -618,6 +618,20 @@ call_runs(ce, m) :- callee_branch(ce, x), call_runs(ce, m) :- callee_branch(ce, x), ts_field_access_target(x, f), holder_holds_function(f, m). +// A CALL THROUGH AN IMPORTED HOLDER. A package's public functions are often its default instance's methods, +// exported as consts: `export const createDraft = immer.createDraft.bind(immer)`, `export const plain = +// engine.plain`. The holder rules know what the const holds (method_value follows the read and `bind`), but a +// call names the const through an import, and the two clauses above take only a local variable, a parameter +// or a field: every caller outside the defining module — its whole test suite — ran nothing. expr_holder +// follows the import, and a namespace member, to the exporting module's own variable, never by name. +call_runs(ce, m) :- call_callee_expr(_, ce, x), + expr_referenced("client", "IMPORT_BINDING", _, x), + expr_holder(x, h), + holder_holds_function(h, m). +call_runs(ce, m) :- call_callee_expr(_, ce, x), + expr_namespace_member(x), + expr_holder(x, h), + holder_holds_function(h, m). // ============================================================================ // A FUNCTION ASSIGNED TO AN INTERFACE'S METHOD MEMBER IMPLEMENTS IT (#1283) @@ -719,6 +733,11 @@ expr_holder(x, f) :- ts_field_access_target(x, f). // an imported `const` is the exporting module's variable expr_holder(x, v) :- expr_referenced("client", "IMPORT_BINDING", ih, x), import_binds(ih, _, "VARIABLE", v). +// …and so is one read off a namespace import, `api.createDraft` +expr_holder(x, v) :- property_access_recv(x, n, q), + expr_namespace(q, _, mod), + module_export(_, mod, n, "VARIABLE", v). +expr_namespace_member(x) :- property_access_recv(x, _, q), expr_namespace(q, _, _). // ── holder_elem_fn(Holder, Fn): an element of the holder's collection is, or holds, Fn ── holder_elem_fn(h, m) :- holder_elem_value(h, x), diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl index 005be814..0b2b39af 100644 --- a/graph/typescript/souffle/decls_all.dl +++ b/graph/typescript/souffle/decls_all.dl @@ -220,6 +220,7 @@ .decl collection_receiver(c0:symbol) .decl keyed_collection_ref(c0:symbol) .decl handed_function(c0:symbol,c1:symbol) +.decl expr_namespace_member(c0:symbol) .decl jsx_value_root(c0:symbol) .decl jsx_handed_value(c0:symbol,c1:symbol) .decl jsx_root_caller(c0:symbol,c1:symbol) diff --git a/tests/cases/typescript/bound-method-export/case.json b/tests/cases/typescript/bound-method-export/case.json new file mode 100644 index 00000000..bc38818e --- /dev/null +++ b/tests/cases/typescript/bound-method-export/case.json @@ -0,0 +1,110 @@ +{ + "lang": "typescript", + "src": "src", + "checks": [ + { + "why": "a const bound to an instance method (`engine.createDraft.bind(engine)`) runs that method: a call through it in the same module reaches the method's body", + "run": [ + "path", + "localUse", + "draftStep" + ], + "want": [ + "Engine.createDraft", + "draftStep" + ], + "avoid": [ + "no chain of resolved calls" + ] + }, + { + "why": "\u2026and through a named import of the const, from another module", + "run": [ + "path", + "draftIt", + "draftStep" + ], + "want": [ + "Engine.createDraft", + "draftStep" + ], + "avoid": [ + "no chain of resolved calls" + ] + }, + { + "why": "\u2026and through a namespace import", + "run": [ + "path", + "patchIt", + "patchStep" + ], + "want": [ + "Engine.applyPatches", + "patchStep" + ], + "avoid": [ + "no chain of resolved calls" + ] + }, + { + "why": "a method read off the instance with no bind is the same method", + "run": [ + "path", + "plainIt", + "plainStep" + ], + "want": [ + "Engine.plain", + "plainStep" + ], + "avoid": [ + "no chain of resolved calls" + ] + }, + { + "why": "impact on the method lists the importing caller", + "run": [ + "impact", + "Engine.createDraft" + ], + "want": [ + "draftIt", + "localUse" + ], + "avoid": [ + "nothing calls" + ] + }, + { + "why": "CONTROL: the same exported name bound in another module runs that module's method: the import is followed to its own module's const, never by name", + "run": [ + "path", + "otherIt", + "draftStep" + ], + "want": [ + "no chain of resolved calls" + ], + "avoid": [ + "Engine.createDraft" + ], + "expect_error": true + }, + { + "why": "CONTROL, the positive side: the other module's call reaches its own method", + "run": [ + "path", + "otherIt", + "otherStep" + ], + "want": [ + "Other.createDraft", + "otherStep" + ], + "avoid": [ + "no chain of resolved calls" + ] + } + ] +} \ No newline at end of file diff --git a/tests/cases/typescript/bound-method-export/src/engine.ts b/tests/cases/typescript/bound-method-export/src/engine.ts new file mode 100644 index 00000000..81605ed7 --- /dev/null +++ b/tests/cases/typescript/bound-method-export/src/engine.ts @@ -0,0 +1,25 @@ +export function draftStep(x: number): number { return x + 1 } +export function patchStep(x: number): number { return x - 1 } +export function freezeStep(x: boolean): boolean { return !x } +export function plainStep(x: number): number { return x * 3 } + +export class Engine { + createDraft(x: number): number { return draftStep(x) } + applyPatches(x: number): number { return patchStep(x) } + setFreeze(x: boolean): boolean { return freezeStep(x) } + plain(x: number): number { return plainStep(x) } +} + +const engine = new Engine() + +// the package's public functions are the default instance's methods, bound to it +export const createDraft = /* @__PURE__ */ engine.createDraft.bind(engine) +export const applyPatches = engine.applyPatches.bind(engine) +export let setFreeze = engine.setFreeze.bind(engine) +// read off the instance with no bind +export const plain = engine.plain + +// a call in the same module, through the bound const +export function localUse(): number { + return createDraft(1) +} diff --git a/tests/cases/typescript/bound-method-export/src/other.ts b/tests/cases/typescript/bound-method-export/src/other.ts new file mode 100644 index 00000000..6144da95 --- /dev/null +++ b/tests/cases/typescript/bound-method-export/src/other.ts @@ -0,0 +1,8 @@ +export function otherStep(x: number): number { return x * 5 } + +export class Other { + createDraft(x: number): number { return otherStep(x) } +} +const other = new Other() +// the same exported name, bound to a different instance's method +export const createDraft = other.createDraft.bind(other) diff --git a/tests/cases/typescript/bound-method-export/src/use.ts b/tests/cases/typescript/bound-method-export/src/use.ts new file mode 100644 index 00000000..a5fdc992 --- /dev/null +++ b/tests/cases/typescript/bound-method-export/src/use.ts @@ -0,0 +1,17 @@ +import { createDraft, plain } from "./engine" +import { createDraft as otherDraft } from "./other" +import * as api from "./engine" + +export function draftIt(): number { + return createDraft(2) +} +export function patchIt(): number { + return api.applyPatches(3) +} +export function plainIt(): number { + return plain(4) +} +// CONTROL: the same exported name from another module runs that module's method, not this one's +export function otherIt(): number { + return otherDraft(6) +} From 5c2130a51eb603eb91c556db12d8318037e10eb5 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:42:57 -0700 Subject: [PATCH 3/4] typescript: a call on the platform's own objects is no by-name caller of a project method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With no standard library staged, `JSON.parse(s)`, `Promise.resolve(v).then(f)`, `Object.keys(o).forEach(g)`, `Reflect.getMetadata(k, t)`, `document.createElement('a')` and `console.error(e)` resolve to nothing, so every one was an untyped `x.parse()` to impact: a project method that happens to be named parse, resolve, all, keys, error or getMetadata got every such line in the repository as a by-name caller, and every test around those lines entered its closure. The engine now traces the receiver to the platform's global object (an AMBIENT_GLOBAL reference to JSON, Promise, Object, Reflect, Array, document, console, …), and what calling it returns, the way it already traces a package's value: the site stays unresolved, is listed apart as a call on a package's or the platform's value, and seeds no closure. A program that declares one of those names itself binds it as its own variable and is unaffected; window, globalThis and self are left out, since a program may hang its own functions on them. An untyped receiver stays a by-name caller. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../engine/call-edge-generation/calls.dl | 37 +++++++++++++++++++ graph/typescript/souffle/decls_all.dl | 1 + .../skills/axiomcode/scripts/ax_edges.py | 2 +- .../skills/axiomcode/scripts/dl/impact.dl | 2 +- .../case.json | 18 +++++++++ .../src/any.test.ts | 4 ++ .../src/app.ts | 17 +++++++++ .../src/config.test.ts | 4 ++ .../src/parser.ts | 7 ++++ 9 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 tests/cases/typescript/builtin-receiver-is-not-a-caller/case.json create mode 100644 tests/cases/typescript/builtin-receiver-is-not-a-caller/src/any.test.ts create mode 100644 tests/cases/typescript/builtin-receiver-is-not-a-caller/src/app.ts create mode 100644 tests/cases/typescript/builtin-receiver-is-not-a-caller/src/config.test.ts create mode 100644 tests/cases/typescript/builtin-receiver-is-not-a-caller/src/parser.ts diff --git a/graph/typescript/engine/call-edge-generation/calls.dl b/graph/typescript/engine/call-edge-generation/calls.dl index f90275c3..b4b87c8f 100644 --- a/graph/typescript/engine/call-edge-generation/calls.dl +++ b/graph/typescript/engine/call-edge-generation/calls.dl @@ -151,5 +151,42 @@ library_value(e, spec) :- expr_kind("client", k, _, e), library_value(e, spec) :- expr_referenced("client", "VARIABLE", v, e), var_initializer("client", _, i, v), !var_reassigned(v), library_value(i, spec). call_passes_project_instance(e) :- expr_child("client", e, "ARGUMENT", _, a), expr_type(a, "client", _). +// …and the runtime's own objects: `JSON.parse(s)`, `Promise.resolve(v).then(f)`, `Object.keys(o).forEach(g)`, +// `document.createElement('a')`, `console.error(e)`. With no standard library staged the name resolves to nothing +// in the run (AMBIENT_GLOBAL), so every such call was an untyped `x.parse()` to the consumers, and a project method +// that happens to be named parse, resolve, all, keys, error or getMetadata was given every one of them as a by-name +// caller — and every test around them. The receiver is the platform's, never the project's: a program that declares +// the name itself binds it as its own variable, not as an ambient global, and stays out. Only the objects whose +// members are called through the name; `window`, `globalThis` and `self` are left out, since a program may hang its +// own functions on them. +library_value(e, n) :- expr_referenced("client", "AMBIENT_GLOBAL", _, e), expr_name("client", n, e), + builtin_global_object(n). +builtin_global_object("JSON"). +builtin_global_object("Math"). +builtin_global_object("Object"). +builtin_global_object("Reflect"). +builtin_global_object("Promise"). +builtin_global_object("Array"). +builtin_global_object("Number"). +builtin_global_object("String"). +builtin_global_object("Boolean"). +builtin_global_object("BigInt"). +builtin_global_object("Symbol"). +builtin_global_object("Date"). +builtin_global_object("RegExp"). +builtin_global_object("Error"). +builtin_global_object("Intl"). +builtin_global_object("Atomics"). +builtin_global_object("ArrayBuffer"). +builtin_global_object("console"). +builtin_global_object("process"). +builtin_global_object("Buffer"). +builtin_global_object("document"). +builtin_global_object("navigator"). +builtin_global_object("localStorage"). +builtin_global_object("sessionStorage"). +builtin_global_object("performance"). +builtin_global_object("crypto"). +builtin_global_object("URL"). library_receiver(ce, spec) :- call_site("client", _, _, rk, recv, ce, _), receiver_kind_is_value(rk), !call_resolved(ce), library_value(recv, spec). diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl index 0b2b39af..500217c8 100644 --- a/graph/typescript/souffle/decls_all.dl +++ b/graph/typescript/souffle/decls_all.dl @@ -619,6 +619,7 @@ .decl unresolved_covered(c0:symbol) .decl package_import(c0:symbol,c1:symbol) .decl library_value(c0:symbol,c1:symbol) +.decl builtin_global_object(c0:symbol) .decl call_passes_project_instance(c0:symbol) .decl library_receiver(c0:symbol,c1:symbol) .decl unresolved_receiver(c0:symbol,c1:symbol,c2:symbol,c3:symbol) diff --git a/plugins/axiomcode/skills/axiomcode/scripts/ax_edges.py b/plugins/axiomcode/skills/axiomcode/scripts/ax_edges.py index f5283b76..533319b3 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/ax_edges.py +++ b/plugins/axiomcode/skills/axiomcode/scripts/ax_edges.py @@ -503,7 +503,7 @@ def nearest(cands): # the candidates enclosing no other candidat # unknown, but a project `get` is not what it calls. Like a stub it keeps a row, apart from the untyped-receiver name # matches, and it seeds no closure: listed among them it filled the first page of `impact Repo.get`. LIBRARY_RECEIVER_KIND = 'library' -LIBRARY_BYNAME_WHY = 'calls a method of this name on a value a package returned or constructed: not this method, unless the package hands it back' +LIBRARY_BYNAME_WHY = 'calls a method of this name on a value a package or the platform (JSON, Promise, document …) returned or constructed: not this method, unless that code hands it back' def library_receiver_sites(q): diff --git a/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl b/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl index f8d70e36..7910a4f8 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl +++ b/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl @@ -424,7 +424,7 @@ direct(q, c, "uses", "calls a method of this name (receiver not typed)", "by nam direct(q, c, "uses", "stubs a method of this name on a mock (receiver not typed): the real method does not run there", "by name", f, l) :- target(q, "method", m, _), named(n, m), unresolved(c, n, "stub", f, l), !is_target_decl(q, c). // A site whose receiver the engine traced to a package's value (`request(app).get('/x')`, ax_edges.library_receiver_sites): // the kind "library". A row of its own, listed after the untyped-receiver matches, and no seed of the closure. -direct(q, c, "uses", "calls a method of this name on a value a package returned or constructed: not this method, unless the package hands it back", "by name", f, l) :- target(q, "method", m, _), named(n, m), unresolved(c, n, "library", f, l), !is_target_decl(q, c). +direct(q, c, "uses", "calls a method of this name on a value a package or the platform (JSON, Promise, document …) returned or constructed: not this method, unless that code hands it back", "by name", f, l) :- target(q, "method", m, _), named(n, m), unresolved(c, n, "library", f, l), !is_target_decl(q, c). // ── a method handed over as a VALUE, never called where it is named ────────────────────────────────────────── // `app.get('/orders/:id', getOrder)`, `useEffect(load)`, `items.map(format)`, `handlers = {"x": handle_x}`: the diff --git a/tests/cases/typescript/builtin-receiver-is-not-a-caller/case.json b/tests/cases/typescript/builtin-receiver-is-not-a-caller/case.json new file mode 100644 index 00000000..1f010be6 --- /dev/null +++ b/tests/cases/typescript/builtin-receiver-is-not-a-caller/case.json @@ -0,0 +1,18 @@ +{"lang": "typescript", "src": "src", + "checks": [ + {"why": "JSON.parse is the platform's: the function that writes it is listed apart as a call on the platform's value, not as a by-name caller of the project's parse, and the test around it is not a test of that method", + "run": ["impact", "FrameParser.parse", "--tests"], + "want": ["readAny", "any.test.ts", "a value a package or the platform"], + "avoid": ["config.test.ts"]}, + {"why": "what a built-in call returns is the platform's too: Object.keys(o).forEach is the array's forEach, so the project's forEach has no by-name caller", + "run": ["impact", "FrameParser.forEach"], + "want": ["a value a package or the platform"], + "avoid": ["calls a method of this name (receiver not typed)"]}, + {"why": "Promise.resolve is the platform's: the project's Queue.resolve keeps only its untyped caller, and only that caller's test", + "run": ["impact", "Queue.resolve", "--tests"], + "want": ["drainAny", "any.test.ts"], + "avoid": ["config.test.ts"]}, + {"why": "CONTROL: an untyped receiver is still a by-name caller, and its test is still selected", + "run": ["impact", "FrameParser.parse", "--tests"], + "want": ["calls a method of this name (receiver not typed)", "any.test.ts"]} + ]} diff --git a/tests/cases/typescript/builtin-receiver-is-not-a-caller/src/any.test.ts b/tests/cases/typescript/builtin-receiver-is-not-a-caller/src/any.test.ts new file mode 100644 index 00000000..0796c624 --- /dev/null +++ b/tests/cases/typescript/builtin-receiver-is-not-a-caller/src/any.test.ts @@ -0,0 +1,4 @@ +import { readAny, drainAny } from "./app" +declare function it(name: string, fn: () => void): void +it("reads through anything", () => { readAny({ parse: () => 1 }, "x") }) +it("drains anything", () => { drainAny({ resolve: () => "y" }) }) diff --git a/tests/cases/typescript/builtin-receiver-is-not-a-caller/src/app.ts b/tests/cases/typescript/builtin-receiver-is-not-a-caller/src/app.ts new file mode 100644 index 00000000..0584ce98 --- /dev/null +++ b/tests/cases/typescript/builtin-receiver-is-not-a-caller/src/app.ts @@ -0,0 +1,17 @@ +// the runtime's own objects: none of these calls can land on the project's parse, forEach or resolve +export function readConfig(raw: string): unknown { + return JSON.parse(raw) +} +export function listKeys(o: object): void { + Object.keys(o).forEach((k) => console.log(k)) +} +export function later(v: number): Promise { + return Promise.resolve(v).then((x) => x + 1) +} +// CONTROL: a receiver the engine cannot type may be a FrameParser: still a by-name caller +export function readAny(p: any, raw: string): unknown { + return p.parse(raw) +} +export function drainAny(q: any): unknown { + return q.resolve("x") +} diff --git a/tests/cases/typescript/builtin-receiver-is-not-a-caller/src/config.test.ts b/tests/cases/typescript/builtin-receiver-is-not-a-caller/src/config.test.ts new file mode 100644 index 00000000..0df97955 --- /dev/null +++ b/tests/cases/typescript/builtin-receiver-is-not-a-caller/src/config.test.ts @@ -0,0 +1,4 @@ +import { readConfig, later } from "./app" +declare function it(name: string, fn: () => void): void +it("reads the config", () => { readConfig("{}") }) +it("waits", () => { later(1) }) diff --git a/tests/cases/typescript/builtin-receiver-is-not-a-caller/src/parser.ts b/tests/cases/typescript/builtin-receiver-is-not-a-caller/src/parser.ts new file mode 100644 index 00000000..1ef5aeb3 --- /dev/null +++ b/tests/cases/typescript/builtin-receiver-is-not-a-caller/src/parser.ts @@ -0,0 +1,7 @@ +export class FrameParser { + parse(text: string): number { return text.length } + forEach(f: (n: number) => void): void { f(1) } +} +export class Queue { + resolve(id: string): string { return id } +} From 21c0f9fc0b1570c0e31b2d9c1dde25ad1ca65c3e Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:28:41 -0700 Subject: [PATCH 4/4] typescript: a list's element is typed through a type variable's constraint and an object-type alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `plugins.map((p) => p.setup(app))` left `p` with nothing to dispatch on in two common shapes: - the element is an object type written as an alias (`type Plugin = { setup(): … }`): the no-lib callback binding gave the parameter the element's TYPE only, and an alias of an object type has no type declaration, only members — so every call on it was unresolved, while an annotated parameter of the same type resolved through its shape. The callback parameter now takes the element's shape too. - the list is typed by a type variable constrained to an array or tuple (`(...modules: Ms)`): its element had no type at all. The constraint is what every element is at least, as it already is for a `` receiver; the element is now read through it, for callbacks and for `for…of`, by the exact link to the type parameter or by its name in scope. A plugin/module system built this way — a builder that maps over its modules and calls `init` — was reached by none of the tests that drive it. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../engine/resolution/contextual-params.dl | 21 +++ .../engine/resolution/type-resolution.dl | 13 ++ .../generic-list-element-type/case.json | 128 ++++++++++++++++++ .../generic-list-element-type/src/modules.ts | 31 +++++ .../generic-list-element-type/src/plugins.ts | 21 +++ .../generic-list-element-type/tsconfig.json | 1 + 6 files changed, 215 insertions(+) create mode 100644 tests/cases/typescript/generic-list-element-type/case.json create mode 100644 tests/cases/typescript/generic-list-element-type/src/modules.ts create mode 100644 tests/cases/typescript/generic-list-element-type/src/plugins.ts create mode 100644 tests/cases/typescript/generic-list-element-type/tsconfig.json diff --git a/graph/typescript/engine/resolution/contextual-params.dl b/graph/typescript/engine/resolution/contextual-params.dl index c9f675e1..99b05b5a 100644 --- a/graph/typescript/engine/resolution/contextual-params.dl +++ b/graph/typescript/engine/resolution/contextual-params.dl @@ -278,6 +278,20 @@ builtin_elem_ref(parent, x) :- ref_is_readonly_operator(parent), builtin_elem_ref(child, x). builtin_elem_ref(ref, x) :- ref_via_alias(ref, rhs), builtin_elem_ref(rhs, x). +// A tuple's elements are its members: `[Module, ...Module[]]`. +builtin_elem_ref(ref, child) :- type_ref(rp, "TUPLE", _, _, _, _, _, ref), + type_ref_parent(rp, ref, _, child). +// A TYPE VARIABLE constrained to a list holds that list's elements: `(ms: Ms)`, +// `(...modules: Ms)`, then `modules.map((m) => m.init())`. The +// constraint is what every element is at least, as for a `` receiver. Both routes +// ref_type_target takes: the exact link to the declaration, and the name in scope where it is absent. +builtin_elem_ref(ref, x) :- type_ref_type_param(rp, tp, ref), + type_param_constraint(rp, cref, tp), + builtin_elem_ref(cref, x). +builtin_elem_ref(ref, x) :- type_ref(_, "TYPE_VARIABLE", _, tn, _, owner, _, ref), + !type_ref_type_param(_, _, ref), + type_var_constraint_in_scope(owner, tn, cref), + builtin_elem_ref(cref, x). builtin_elem_ref(ref, a) :- ref_via_alias(ref, rhs), builtin_elem_ref(rhs, x), type_ref(_, "TYPE_VARIABLE", _, n, _, _, _, x), @@ -339,6 +353,13 @@ lambda_param_ref(lp, c) :- call_site("client", _, "then", _, recv, ce, _), !param_type_ref(_, _, lp). lambda_param_type(lp, prov, t) :- lambda_param_ref(lp, c), ref_type_target(c, prov, t). +// …and its SHAPE, where the element is an object type written as a type alias or a literal rather +// than a class or an interface: `type Plugin = { init(): void }`, then `plugins.map((p) => p.init())`. +// The element has no type declaration to dispatch on, only members, so the type clause above gave +// the parameter nothing and every call on it was unresolved; an annotated parameter of the same type +// resolved, through param_shape_target. The element reference is the same one, read as a shape. +lambda_param_shape(lp, sh) :- lambda_param_ref(lp, c), + ref_shape_target(c, sh). // The parameter's reference is its declared reference, as an annotation's would be. expr_decl_ref(e, c) :- expr_referenced("client", "PARAMETER", p, e), lambda_param_ref(p, c). diff --git a/graph/typescript/engine/resolution/type-resolution.dl b/graph/typescript/engine/resolution/type-resolution.dl index 2c8e5ef2..0d3cff9f 100644 --- a/graph/typescript/engine/resolution/type-resolution.dl +++ b/graph/typescript/engine/resolution/type-resolution.dl @@ -587,6 +587,19 @@ ref_element_target(ref, prov, t) :- type_ref(rp, k, _, _, _, _, _, ref), ref_kind_is_array(k), type_ref_parent(rp, ref, _, child), ref_type_target(child, prov, t). +// …and of a TYPE VARIABLE constrained to an array or a tuple: `(ms: Ms)`, +// `(...modules: Ms)`, then `modules.map((m) => m.init())`. The +// constraint is what every element is at least, exactly as it is for a `` receiver +// (ref_type_target above), so the callback's parameter is typed by the constraint's element. Without it +// the element of a generic list had no type, and every call on it was unresolved. Same two routes as +// ref_type_target: the exact link to the declaration, and the name in scope where the link is absent. +ref_element_target(ref, prov, t) :- type_ref_type_param(rp, tp, ref), + type_param_constraint(rp, cref, tp), + ref_element_target(cref, prov, t). +ref_element_target(ref, prov, t) :- type_ref(_, "TYPE_VARIABLE", _, tn, _, owner, _, ref), + !type_ref_type_param(_, _, ref), + type_var_constraint_in_scope(owner, tn, cref), + ref_element_target(cref, prov, t). // ── ref_element_shape(Ref, Shape) — an element that is ITSELF a function type ── // `fns: readonly ((data: T) => boolean)[]` then `fns.every((fn) => fn(1))`. The element diff --git a/tests/cases/typescript/generic-list-element-type/case.json b/tests/cases/typescript/generic-list-element-type/case.json new file mode 100644 index 00000000..76b81bfb --- /dev/null +++ b/tests/cases/typescript/generic-list-element-type/case.json @@ -0,0 +1,128 @@ +{ + "lang": "typescript", + "src": "src", + "checks": [ + { + "why": "a list typed by `Ms extends Module[]` holds Modules: a call on its element dispatches on Module, and reaches the implementations", + "run": [ + "path", + "buildAll", + "coreInit" + ], + "want": [ + "coreInit" + ], + "avoid": [ + "no chain of resolved calls" + ] + }, + { + "why": "\u2026and for a rest parameter typed by a non-empty tuple constraint", + "run": [ + "path", + "buildCreate", + "reactInit" + ], + "want": [ + "reactInit" + ], + "avoid": [ + "no chain of resolved calls" + ] + }, + { + "why": "\u2026and through `readonly`", + "run": [ + "path", + "each", + "coreInit" + ], + "want": [ + "coreInit" + ], + "avoid": [ + "no chain of resolved calls" + ] + }, + { + "why": "\u2026and a for-of over the same list: the binding is an element, typed by the constraint", + "run": [ + "path", + "loopAll", + "coreInit" + ], + "want": [ + "coreInit" + ], + "avoid": [ + "no chain of resolved calls" + ] + }, + { + "why": "a list of an object-type alias: the callback's parameter takes the element's shape, so a call on it dispatches to what implements that shape", + "run": [ + "path", + "install", + "routerSetup" + ], + "want": [ + "routerSetup" + ], + "avoid": [ + "no chain of resolved calls" + ] + }, + { + "why": "\u2026through a type variable constrained to a tuple of them, iterated in a returned closure", + "run": [ + "path", + "start", + "routerSetup" + ], + "want": [ + "routerSetup" + ], + "avoid": [ + "no chain of resolved calls" + ] + }, + { + "why": "CONTROL: a list of another shape with a member of the same name reaches its own implementation, not Plugin's", + "run": [ + "path", + "runHooks", + "routerSetup" + ], + "want": [ + "no chain of resolved calls" + ], + "expect_error": true + }, + { + "why": "CONTROL, the positive side: that list reaches its own shape's implementation", + "run": [ + "path", + "runHooks", + "storeSetup" + ], + "want": [ + "storeSetup" + ], + "avoid": [ + "no chain of resolved calls" + ] + }, + { + "why": "CONTROL: an element typed `unknown` by its constraint names no type: nothing is dispatched to Auditor.audit's implementation", + "run": [ + "path", + "loose", + "auditInit" + ], + "want": [ + "no chain of resolved calls" + ], + "expect_error": true + } + ] +} \ No newline at end of file diff --git a/tests/cases/typescript/generic-list-element-type/src/modules.ts b/tests/cases/typescript/generic-list-element-type/src/modules.ts new file mode 100644 index 00000000..6e377d46 --- /dev/null +++ b/tests/cases/typescript/generic-list-element-type/src/modules.ts @@ -0,0 +1,31 @@ +export interface Module { name: string; init(): number } +export function coreInit(): number { return 1 } +export function reactInit(): number { return 2 } +export function auditInit(): number { return 3 } + +export const coreModule = (): Module => ({ name: "core", init() { return coreInit() } }) +export const reactModule = (): Module => ({ name: "react", init() { return reactInit() } }) + +// a list typed by a type parameter constrained to an array, or to a non-empty tuple, then iterated +export function buildAll(modules: Ms): number[] { + return modules.map((m) => m.init()) +} +export function buildCreate(...modules: Ms): number[] { + return modules.map((m) => m.init()) +} +export function each(modules: Ms): void { + modules.forEach((m) => { m.init() }) +} + +export interface Auditor { audit(): number } +export const auditor = (): Auditor => ({ audit() { return auditInit() } }) +// CONTROL: an unconstrained type variable has no element type: the call on its element stays unresolved +export function loose(xs: Ms): unknown[] { + return xs.map((x) => (x as any).audit()) +} + +export function loopAll(modules: Ms): number { + let n = 0 + for (const m of modules) n += m.init() + return n +} diff --git a/tests/cases/typescript/generic-list-element-type/src/plugins.ts b/tests/cases/typescript/generic-list-element-type/src/plugins.ts new file mode 100644 index 00000000..4221175c --- /dev/null +++ b/tests/cases/typescript/generic-list-element-type/src/plugins.ts @@ -0,0 +1,21 @@ +// an object type written as a type alias: the element has members and no declaration to dispatch on +export type Plugin = { name: Name; setup(app: unknown): number } +export function routerSetup(): number { return 4 } +export function storeSetup(): number { return 5 } +export const routerPlugin = (): Plugin<"router"> => ({ name: "router", setup() { return routerSetup() } }) + +export function install(plugins: Plugin[]): number[] { + return plugins.map((p) => p.setup(null)) +} +// a generic list of them, iterated in a closure the builder returns +export function buildApp, ...Plugin[]]>(...plugins: Ps) { + return function start(app: unknown): number[] { + return plugins.map((p) => p.setup(app)) + } +} +// CONTROL: the element's shape is not a Plugin: its setup is not Plugin's, so nothing reaches routerSetup +export type Hook = { setup(): number } +export const storeHook = (): Hook => ({ setup() { return storeSetup() } }) +export function runHooks(hooks: Hook[]): number[] { + return hooks.map((h) => h.setup()) +} diff --git a/tests/cases/typescript/generic-list-element-type/tsconfig.json b/tests/cases/typescript/generic-list-element-type/tsconfig.json new file mode 100644 index 00000000..bfebedc5 --- /dev/null +++ b/tests/cases/typescript/generic-list-element-type/tsconfig.json @@ -0,0 +1 @@ +{ "compilerOptions": { "strict": true } }