74327814ee
* 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>
135 lines
4.7 KiB
TypeScript
135 lines
4.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { stripCommentsForRegex } from '../src/resolution/strip-comments';
|
|
|
|
describe('stripCommentsForRegex', () => {
|
|
it('python: strips line comments', () => {
|
|
const src = "x = 1 # path('/fake/', View)\nreal = 2";
|
|
const out = stripCommentsForRegex(src, 'python');
|
|
expect(out).not.toMatch(/path\('\/fake\//);
|
|
expect(out).toMatch(/real = 2/);
|
|
});
|
|
|
|
it('python: strips triple-quoted docstrings', () => {
|
|
const src = `"""
|
|
path('/in-docstring/', View)
|
|
"""
|
|
real = 1
|
|
`;
|
|
const out = stripCommentsForRegex(src, 'python');
|
|
expect(out).not.toMatch(/in-docstring/);
|
|
expect(out).toMatch(/real = 1/);
|
|
});
|
|
|
|
it('python: keeps # inside strings', () => {
|
|
const src = `path('#/fragment/', View)\n`;
|
|
const out = stripCommentsForRegex(src, 'python');
|
|
expect(out).toContain("'#/fragment/'");
|
|
});
|
|
|
|
it('python: handles triple-single-quoted docstrings', () => {
|
|
const src = `'''\npath('/fake/')\n'''\nreal = 1\n`;
|
|
const out = stripCommentsForRegex(src, 'python');
|
|
expect(out).not.toMatch(/fake/);
|
|
expect(out).toMatch(/real = 1/);
|
|
});
|
|
|
|
it('typescript: strips //, /* */', () => {
|
|
const src =
|
|
"// app.get('/fake', x)\n/* app.get('/also-fake', y) */\napp.get('/real', z)";
|
|
const out = stripCommentsForRegex(src, 'typescript');
|
|
expect(out).not.toMatch(/fake/);
|
|
expect(out).toMatch(/'\/real'/);
|
|
});
|
|
|
|
it('typescript: keeps // inside strings', () => {
|
|
const src = `const url = "https://example.com/path";\n`;
|
|
const out = stripCommentsForRegex(src, 'typescript');
|
|
expect(out).toContain('https://example.com/path');
|
|
});
|
|
|
|
it('php: strips //, #, and /* */', () => {
|
|
const src =
|
|
"// Route::get('/a', X::class)\n# Route::get('/b', Y::class)\n/* Route::get('/c', Z::class) */\nReal::go();";
|
|
const out = stripCommentsForRegex(src, 'php');
|
|
expect(out).not.toMatch(/'\/a'/);
|
|
expect(out).not.toMatch(/'\/b'/);
|
|
expect(out).not.toMatch(/'\/c'/);
|
|
expect(out).toContain('Real::go();');
|
|
});
|
|
|
|
it('ruby: strips =begin/=end', () => {
|
|
const src =
|
|
"=begin\nget '/fake', to: 'x#y'\n=end\nget '/real', to: 'a#b'\n";
|
|
const out = stripCommentsForRegex(src, 'ruby');
|
|
expect(out).not.toMatch(/fake/);
|
|
expect(out).toMatch(/'\/real'/);
|
|
});
|
|
|
|
it('ruby: strips # comments', () => {
|
|
const src = "# get '/fake', to: 'x#y'\nget '/real', to: 'a#b'\n";
|
|
const out = stripCommentsForRegex(src, 'ruby');
|
|
expect(out).not.toMatch(/fake/);
|
|
expect(out).toMatch(/'\/real'/);
|
|
});
|
|
|
|
it('rust: handles nested block comments', () => {
|
|
const src =
|
|
'/* outer /* inner */ still in outer */ .route("/real", get(h))';
|
|
const out = stripCommentsForRegex(src, 'rust');
|
|
expect(out).not.toMatch(/inner/);
|
|
expect(out).toMatch(/\/real/);
|
|
});
|
|
|
|
it('go: keeps backtick raw strings intact, strips // comments', () => {
|
|
const src = '// r.GET("/fake", h)\nr.GET(`/real`, h2)\n';
|
|
const out = stripCommentsForRegex(src, 'go');
|
|
expect(out).not.toMatch(/fake/);
|
|
// backtick raw string contents preserved
|
|
expect(out).toMatch(/`\/real`/);
|
|
});
|
|
|
|
it('go: strips block comments containing route-shaped text', () => {
|
|
const src = '/* r.GET("/fake", h) */\nr.GET("/real", h2)\n';
|
|
const out = stripCommentsForRegex(src, 'go');
|
|
expect(out).not.toMatch(/fake/);
|
|
expect(out).toMatch(/"\/real"/);
|
|
});
|
|
|
|
it('java: strips // and /* */ comments', () => {
|
|
const src =
|
|
'// @GetMapping("/fake")\n/* @PostMapping("/also-fake") */\n@GetMapping("/real")\n';
|
|
const out = stripCommentsForRegex(src, 'java');
|
|
expect(out).not.toMatch(/fake/);
|
|
expect(out).toMatch(/"\/real"/);
|
|
});
|
|
|
|
it('csharp: strips // and /* */ comments', () => {
|
|
const src =
|
|
'// [HttpGet("/fake")]\n/* [HttpPost("/also-fake")] */\n[HttpGet("/real")]\n';
|
|
const out = stripCommentsForRegex(src, 'csharp');
|
|
expect(out).not.toMatch(/fake/);
|
|
expect(out).toMatch(/"\/real"/);
|
|
});
|
|
|
|
it('swift: strips // and /* */ comments', () => {
|
|
const src =
|
|
'// app.get("fake", use: x)\n/* app.get("also-fake", use: y) */\napp.get("real", use: z)\n';
|
|
const out = stripCommentsForRegex(src, 'swift');
|
|
expect(out).not.toMatch(/fake/);
|
|
expect(out).toMatch(/"real"/);
|
|
});
|
|
|
|
it('preserves line numbers (newlines retained)', () => {
|
|
const src = "line1\n# comment with path('/fake/')\nline3";
|
|
const out = stripCommentsForRegex(src, 'python');
|
|
expect(out.split('\n').length).toBe(3);
|
|
expect(out.split('\n')[2]).toBe('line3');
|
|
});
|
|
|
|
it('preserves overall length so source offsets stay valid', () => {
|
|
const src = "x = 1 # path('/fake/', View)\nreal = 2";
|
|
const out = stripCommentsForRegex(src, 'python');
|
|
expect(out.length).toBe(src.length);
|
|
});
|
|
});
|