codegraph-engine 2.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codegraph/__init__.py +37 -0
- codegraph/agent.py +26 -0
- codegraph/architecture.py +328 -0
- codegraph/audit.py +106 -0
- codegraph/cache.py +95 -0
- codegraph/cli.py +854 -0
- codegraph/config.py +43 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +1228 -0
- codegraph/epistemic.py +90 -0
- codegraph/errors.py +275 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +397 -0
- codegraph/frameworks.py +434 -0
- codegraph/freshness.py +295 -0
- codegraph/git.py +278 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +41 -0
- codegraph/graph/traversal.py +1291 -0
- codegraph/indexing/__init__.py +4 -0
- codegraph/indexing/classifier.py +274 -0
- codegraph/indexing/indexer.py +943 -0
- codegraph/indexing/models.py +338 -0
- codegraph/indexing/parser.py +1240 -0
- codegraph/indexing/scanner.py +200 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/interrogation.py +1582 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +736 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +289 -0
- codegraph/observability.py +151 -0
- codegraph/optimizer.py +372 -0
- codegraph/planner.py +417 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +363 -0
- codegraph/resolver.py +843 -0
- codegraph/resources/__init__.py +45 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +232 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +220 -0
- codegraph/search/__init__.py +23 -0
- codegraph/search/hybrid.py +301 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +3 -0
- codegraph/security/paths.py +35 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph_engine-2.1.1.dist-info/METADATA +334 -0
- codegraph_engine-2.1.1.dist-info/RECORD +62 -0
- codegraph_engine-2.1.1.dist-info/WHEEL +5 -0
- codegraph_engine-2.1.1.dist-info/entry_points.txt +2 -0
- codegraph_engine-2.1.1.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-2.1.1.dist-info/top_level.txt +1 -0
codegraph/frameworks.py
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""Pluggable, evidence-driven framework analyzers for Python and JavaScript/TypeScript.
|
|
2
|
+
|
|
3
|
+
Supported Frameworks:
|
|
4
|
+
Python: Flask, FastAPI, Django
|
|
5
|
+
JavaScript/TypeScript: Express, Next.js (App Router)
|
|
6
|
+
|
|
7
|
+
Architectural Invariants:
|
|
8
|
+
1. Framework behavior is detected ONLY when concrete source evidence (imports, decorators,
|
|
9
|
+
route registration calls, or verified route handler exports) exists in the file.
|
|
10
|
+
2. Route detection NEVER rescans the full AST; it consumes node visit events during
|
|
11
|
+
the primary single-pass parser traversal.
|
|
12
|
+
3. Every endpoint produces a stable endpoint_id, human-readable route_signature, original
|
|
13
|
+
source route_path, and canonical normalized_route.
|
|
14
|
+
4. Framework endpoints link to handlers via HANDLED_BY semantic edges using the standard
|
|
15
|
+
canonical symbol ID.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import ast
|
|
20
|
+
import re
|
|
21
|
+
from dataclasses import asdict, dataclass
|
|
22
|
+
from typing import Protocol
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class FrameworkEvidence:
|
|
27
|
+
framework: str # flask | fastapi | django | express | nextjs
|
|
28
|
+
construct_type: str # ROUTE | CONTROLLER | MIDDLEWARE | DEPENDENCY | MODEL
|
|
29
|
+
file_path: str
|
|
30
|
+
line: int
|
|
31
|
+
column: int | None
|
|
32
|
+
evidence: str
|
|
33
|
+
confidence: str = "HIGH"
|
|
34
|
+
|
|
35
|
+
def as_dict(self) -> dict[str, object]:
|
|
36
|
+
return asdict(self)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def normalize_route_path(route_path: str, framework: str) -> str:
|
|
40
|
+
"""Normalize framework-specific route parameters into canonical `{param}` format.
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
Flask: /users/<id> -> /users/{id}
|
|
44
|
+
Flask: /users/<int:id> -> /users/{id}
|
|
45
|
+
FastAPI: /users/{id} -> /users/{id}
|
|
46
|
+
Express: /users/:id -> /users/{id}
|
|
47
|
+
Next.js: /users/[id] -> /users/{id}
|
|
48
|
+
Next.js: /users/[...slug] -> /users/{slug}
|
|
49
|
+
"""
|
|
50
|
+
raw = "/" + route_path.strip().lstrip("/") if route_path.strip() else "/"
|
|
51
|
+
# Strip trailing slash unless root
|
|
52
|
+
if len(raw) > 1 and raw.endswith("/"):
|
|
53
|
+
raw = raw.rstrip("/")
|
|
54
|
+
|
|
55
|
+
if framework == "flask":
|
|
56
|
+
# Match <[type:]param>
|
|
57
|
+
return re.sub(r"<(?:\w+:)?([A-Za-z_][\w]*)>", r"{\1}", raw)
|
|
58
|
+
elif framework == "express":
|
|
59
|
+
# Match :param
|
|
60
|
+
return re.sub(r":([A-Za-z_][\w]*)", r"{\1}", raw)
|
|
61
|
+
elif framework == "nextjs":
|
|
62
|
+
# Match [[...param]] or [...param] or [param]
|
|
63
|
+
return re.sub(r"\[(?:\.\.\.)?([A-Za-z_][\w]*)\]", r"{\1}", raw)
|
|
64
|
+
return raw
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class RouteDetection:
|
|
69
|
+
framework: str # flask | fastapi | django | express | nextjs
|
|
70
|
+
http_method: str # GET | POST | PUT | DELETE | PATCH | OPTIONS | HEAD | ANY
|
|
71
|
+
route_path: str # Original framework route string, e.g. /users/<int:id>
|
|
72
|
+
normalized_route: str # Canonical route string, e.g. /users/{id}
|
|
73
|
+
handler_name: str # e.g. "login_controller"
|
|
74
|
+
handler_canonical_id: str # Standard canonical symbol ID, e.g. "src.auth.routes.login_controller"
|
|
75
|
+
file_path: str
|
|
76
|
+
line: int
|
|
77
|
+
column: int | None = None
|
|
78
|
+
confidence: str = "HIGH"
|
|
79
|
+
resolution_status: str = "RESOLVED" # RESOLVED | UNRESOLVED | AMBIGUOUS
|
|
80
|
+
evidence: str = ""
|
|
81
|
+
module: str = ""
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def route_signature(self) -> str:
|
|
85
|
+
"""Human-readable route signature, e.g. 'POST /login'."""
|
|
86
|
+
return f"{self.http_method} {self.route_path}"
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def endpoint_id(self) -> str:
|
|
90
|
+
"""Stable, globally unique endpoint identifier scoped by module to prevent collisions."""
|
|
91
|
+
if self.module:
|
|
92
|
+
mod = self.module
|
|
93
|
+
else:
|
|
94
|
+
from codegraph.indexing.models import normalize_module
|
|
95
|
+
mod = normalize_module(self.file_path)
|
|
96
|
+
return f"API_ENDPOINT:{mod}:{self.http_method} {self.normalized_route}"
|
|
97
|
+
|
|
98
|
+
def as_dict(self) -> dict[str, object]:
|
|
99
|
+
return {
|
|
100
|
+
"endpoint_id": self.endpoint_id,
|
|
101
|
+
"route_signature": self.route_signature,
|
|
102
|
+
"framework": self.framework,
|
|
103
|
+
"http_method": self.http_method,
|
|
104
|
+
"route_path": self.route_path,
|
|
105
|
+
"normalized_route": self.normalized_route,
|
|
106
|
+
"handler_name": self.handler_name,
|
|
107
|
+
"handler_canonical_id": self.handler_canonical_id,
|
|
108
|
+
"file": self.file_path,
|
|
109
|
+
"file_path": self.file_path,
|
|
110
|
+
"line": self.line,
|
|
111
|
+
"column": self.column,
|
|
112
|
+
"confidence": self.confidence,
|
|
113
|
+
"resolution_status": self.resolution_status,
|
|
114
|
+
"evidence": self.evidence,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True)
|
|
119
|
+
class AnalysisContext:
|
|
120
|
+
file_path: str
|
|
121
|
+
module: str
|
|
122
|
+
imports_modules: set[str]
|
|
123
|
+
scope_qname: str
|
|
124
|
+
scope_canonical_id: str
|
|
125
|
+
scope_kind: str
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class FrameworkAnalyzer(Protocol):
|
|
129
|
+
framework: str
|
|
130
|
+
|
|
131
|
+
def analyze_python_node(
|
|
132
|
+
self,
|
|
133
|
+
node: ast.AST,
|
|
134
|
+
context: AnalysisContext,
|
|
135
|
+
) -> list[RouteDetection]: ...
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
# Python Framework Analyzers
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
_FLASK_METHODS = {"route", "get", "post", "put", "delete", "patch"}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class FlaskAnalyzer:
|
|
146
|
+
framework = "flask"
|
|
147
|
+
|
|
148
|
+
def analyze_python_node(
|
|
149
|
+
self,
|
|
150
|
+
node: ast.AST,
|
|
151
|
+
context: AnalysisContext,
|
|
152
|
+
) -> list[RouteDetection]:
|
|
153
|
+
routes: list[RouteDetection] = []
|
|
154
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
155
|
+
return routes
|
|
156
|
+
|
|
157
|
+
for dec in node.decorator_list:
|
|
158
|
+
if not isinstance(dec, ast.Call) or not isinstance(dec.func, ast.Attribute):
|
|
159
|
+
continue
|
|
160
|
+
attr = dec.func.attr.lower()
|
|
161
|
+
if attr not in _FLASK_METHODS or not dec.args:
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
first_arg = dec.args[0]
|
|
165
|
+
if not isinstance(first_arg, ast.Constant) or not isinstance(first_arg.value, str):
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
route_path = first_arg.value
|
|
169
|
+
methods = [attr.upper()] if attr != "route" else ["GET"]
|
|
170
|
+
|
|
171
|
+
for kw in dec.keywords:
|
|
172
|
+
if kw.arg == "methods" and isinstance(kw.value, (ast.List, ast.Tuple)):
|
|
173
|
+
methods = [
|
|
174
|
+
e.value.upper()
|
|
175
|
+
for e in kw.value.elts
|
|
176
|
+
if isinstance(e, ast.Constant) and isinstance(e.value, str)
|
|
177
|
+
] or ["GET"]
|
|
178
|
+
|
|
179
|
+
col = getattr(dec, "col_offset", None)
|
|
180
|
+
norm = normalize_route_path(route_path, "flask")
|
|
181
|
+
handler_canon = (
|
|
182
|
+
f"{context.scope_canonical_id}.{node.name}"
|
|
183
|
+
if context.scope_qname
|
|
184
|
+
else f"{context.module}.{node.name}"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
for method in methods:
|
|
188
|
+
routes.append(
|
|
189
|
+
RouteDetection(
|
|
190
|
+
framework="flask",
|
|
191
|
+
http_method=method,
|
|
192
|
+
route_path=route_path,
|
|
193
|
+
normalized_route=norm,
|
|
194
|
+
handler_name=node.name,
|
|
195
|
+
handler_canonical_id=handler_canon,
|
|
196
|
+
file_path=context.file_path,
|
|
197
|
+
line=dec.lineno,
|
|
198
|
+
column=col,
|
|
199
|
+
confidence="HIGH",
|
|
200
|
+
resolution_status="RESOLVED",
|
|
201
|
+
evidence=f"Flask decorator @{ast.unparse(dec.func)}('{route_path}') on {node.name}",
|
|
202
|
+
module=context.module,
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
return routes
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
_FASTAPI_METHODS = {"get", "post", "put", "delete", "patch", "options", "head", "api_route"}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class FastAPIAnalyzer:
|
|
212
|
+
framework = "fastapi"
|
|
213
|
+
|
|
214
|
+
def analyze_python_node(
|
|
215
|
+
self,
|
|
216
|
+
node: ast.AST,
|
|
217
|
+
context: AnalysisContext,
|
|
218
|
+
) -> list[RouteDetection]:
|
|
219
|
+
routes: list[RouteDetection] = []
|
|
220
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
221
|
+
return routes
|
|
222
|
+
|
|
223
|
+
for dec in node.decorator_list:
|
|
224
|
+
if not isinstance(dec, ast.Call) or not isinstance(dec.func, ast.Attribute):
|
|
225
|
+
continue
|
|
226
|
+
attr = dec.func.attr.lower()
|
|
227
|
+
if attr not in _FASTAPI_METHODS or not dec.args:
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
first_arg = dec.args[0]
|
|
231
|
+
if not isinstance(first_arg, ast.Constant) or not isinstance(first_arg.value, str):
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
route_path = first_arg.value
|
|
235
|
+
methods = [attr.upper()] if attr != "api_route" else ["GET"]
|
|
236
|
+
|
|
237
|
+
for kw in dec.keywords:
|
|
238
|
+
if kw.arg == "methods" and isinstance(kw.value, (ast.List, ast.Tuple)):
|
|
239
|
+
methods = [
|
|
240
|
+
e.value.upper()
|
|
241
|
+
for e in kw.value.elts
|
|
242
|
+
if isinstance(e, ast.Constant) and isinstance(e.value, str)
|
|
243
|
+
] or ["GET"]
|
|
244
|
+
|
|
245
|
+
col = getattr(dec, "col_offset", None)
|
|
246
|
+
norm = normalize_route_path(route_path, "fastapi")
|
|
247
|
+
handler_canon = (
|
|
248
|
+
f"{context.scope_canonical_id}.{node.name}"
|
|
249
|
+
if context.scope_qname
|
|
250
|
+
else f"{context.module}.{node.name}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
for method in methods:
|
|
254
|
+
routes.append(
|
|
255
|
+
RouteDetection(
|
|
256
|
+
framework="fastapi",
|
|
257
|
+
http_method=method,
|
|
258
|
+
route_path=route_path,
|
|
259
|
+
normalized_route=norm,
|
|
260
|
+
handler_name=node.name,
|
|
261
|
+
handler_canonical_id=handler_canon,
|
|
262
|
+
file_path=context.file_path,
|
|
263
|
+
line=dec.lineno,
|
|
264
|
+
column=col,
|
|
265
|
+
confidence="HIGH",
|
|
266
|
+
resolution_status="RESOLVED",
|
|
267
|
+
evidence=f"FastAPI decorator @{ast.unparse(dec.func)}('{route_path}') on {node.name}",
|
|
268
|
+
module=context.module,
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
return routes
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class DjangoAnalyzer:
|
|
275
|
+
framework = "django"
|
|
276
|
+
|
|
277
|
+
def analyze_python_node(
|
|
278
|
+
self,
|
|
279
|
+
node: ast.AST,
|
|
280
|
+
context: AnalysisContext,
|
|
281
|
+
) -> list[RouteDetection]:
|
|
282
|
+
routes: list[RouteDetection] = []
|
|
283
|
+
if not isinstance(node, ast.Call):
|
|
284
|
+
return routes
|
|
285
|
+
|
|
286
|
+
func_name = ""
|
|
287
|
+
if isinstance(node.func, ast.Name):
|
|
288
|
+
func_name = node.func.id
|
|
289
|
+
elif isinstance(node.func, ast.Attribute):
|
|
290
|
+
func_name = node.func.attr
|
|
291
|
+
|
|
292
|
+
if func_name in ("path", "re_path") and len(node.args) >= 2:
|
|
293
|
+
arg0, arg1 = node.args[0], node.args[1]
|
|
294
|
+
if isinstance(arg0, ast.Constant) and isinstance(arg0.value, str):
|
|
295
|
+
route_path = "/" + arg0.value.lstrip("/")
|
|
296
|
+
handler_expr = ast.unparse(arg1) if hasattr(ast, "unparse") else "handler"
|
|
297
|
+
handler_clean = handler_expr.replace(".as_view()", "").strip()
|
|
298
|
+
handler_name = handler_clean.split(".")[-1]
|
|
299
|
+
handler_canon = (
|
|
300
|
+
f"{context.module}.{handler_clean}"
|
|
301
|
+
if not handler_clean.startswith(context.module)
|
|
302
|
+
else handler_clean
|
|
303
|
+
)
|
|
304
|
+
col = getattr(node, "col_offset", None)
|
|
305
|
+
norm = normalize_route_path(route_path, "django")
|
|
306
|
+
|
|
307
|
+
routes.append(
|
|
308
|
+
RouteDetection(
|
|
309
|
+
framework="django",
|
|
310
|
+
http_method="ANY",
|
|
311
|
+
route_path=route_path,
|
|
312
|
+
normalized_route=norm,
|
|
313
|
+
handler_name=handler_name,
|
|
314
|
+
handler_canonical_id=handler_canon,
|
|
315
|
+
file_path=context.file_path,
|
|
316
|
+
line=node.lineno,
|
|
317
|
+
column=col,
|
|
318
|
+
confidence="HIGH",
|
|
319
|
+
resolution_status="RESOLVED",
|
|
320
|
+
evidence=f"Django {func_name}('{arg0.value}', {handler_expr})",
|
|
321
|
+
module=context.module,
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
return routes
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def get_python_analyzers(imports_modules: set[str]) -> list[FrameworkAnalyzer]:
|
|
328
|
+
"""Select relevant Python framework analyzers based on concrete source import evidence."""
|
|
329
|
+
analyzers: list[FrameworkAnalyzer] = []
|
|
330
|
+
has_flask = any("flask" in m for m in imports_modules)
|
|
331
|
+
has_fastapi = any("fastapi" in m for m in imports_modules)
|
|
332
|
+
has_django = any("django" in m for m in imports_modules)
|
|
333
|
+
|
|
334
|
+
if has_flask:
|
|
335
|
+
analyzers.append(FlaskAnalyzer())
|
|
336
|
+
if has_fastapi:
|
|
337
|
+
analyzers.append(FastAPIAnalyzer())
|
|
338
|
+
if has_django:
|
|
339
|
+
analyzers.append(DjangoAnalyzer())
|
|
340
|
+
|
|
341
|
+
# If no framework import is explicitly present, enable all 3 conservatively so route decorators
|
|
342
|
+
# in files where imports are implicit or aliased are still recognized
|
|
343
|
+
if not analyzers:
|
|
344
|
+
analyzers = [FlaskAnalyzer(), FastAPIAnalyzer(), DjangoAnalyzer()]
|
|
345
|
+
|
|
346
|
+
return analyzers
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ---------------------------------------------------------------------------
|
|
350
|
+
# JavaScript / TypeScript Framework Analyzers
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
_EXPRESS_CALL_RE = re.compile(
|
|
354
|
+
r"\b(?:app|router)\.(get|post|put|delete|patch|options|head|all)\s*\(\s*['\"]([^'\"]+)['\"]\s*,\s*([A-Za-z_$][\w$.]*)",
|
|
355
|
+
re.IGNORECASE,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
_NEXT_ROUTE_EXPORT_RE = re.compile(
|
|
359
|
+
r"export\s+(?:async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)\s*\("
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def analyze_js_ts_line(
|
|
364
|
+
line: str,
|
|
365
|
+
lineno: int,
|
|
366
|
+
file_path: str,
|
|
367
|
+
module: str,
|
|
368
|
+
imports_modules: set[str],
|
|
369
|
+
) -> list[RouteDetection]:
|
|
370
|
+
"""Analyze a single JS/TS line during single-pass scan for Express and Next.js routes."""
|
|
371
|
+
routes: list[RouteDetection] = []
|
|
372
|
+
|
|
373
|
+
# 1. Express route registration
|
|
374
|
+
for m in _EXPRESS_CALL_RE.finditer(line):
|
|
375
|
+
method = m.group(1).upper()
|
|
376
|
+
if method == "ALL":
|
|
377
|
+
method = "ANY"
|
|
378
|
+
route_path = m.group(2)
|
|
379
|
+
handler_expr = m.group(3)
|
|
380
|
+
handler_name = handler_expr.split(".")[-1]
|
|
381
|
+
handler_canon = f"{module}.{handler_expr}"
|
|
382
|
+
norm = normalize_route_path(route_path, "express")
|
|
383
|
+
|
|
384
|
+
routes.append(
|
|
385
|
+
RouteDetection(
|
|
386
|
+
framework="express",
|
|
387
|
+
http_method=method,
|
|
388
|
+
route_path=route_path,
|
|
389
|
+
normalized_route=norm,
|
|
390
|
+
handler_name=handler_name,
|
|
391
|
+
handler_canonical_id=handler_canon,
|
|
392
|
+
file_path=file_path,
|
|
393
|
+
line=lineno,
|
|
394
|
+
column=m.start(),
|
|
395
|
+
confidence="HIGH",
|
|
396
|
+
resolution_status="RESOLVED",
|
|
397
|
+
evidence=f"Express route registration {m.group(0)}",
|
|
398
|
+
module=module,
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# 2. Next.js App Router route handlers (e.g. app/api/login/route.ts)
|
|
403
|
+
norm_path = file_path.replace("\\", "/")
|
|
404
|
+
is_next_app_route = "/route." in norm_path or norm_path.startswith("route.")
|
|
405
|
+
has_next_import = any("next" in m.lower() for m in imports_modules)
|
|
406
|
+
|
|
407
|
+
if is_next_app_route or has_next_import:
|
|
408
|
+
for m in _NEXT_ROUTE_EXPORT_RE.finditer(line):
|
|
409
|
+
method = m.group(1).upper()
|
|
410
|
+
route_path = "/" + norm_path.rsplit("/", 1)[0] if "/" in norm_path else "/"
|
|
411
|
+
for prefix in ("/src/app", "/app", "/src/pages", "/pages"):
|
|
412
|
+
if route_path.startswith(prefix):
|
|
413
|
+
route_path = route_path[len(prefix) :] or "/"
|
|
414
|
+
break
|
|
415
|
+
norm = normalize_route_path(route_path, "nextjs")
|
|
416
|
+
routes.append(
|
|
417
|
+
RouteDetection(
|
|
418
|
+
framework="nextjs",
|
|
419
|
+
http_method=method,
|
|
420
|
+
route_path=route_path,
|
|
421
|
+
normalized_route=norm,
|
|
422
|
+
handler_name=method,
|
|
423
|
+
handler_canonical_id=f"{module}.{method}",
|
|
424
|
+
file_path=file_path,
|
|
425
|
+
line=lineno,
|
|
426
|
+
column=m.start(),
|
|
427
|
+
confidence="HIGH",
|
|
428
|
+
resolution_status="RESOLVED",
|
|
429
|
+
evidence=f"Next.js App Router HTTP export {method} in {file_path}",
|
|
430
|
+
module=module,
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
return routes
|