Skip to content

Commit 7432781

Browse files
timomearacolbymchenryclaude
authored
feat: wire up framework route extraction (colbymchenry#89)
* docs: add framework extract wiring plan * feat(resolution): replace extractNodes with extract() returning nodes and references * feat(resolution): add getApplicableFrameworks helper for per-language dispatch * feat(django): emit route nodes and route->view references in extract() * feat(flask,fastapi): emit route nodes and route->handler references * feat(express): emit route nodes and route->handler references * feat(laravel): emit route nodes and route->handler references * feat(rails): emit route nodes and route->handler references * feat(spring): emit route nodes and route->handler references * feat(go): emit route nodes and route->handler references * feat(rust): emit route nodes and route->handler references * feat(aspnet): emit route nodes and route->handler references * feat(swift,vapor): emit route nodes and route->handler references * chore(react,svelte): migrate resolvers to extract() interface * feat(extraction): run framework extractors after tree-sitter parse * docs: document framework route extraction * feat(strip-comments): add per-language comment stripper for framework extractors Replaces comment characters and string-literal contents with spaces (not removal) so source offsets stay valid for downstream regex match index -> line number conversion. Handles Python triple-quoted docstrings, Ruby =begin/=end, Rust nested block comments, and the standard //, #, /* */ forms across the supported languages. This is consumed by framework extract() methods in a follow-up commit so that commented-out / docstring routing examples don't surface as phantom route nodes in the graph. * feat(frameworks): strip comments before regex extraction (prevents phantom routes) Pipes the per-language stripCommentsForRegex helper into every framework extract() that scans raw source: django/flask/fastapi (python.ts), express, laravel, rails, spring, go, rust, aspnet, vapor, plus swiftui/uikit struct extraction in swift.ts. Without this, examples like: # path('/admin/', AdminPanel.as_view()) """ path('/users/', UserListView.as_view()) """ urlpatterns = [path('/real/', RealView.as_view())] produced 3 phantom route nodes. Now only the real one is extracted. Each framework gets a regression test in __tests__/frameworks.test.ts asserting that line-, block-, docstring- and (where relevant) heredoc-style commented-out routes do not surface as nodes. --------- Co-authored-by: Colby McHenry <me@colbymchenry.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5ab8174 commit 7432781

23 files changed

Lines changed: 3073 additions & 712 deletions

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,32 @@ All tests used Claude Opus 4.6 (1M context) with Claude Code v2.1.91. Each test
107107
| **Impact Analysis** | Trace callers, callees, and the full impact radius of any symbol before making changes |
108108
| **Always Fresh** | File watcher uses native OS events (FSEvents/inotify/ReadDirectoryChangesW) with debounced auto-sync — the graph stays current as you code, zero config |
109109
| **19+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Swift, Kotlin, Dart, Svelte, Liquid, Pascal/Delphi |
110+
| **Framework-aware Routes** | Recognizes web-framework routing files and links URL patterns to their handlers across 13 frameworks |
110111
| **100% Local** | No data leaves your machine. No API keys. No external services. SQLite database only |
111112

112113
---
113114

115+
## Framework-aware Routes
116+
117+
CodeGraph detects web-framework routing files and emits `route` nodes linked by `references` edges to their handler classes or functions. Querying callers of a view/controller now surfaces the URL pattern that binds it.
118+
119+
| Framework | Shapes recognized |
120+
|---|---|
121+
| **Django** | `path()`, `re_path()`, `url()`, `include()` in `urls.py` (CBV `.as_view()`, dotted paths) |
122+
| **Flask** | `@app.route('/path', methods=[...])`, blueprint routes |
123+
| **FastAPI** | `@app.get(...)`, `@router.post(...)`, all standard methods |
124+
| **Express** | `app.get(...)`, `router.post(...)` with middleware chains |
125+
| **Laravel** | `Route::get()`, `Route::resource()`, `Controller@action`, tuple syntax |
126+
| **Rails** | `get '/x', to: 'users#index'`, hash-rocket `=>` syntax |
127+
| **Spring** | `@GetMapping`, `@PostMapping`, `@RequestMapping` on methods |
128+
| **Gin / chi / gorilla / mux** | `r.GET(...)`, `router.HandleFunc(...)` |
129+
| **Axum / actix / Rocket** | `.route("/x", get(handler))` |
130+
| **ASP.NET** | `[HttpGet("/x")]` attributes on action methods |
131+
| **Vapor** | `app.get("x", use: handler)` |
132+
| **React Router** / **SvelteKit** | Route component nodes |
133+
134+
---
135+
114136
## Quick Start
115137

116138
### 1. Run the Installer
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, it, expect, beforeAll, afterEach } from 'vitest';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
import * as os from 'os';
5+
import { CodeGraph } from '../src';
6+
import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';
7+
8+
beforeAll(async () => {
9+
await initGrammars();
10+
await loadAllGrammars();
11+
});
12+
13+
describe('Django end-to-end framework extraction', () => {
14+
let tmpDir: string | undefined;
15+
afterEach(() => {
16+
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
17+
tmpDir = undefined;
18+
});
19+
20+
it('creates a route->view edge from urls.py to view class', async () => {
21+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-django-'));
22+
fs.writeFileSync(path.join(tmpDir, 'manage.py'), '# marker\n');
23+
fs.writeFileSync(path.join(tmpDir, 'requirements.txt'), 'django==4.2\n');
24+
fs.mkdirSync(path.join(tmpDir, 'users'));
25+
fs.writeFileSync(path.join(tmpDir, 'users/__init__.py'), '');
26+
fs.writeFileSync(
27+
path.join(tmpDir, 'users/views.py'),
28+
'class UserListView:\n def get(self, request): pass\n'
29+
);
30+
fs.writeFileSync(
31+
path.join(tmpDir, 'users/urls.py'),
32+
'from django.urls import path\n' +
33+
'from users.views import UserListView\n' +
34+
'urlpatterns = [path("users/", UserListView.as_view(), name="user-list")]\n'
35+
);
36+
37+
const cg = CodeGraph.initSync(tmpDir);
38+
await cg.indexAll();
39+
40+
// Route node exists
41+
const routes = cg.getNodesByKind('route');
42+
expect(routes.length).toBeGreaterThan(0);
43+
const route = routes.find((n) => n.name === 'users/');
44+
expect(route).toBeDefined();
45+
46+
// View class exists
47+
const classNodes = cg.getNodesByKind('class');
48+
const view = classNodes.find((n) => n.name === 'UserListView');
49+
expect(view).toBeDefined();
50+
51+
// Edge route -> view exists
52+
const edges = cg.getOutgoingEdges(route!.id);
53+
const toView = edges.find((e) => e.target === view!.id);
54+
expect(toView).toBeDefined();
55+
expect(toView!.kind).toBe('references');
56+
57+
cg.close();
58+
});
59+
});

0 commit comments

Comments
 (0)