feat(resolution): synthesize RTK Query hook→endpoint dispatch edges
Adds the RTK Query member of the dispatch-through-indirection family
(synthesizedBy:'rtk-query'). An RTK Query endpoint defined inside
`createApi({ endpoints })` and the `useGetXQuery`/`useUpdateYMutation` hook it
generates were both invisible to static extraction, so a `component →
useGetXQuery → getX → queryFn` flow had nothing to connect and explore
dead-ended on the API slice.
Extraction (tree-sitter.ts): mint a function node per endpoint — named by its
key, spanning the queryFn/query handler so its calls attribute — handling both
the `endpoints: build => ({...})` arrow and `endpoints(builder){ return {...} }`
method forms, with a bare-node fallback for factory handlers
(`queryFn: makeFn(url)`); and a function node per generated-hook binding from
`export const {...} = api`, carrying a sentinel signature.
Resolution (callback-synthesizer.ts): rtkQueryEdges bridges each generated-hook
node to its same-file endpoint by the naming convention (strip use + optional
Lazy + Query|Mutation, lowercase head). Component→hook is normal import/call
resolution; the hook→endpoint hop surfaces in explore as `dynamic: rtk query`.
Validated 100% precision (hooks == synth edges, 0 cross-file) on basetool (54),
minusx-metabase (11), shapeshift (13); 0 on the uwave-web control (no createApi
→ a complete no-op). The sentinel gate correctly ignores hand-written
look-alikes (shapeshift's useFoxyQuery is a real custom hook, never bridged).
Full suite green (1608); new __tests__/rtk-query-synthesizer.test.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7f970296cf
commit
e9f7422223
@@ -1867,11 +1867,68 @@ function objectRegistryEdges(ctx: ResolutionContext): Edge[] {
|
||||
return edges;
|
||||
}
|
||||
|
||||
// ── RTK Query generated-hook → endpoint ──────────────────────────────────────
|
||||
// RTK Query generates one `useGetXQuery`/`useUpdateYMutation` hook per endpoint
|
||||
// (`createApi({ endpoints: b => ({ getX: b.query(...) }) })`). Components call the
|
||||
// hook; the fetch logic lives in the endpoint's queryFn. The hook↔endpoint link is
|
||||
// pure NAMING CONVENTION (no static edge): strip `use` + the optional `Lazy`
|
||||
// variant + the `Query|Mutation` suffix, lowercase the head → the endpoint key.
|
||||
// Both are extracted as function nodes (the hook from its `export const {…}=api`
|
||||
// binding, carrying a sentinel signature; the endpoint from the createApi object),
|
||||
// so bridging hook→endpoint connects `component → useGetXQuery → getX → queryFn`.
|
||||
// Gated on the extraction sentinel so it only ever fires on genuinely-generated
|
||||
// hooks (never a hand-written `useFooQuery`), and on a SAME-FILE endpoint (RTK
|
||||
// colocates the hooks and their api in one module) — 0 on any non-RTK repo.
|
||||
const RTK_HOOK_DERIVE_RE = /^use([A-Z][A-Za-z0-9]*?)(?:Query|Mutation)$/;
|
||||
// MUST match the signature set in tree-sitter.ts `extractRtkHookBindings`.
|
||||
const RTK_GENERATED_HOOK_SIGNATURE = '= RTK Query generated hook';
|
||||
|
||||
/** Derive the endpoint key from a generated-hook name (`useLazyGetRecordsQuery`
|
||||
* → `getRecords`), or null if it doesn't fit the convention. */
|
||||
function rtkEndpointNameFromHook(hook: string): string | null {
|
||||
const m = RTK_HOOK_DERIVE_RE.exec(hook);
|
||||
if (!m) return null;
|
||||
let mid = m[1]!;
|
||||
if (mid.startsWith('Lazy')) mid = mid.slice(4); // useLazyGetX → getX (same endpoint)
|
||||
if (!mid) return null;
|
||||
return mid.charAt(0).toLowerCase() + mid.slice(1);
|
||||
}
|
||||
|
||||
function rtkQueryEdges(queries: QueryBuilder, ctx: ResolutionContext): Edge[] {
|
||||
const edges: Edge[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const hook of queries.iterateNodesByKind('function')) {
|
||||
// Only our extracted generated-hook bindings (sentinel) — not a real hook fn.
|
||||
if (hook.signature !== RTK_GENERATED_HOOK_SIGNATURE) continue;
|
||||
const endpointName = rtkEndpointNameFromHook(hook.name);
|
||||
if (!endpointName) continue;
|
||||
// The endpoint is a same-file function by the derived name (RTK colocates the
|
||||
// api definition and its generated-hook exports in one module).
|
||||
const target = ctx
|
||||
.getNodesByName(endpointName)
|
||||
.find((n) => n.kind === 'function' && n.filePath === hook.filePath);
|
||||
if (!target || target.id === hook.id) continue;
|
||||
const key = `${hook.id}>${target.id}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
edges.push({
|
||||
source: hook.id,
|
||||
target: target.id,
|
||||
kind: 'calls',
|
||||
line: hook.startLine,
|
||||
provenance: 'heuristic',
|
||||
metadata: { synthesizedBy: 'rtk-query', via: endpointName, registeredAt: `${hook.filePath}:${hook.startLine}` },
|
||||
});
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synthesize dispatcher→callback edges (field observers + EventEmitters +
|
||||
* React re-render + JSX children + Vue templates + SvelteKit load + RN event
|
||||
* channel + Fabric native-impl + MyBatis Java↔XML + Gin middleware chain +
|
||||
* Redux-thunk dispatch chain + object-literal registry dispatch).
|
||||
* Redux-thunk dispatch chain + object-literal registry dispatch + RTK Query
|
||||
* generated-hook → endpoint).
|
||||
* Returns the count added. Never throws into indexing — callers wrap in try/catch.
|
||||
*/
|
||||
export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionContext): number {
|
||||
@@ -1911,6 +1968,7 @@ export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionCo
|
||||
const ginEdges = ginMiddlewareChainEdges(queries, ctx);
|
||||
const thunkEdges = reduxThunkEdges(queries, ctx);
|
||||
const registryEdges = objectRegistryEdges(ctx);
|
||||
const rtkEdges = rtkQueryEdges(queries, ctx);
|
||||
|
||||
const merged: Edge[] = [];
|
||||
const seen = new Set<string>();
|
||||
@@ -1936,6 +1994,7 @@ export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionCo
|
||||
...ginEdges,
|
||||
...thunkEdges,
|
||||
...registryEdges,
|
||||
...rtkEdges,
|
||||
]) {
|
||||
const key = `${e.source}>${e.target}`;
|
||||
if (seen.has(key)) continue;
|
||||
|
||||
Reference in New Issue
Block a user