GitNexus/gitnexus/test/fixtures/api-impact-seed.ts
samuel mbabhazi 9772bcd24f
feat(api_impact): list the callers of the requesting function as indirect consumers (#3720)
* feat(api_impact): list the callers of the requesting function as indirect consumers

The FETCHES edge is file-level, so api_impact saw only the file that sends
the request. The files that call its request function never mention the URL
and were missing from the report (#3644).

The edge's reason now names the callables whose spans contain the request
sites, one `|via:<id>` field each, under the containment rule the R3-6 sink
set already uses; that rule moves into EnclosingCallableIndex so both stay
one. Several sites of one file for one route now share the edge instead of
colliding on its id. api_impact follows the CALLS edges into those callables
one level up and reports the callers as `indirectConsumers`, apart from the
direct consumers and outside the risk level. Direct consumer files and the
handler file are left out. An index built before the attribution carries no
field. Expo navigation sites carry their real line so they attribute to the
component that contains them.

Closes #3644

* fix(api_impact): attribute a request site by its AST enclosing callable

A helper defined on the line of the request has the smaller span, so the
line-only lookup named it and the callers of the real requesting function
vanished from indirectConsumers. The parser now records the enclosing
callable of each fetch site and of each wrapper call (through the call's
position), and attribution prefers that identity; the span lookup remains
for sites found by text scan. The same preference applies to the R3-6 sink
set. A real analyze of the fixture followed by api_impact pins the helper
case and the wrapper case.

* test(bench): rebaseline receiver resolution for the indirect-consumers fixture

The api-impact-indirect-consumers fixture adds three receiver-unresolved
sites to the corpus: two `.then(...)` calls on the untyped return of
`fetch(...)` and of the local wrapper `apiFetch(...)`, and one
`req.query.path` read inside an inline Express handler with an untyped
`req`. Call drops 138 -> 140, reads 30 -> 31, all kinds 168 -> 171. No
shape-arm state and no existing site changed.

* fix(api_impact): preserve module-scope request attribution

Keep AST-confirmed module scope distinct from missing attribution for direct
fetches, HTTP clients, and wrapper calls. Prevent overlapping callable spans
from inventing indirect consumers or process sinks.

Preserve span fallback for callable forms the generic identity walk cannot
name. Add eight regression cases, including real analyze/api_impact coverage.
Rebaseline receiver-resolution counts for the two then calls and one property
read added by this PR's existing fixtures; shape expectations are unchanged.

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-10-25 18:29:09 +00:00

81 lines
8.3 KiB
TypeScript

import type { FTSIndexDef } from '../helpers/test-indexed-db.js';
/**
* Seed data for api_impact E2E tests.
*
* Simulates what the pipeline would produce for a Next.js project with:
* - /api/grants route (success + error shapes)
* - /api/secure route (middleware-wrapped, two method exports)
* - Multiple consumers with different fetch patterns
* - Multi-fetch consumer (useMulti fetches both routes)
* - Mismatch consumer (accesses 'meta' which doesn't exist)
*/
export const API_IMPACT_SEED_DATA = [
// ─── Files ─────────────────────────────────────────────────────────
`CREATE (f:File {id: 'file:app/api/grants/route.ts', name: 'route.ts', filePath: 'app/api/grants/route.ts', content: 'export async function GET() { ... }'})`,
`CREATE (f:File {id: 'file:app/api/secure/route.ts', name: 'route.ts', filePath: 'app/api/secure/route.ts', content: 'export const GET = withAuth(withRateLimit(handler))'})`,
`CREATE (f:File {id: 'file:components/GrantsList.tsx', name: 'GrantsList.tsx', filePath: 'components/GrantsList.tsx', content: 'const { data, pagination } = await res.json()'})`,
`CREATE (f:File {id: 'file:hooks/useGrants.ts', name: 'useGrants.ts', filePath: 'hooks/useGrants.ts', content: 'const data = await result.json(); data.items'})`,
`CREATE (f:File {id: 'file:hooks/useMulti.ts', name: 'useMulti.ts', filePath: 'hooks/useMulti.ts', content: 'fetches /api/grants and /api/secure'})`,
// ─── Route nodes ───────────────────────────────────────────────────
// /api/grants: success keys = [data, pagination], error keys = [error, message]
`CREATE (r:Route {id: 'Route:/api/grants', name: '/api/grants', filePath: 'app/api/grants/route.ts', responseKeys: ['data', 'pagination'], errorKeys: ['error', 'message'], middleware: []})`,
// /api/secure: middleware wrapped, separate success keys per method
`CREATE (r:Route {id: 'Route:/api/secure', name: '/api/secure', filePath: 'app/api/secure/route.ts', responseKeys: ['items', 'count'], errorKeys: [], middleware: ['withAuth', 'withRateLimit'], unverifiedConsumers: 2})`,
// ─── Functions (consumers) ─────────────────────────────────────────
`CREATE (fn:Function {id: 'func:GrantsList', name: 'GrantsList', filePath: 'components/GrantsList.tsx', startLine: 1, endLine: 10, isExported: true, content: 'export async function GrantsList()', description: 'Grants list component'})`,
`CREATE (fn:Function {id: 'func:useGrants', name: 'useGrants', filePath: 'hooks/useGrants.ts', startLine: 1, endLine: 5, isExported: true, content: 'export async function useGrants()', description: 'Grants data hook'})`,
`CREATE (fn:Function {id: 'func:useMulti', name: 'useMulti', filePath: 'hooks/useMulti.ts', startLine: 1, endLine: 8, isExported: true, content: 'export async function useMulti()', description: 'Multi-route fetcher'})`,
// ─── Functions (handlers) ──────────────────────────────────────────
`CREATE (fn:Function {id: 'func:grants-GET', name: 'GET', filePath: 'app/api/grants/route.ts', startLine: 3, endLine: 10, isExported: true, content: 'export async function GET()', description: 'Grants GET handler'})`,
`CREATE (fn:Function {id: 'func:secure-GET', name: 'GET', filePath: 'app/api/secure/route.ts', startLine: 5, endLine: 7, isExported: true, content: 'export const GET = withAuth(withRateLimit(handler))', description: 'Secure GET handler'})`,
`CREATE (fn:Function {id: 'func:secure-POST', name: 'POST', filePath: 'app/api/secure/route.ts', startLine: 9, endLine: 12, isExported: true, content: 'export const POST = async (req)', description: 'Secure POST handler'})`,
// ─── FETCHES edges (consumer → route) ──────────────────────────────
// GrantsList fetches /api/grants — accesses 'data' and 'pagination'
`MATCH (a:Function), (r:Route) WHERE a.id = 'func:GrantsList' AND r.id = 'Route:/api/grants'
CREATE (a)-[:CodeRelation {type: 'FETCHES', confidence: 1.0, reason: 'fetch-url-match|keys:data,pagination', step: 0}]->(r)`,
// useGrants fetches /api/grants — accesses 'items' (valid key from useGrants perspective — data.items)
`MATCH (a:Function), (r:Route) WHERE a.id = 'func:useGrants' AND r.id = 'Route:/api/grants'
CREATE (a)-[:CodeRelation {type: 'FETCHES', confidence: 1.0, reason: 'fetch-url-match|keys:items', step: 0}]->(r)`,
// useMulti fetches /api/grants — accesses 'data' and 'meta' (meta is a mismatch)
`MATCH (a:Function), (r:Route) WHERE a.id = 'func:useMulti' AND r.id = 'Route:/api/grants'
CREATE (a)-[:CodeRelation {type: 'FETCHES', confidence: 0.8, reason: 'fetch-url-match|keys:data,meta|fetches:2', step: 0}]->(r)`,
// useMulti also fetches /api/secure — accesses 'items'
`MATCH (a:Function), (r:Route) WHERE a.id = 'func:useMulti' AND r.id = 'Route:/api/secure'
CREATE (a)-[:CodeRelation {type: 'FETCHES', confidence: 0.8, reason: 'fetch-url-match|keys:items|fetches:2', step: 0}]->(r)`,
// ─── HANDLES_ROUTE edges (handler → route) ────────────────────────
`MATCH (fn:Function), (r:Route) WHERE fn.id = 'func:grants-GET' AND r.id = 'Route:/api/grants'
CREATE (fn)-[:CodeRelation {type: 'HANDLES_ROUTE', confidence: 1.0, reason: 'nextjs-app-router', step: 0}]->(r)`,
`MATCH (fn:Function), (r:Route) WHERE fn.id = 'func:secure-GET' AND r.id = 'Route:/api/secure'
CREATE (fn)-[:CodeRelation {type: 'HANDLES_ROUTE', confidence: 1.0, reason: 'nextjs-app-router', step: 0}]->(r)`,
`MATCH (fn:Function), (r:Route) WHERE fn.id = 'func:secure-POST' AND r.id = 'Route:/api/secure'
CREATE (fn)-[:CodeRelation {type: 'HANDLES_ROUTE', confidence: 1.0, reason: 'nextjs-app-router', step: 0}]->(r)`,
// ─── Indirect consumers (#3644) ────────────────────────────────────
// backend-client.ts is the file that sends the request; the edge names the
// function holding the site. App and Header call that function without ever
// mentioning the URL. useMulti calls it too, but already fetches the route
// itself, so it stays a direct consumer only.
`CREATE (f:File {id: 'file:services/backend-client.ts', name: 'backend-client.ts', filePath: 'services/backend-client.ts', content: 'export const fetchSecure = () => fetch("/api/secure")'})`,
`CREATE (fn:Function {id: 'func:fetchSecure', name: 'fetchSecure', filePath: 'services/backend-client.ts', startLine: 1, endLine: 3, isExported: true, content: 'export const fetchSecure = () => fetch("/api/secure")', description: 'Secure request function'})`,
`CREATE (fn:Function {id: 'func:App', name: 'App', filePath: 'App.tsx', startLine: 1, endLine: 20, isExported: true, content: 'export function App() { fetchSecure() }', description: 'App root'})`,
`CREATE (fn:Function {id: 'func:Header', name: 'Header', filePath: 'components/Header.tsx', startLine: 1, endLine: 12, isExported: true, content: 'export function Header() { fetchSecure() }', description: 'Header'})`,
`MATCH (a:File), (r:Route) WHERE a.id = 'file:services/backend-client.ts' AND r.id = 'Route:/api/secure'
CREATE (a)-[:CodeRelation {type: 'FETCHES', confidence: 0.9, reason: 'fetch-url-match|via:func:fetchSecure', step: 0}]->(r)`,
`MATCH (a:Function), (b:Function) WHERE a.id = 'func:App' AND b.id = 'func:fetchSecure'
CREATE (a)-[:CodeRelation {type: 'CALLS', confidence: 1.0, reason: 'direct', step: 0}]->(b)`,
`MATCH (a:Function), (b:Function) WHERE a.id = 'func:Header' AND b.id = 'func:fetchSecure'
CREATE (a)-[:CodeRelation {type: 'CALLS', confidence: 1.0, reason: 'direct', step: 0}]->(b)`,
`MATCH (a:Function), (b:Function) WHERE a.id = 'func:useMulti' AND b.id = 'func:fetchSecure'
CREATE (a)-[:CodeRelation {type: 'CALLS', confidence: 1.0, reason: 'direct', step: 0}]->(b)`,
];
export const API_IMPACT_FTS_INDEXES: FTSIndexDef[] = [
{ table: 'Function', indexName: 'function_fts', columns: ['name', 'content', 'description'] },
{ table: 'File', indexName: 'file_fts', columns: ['name', 'content'] },
];