mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-15 23:32:49 +00:00
* fix(routes): connect decorator routes to their handler function
A Route node's only relationship was HANDLES_ROUTE from its FILE. The graph knew
a route existed and which file declared it, but not which function implemented
it. Two consequences on a 12.4k-file repository with 162 FastAPI routes:
- Every decorated handler was indistinguishable from dead code. Its sole edge
was DEFINES, so a reachability query reported it unreferenced even though the
framework invokes it on every request.
- `route_map` / `api_impact` could only answer at file granularity, and
`processes.ts` routed every route through its `routesWithoutHandlerByFile`
fallback instead of keying by handler.
Two halves of one gap, both already designed for and neither wired:
1. `ExtractedDecoratorRoute.handlerName` is documented as "captured at extraction
where the decorated definition node is in hand", and `resolveRouteHandlerSymbols`
already consumes it to stamp `handlerSymbolId`. Only the Spring extractor ever
set it, so for every decorator-routed framework — FastAPI, Flask, NestJS — it
arrived undefined and 0 of 162 routes carried a handler. A route decorator's
parent IS the decorated definition, so the name is in hand: add
`decoratedDefinitionName` and thread it through. It climbs consecutive
decorators so stacked forms (`@router.get(...)` over `@requires_auth`) resolve,
caps the climb so a malformed tree cannot loop, and returns undefined rather
than guessing — the routes phase already treats a missing name as
"fall back to file-level".
2. With a handler symbol resolved there is finally something to point an edge at.
Emit a definition-level HANDLES_ROUTE alongside the file-level one. The sibling
decorator overlay already does exactly this: `pipeline-phases/tools.ts` anchors
HANDLES_TOOL on the definition the decorator sat on, not its file. Routes were
the outlier.
Kept as one change because the edge is inert without the symbol — emitted from a
branch lacking part 1 it produces zero edges, since `handlerSymbolId` is empty.
Additive, and both existing consumers are unaffected:
`group/extractors/http-route-extractor.ts` types its query `(handlerFile:File)`;
`manifest-extractor.ts` matches an untyped `(handler)` but takes `LIMIT 1` ordered
by `handler.id`, and `File:…` sorts before `Function:…`, so its selected row is
unchanged.
Direction is Function → Route, matching how every other overlay attaches
(MEMBER_OF → Community, STEP_IN_PROCESS → Process, HANDLES_TOOL → Tool: the symbol
is the source). That also keeps it free of schema risk — `Function|Route` is
already declared by the ATTACHMENT rule in `lbug/schema.ts`
(`DEFINITION_ANCHOR_LABELS × ATTACHMENT_TARGET_LABELS`), which that file documents
as deliberate headroom for this case. Route → Function would have needed a new
hand-listed pair, and an undeclared pair aborts `analyze` outright — a failure
that file records having hit four separate times.
Verified on a FastAPI fixture (edges 9 → 11):
api.py (File) -> GET /widgets, POST /widgets [unchanged]
list_widgets (line 10) -> GET /widgets [new]
create_widget (line 15) -> POST /widgets [new]
On the 12.4k-file repository: 161 of 162 routes now resolve to their handler
function, up from 0. The single abstention is `uniqueSymbolId` correctly refusing
to guess where the name is not uniquely resolvable in its file.
`npx tsc --noEmit` clean; schema-pair coverage and route suites pass (196 tests).
* fix(routes): harden decorator handler attribution (#2865)
Keep definition-level route links correct across warm caches and malformed symbol lookups, and avoid per-route group-sync scans. Move Python AST ownership behind the language provider and add end-to-end regression coverage.
Note: full npm test could not complete in this container due unrelated worker startup failures and a stalled retry; targeted route suites, typecheck, format, and lint passed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(routes): reuse per-file symbol lookup and drop duplicate warm-cache test
Share extract()'s CONTAINING_QUERY memo with the graph provider path, resolve each route handler once, and fold the decorator-edge warm-cache assertions into the existing FastAPI composed-route round-trip.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Carter LaSalle <carterlasalle@gmail.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
96 lines
4 KiB
TypeScript
96 lines
4 KiB
TypeScript
/**
|
|
* Pins Python's `decoratorRouteHandlerName` provider hook against real
|
|
* tree-sitter-python trees.
|
|
*
|
|
* The hook feeds `ExtractedDecoratorRoute.handlerName`, which the routes phase
|
|
* turns into `handlerSymbolId` and a definition-level `HANDLES_ROUTE` edge. Two
|
|
* failure directions matter and both are asserted here:
|
|
*
|
|
* • too little — a plain module function, a method, a stacked-decorator run,
|
|
* or an `async def` must all yield the decorated function's name, or every
|
|
* Flask/FastAPI handler silently degrades to a file-level edge;
|
|
* • too much — a class-attached route decorator must yield nothing. A class
|
|
* does not handle a request, and returning its name would resolve
|
|
* `handlerSymbolId` to the wrong symbol.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import Parser from 'tree-sitter';
|
|
import Python from 'tree-sitter-python';
|
|
import { pythonDecoratorRouteHandlerName } from '../../src/core/ingestion/route-extractors/python-decorator-handler.js';
|
|
import type { SyntaxNode } from '../../src/core/ingestion/utils/ast-helpers.js';
|
|
|
|
const parser = new Parser();
|
|
parser.setLanguage(Python);
|
|
|
|
/** Every `decorator` node in `src`, in source order. */
|
|
function decorators(src: string): SyntaxNode[] {
|
|
const found: SyntaxNode[] = [];
|
|
const walk = (node: SyntaxNode): void => {
|
|
if (node.type === 'decorator') found.push(node);
|
|
for (const child of node.children) walk(child);
|
|
};
|
|
walk(parser.parse(src).rootNode);
|
|
return found;
|
|
}
|
|
|
|
/** Handler names the hook reports for each decorator in `src`. */
|
|
const handlerNames = (src: string): Array<string | undefined> =>
|
|
decorators(src).map((node) => pythonDecoratorRouteHandlerName(node));
|
|
|
|
describe('pythonDecoratorRouteHandlerName', () => {
|
|
it('names the module-level function a route decorator sits on', () => {
|
|
expect(handlerNames('@router.get("/widgets")\ndef list_widgets(): pass\n')).toEqual([
|
|
'list_widgets',
|
|
]);
|
|
});
|
|
|
|
it('names a method inside a class', () => {
|
|
expect(
|
|
handlerNames('class WidgetView:\n @router.post("/widgets")\n def create(self): pass\n'),
|
|
).toEqual(['create']);
|
|
});
|
|
|
|
it('names the same function for every decorator in a stacked run', () => {
|
|
// tree-sitter-python puts all decorators of a run under one
|
|
// `decorated_definition`, so no ancestor walk is needed to reach the
|
|
// definition past the sibling decorator.
|
|
expect(handlerNames('@router.get("/me")\n@requires_auth\ndef whoami(): pass\n')).toEqual([
|
|
'whoami',
|
|
'whoami',
|
|
]);
|
|
});
|
|
|
|
it('names an async handler (`async def` is still a function_definition)', () => {
|
|
expect(handlerNames('@app.get("/health")\nasync def health(): pass\n')).toEqual(['health']);
|
|
});
|
|
|
|
it('returns undefined for a class-attached route decorator', () => {
|
|
expect(handlerNames('@router.get("/widgets")\nclass WidgetResource: pass\n')).toEqual([
|
|
undefined,
|
|
]);
|
|
});
|
|
|
|
it('does not climb out of a class body to borrow the enclosing class name', () => {
|
|
// The decorator's parent here is the class body's `decorated_definition`
|
|
// holding a class, not a function. An ancestor walk would have found
|
|
// `Outer`; direct-shape ownership reports nothing.
|
|
expect(handlerNames('class Outer:\n @router.get("/x")\n class Inner: pass\n')).toEqual([
|
|
undefined,
|
|
]);
|
|
});
|
|
|
|
it('names the real def when a commented-out def precedes it', () => {
|
|
// Python applies the decorator to the next real definition; the comment is
|
|
// not a definition, so `real_handler` is the correct answer.
|
|
expect(
|
|
handlerNames('@router.get("/x")\n# def old_handler(): pass\ndef real_handler(): pass\n'),
|
|
).toEqual(['real_handler']);
|
|
});
|
|
|
|
it('returns undefined for a non-route decorator context with no decorated definition', () => {
|
|
// A bare decorator with no following definition is an ERROR/partial parse;
|
|
// the hook must not invent a name from whatever the parent happens to be.
|
|
expect(handlerNames('@router.get("/x")\n')).toEqual([undefined]);
|
|
});
|
|
});
|