codegraph-engine 3.0.0__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.
Files changed (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +859 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,960 @@
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 in ("express", "nestjs"):
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"\[{1,2}(?:\.\.\.)?([A-Za-z_][\w]*)\]{1,2}", r"{\1}", raw)
64
+ return raw
65
+
66
+
67
+ def join_route_prefixes(*parts: str) -> str:
68
+ """Join route prefix segments cleanly and canonically.
69
+
70
+ Examples:
71
+ join_route_prefixes("/api/v1", "/users") -> "/api/v1/users"
72
+ join_route_prefixes("api/v1/", "/users/") -> "/api/v1/users"
73
+ join_route_prefixes("", "/items") -> "/items"
74
+ join_route_prefixes("/api", "") -> "/api"
75
+ join_route_prefixes("/", "/") -> "/"
76
+ """
77
+ cleaned: list[str] = []
78
+ for part in parts:
79
+ p = part.strip()
80
+ if not p or p == "/":
81
+ continue
82
+ p = p.strip("/")
83
+ if p:
84
+ cleaned.append(p)
85
+ if not cleaned:
86
+ return "/"
87
+ return "/" + "/".join(cleaned)
88
+
89
+
90
+ @dataclass(frozen=True)
91
+ class RouterMountDetection:
92
+ """Detection of a router mounting or inclusion statement across frameworks.
93
+
94
+ Examples:
95
+ FastAPI: app.include_router(users_router, prefix="/api/v1")
96
+ Flask: app.register_blueprint(auth_bp, url_prefix="/auth")
97
+ Django: path("api/v1/", include("myapp.urls"))
98
+ Express: app.use("/api/v1", apiRouter)
99
+ """
100
+
101
+ framework: str # flask | fastapi | django | express
102
+ parent_router: str # e.g. "app", "api_router", "urlpatterns"
103
+ child_router: str # e.g. "user_router", "auth_bp", "myapp.urls", "usersRouter"
104
+ prefix: str # e.g. "/api/v1", "/users", "api/v1/"
105
+ file_path: str
106
+ line: int
107
+ column: int | None = None
108
+ confidence: str = "HIGH"
109
+ evidence: str = ""
110
+ module: str = ""
111
+
112
+ def as_dict(self) -> dict[str, object]:
113
+ return asdict(self)
114
+
115
+
116
+ @dataclass(frozen=True)
117
+ class RouterDefinition:
118
+ """Detection of a router instantiation with an inherent base prefix.
119
+
120
+ Examples:
121
+ FastAPI: router = APIRouter(prefix="/items")
122
+ Flask: bp = Blueprint("auth", __name__, url_prefix="/auth")
123
+ """
124
+
125
+ framework: str # flask | fastapi
126
+ router_name: str # e.g. "router", "auth_bp"
127
+ prefix: str # e.g. "/items", "/auth"
128
+ file_path: str
129
+ line: int
130
+ column: int | None = None
131
+ evidence: str = ""
132
+ module: str = ""
133
+
134
+ def as_dict(self) -> dict[str, object]:
135
+ return asdict(self)
136
+
137
+
138
+ @dataclass(frozen=True)
139
+ class RouteDetection:
140
+ framework: str # flask | fastapi | django | express | nextjs
141
+ http_method: str # GET | POST | PUT | DELETE | PATCH | OPTIONS | HEAD | ANY
142
+ route_path: str # Original framework route string, e.g. /users/<int:id>
143
+ normalized_route: str # Canonical route string, e.g. /users/{id}
144
+ handler_name: str # e.g. "login_controller"
145
+ handler_canonical_id: str # Standard canonical symbol ID, e.g. "src.auth.routes.login_controller"
146
+ file_path: str
147
+ line: int
148
+ column: int | None = None
149
+ confidence: str = "HIGH"
150
+ resolution_status: str = "RESOLVED" # RESOLVED | UNRESOLVED | AMBIGUOUS
151
+ evidence: str = ""
152
+ module: str = ""
153
+ endpoint_id_override: str | None = None
154
+ router_name: str = ""
155
+
156
+ @property
157
+ def route_signature(self) -> str:
158
+ """Human-readable route signature, e.g. 'POST /login'."""
159
+ return f"{self.http_method} {self.route_path}"
160
+
161
+ @property
162
+ def endpoint_id(self) -> str:
163
+ """Stable, deterministic, globally unique endpoint identifier for a route declaration."""
164
+ if self.endpoint_id_override:
165
+ return self.endpoint_id_override
166
+
167
+ clean_path = self.file_path.replace("\\", "/").strip()
168
+ while clean_path.startswith("./"):
169
+ clean_path = clean_path[2:]
170
+ clean_path = clean_path.lstrip("/") or "root"
171
+
172
+ handler_part = f"#{self.handler_name}" if self.handler_name else ""
173
+ fw_part = f":{self.framework}" if self.framework else ""
174
+ return f"API_ENDPOINT{fw_part}:{clean_path}:{self.line}:{self.http_method} {self.normalized_route}{handler_part}"
175
+
176
+ def as_dict(self) -> dict[str, object]:
177
+ return {
178
+ "endpoint_id": self.endpoint_id,
179
+ "route_signature": self.route_signature,
180
+ "framework": self.framework,
181
+ "http_method": self.http_method,
182
+ "route_path": self.route_path,
183
+ "normalized_route": self.normalized_route,
184
+ "handler_name": self.handler_name,
185
+ "handler_canonical_id": self.handler_canonical_id,
186
+ "file": self.file_path,
187
+ "file_path": self.file_path,
188
+ "line": self.line,
189
+ "column": self.column,
190
+ "confidence": self.confidence,
191
+ "resolution_status": self.resolution_status,
192
+ "evidence": self.evidence,
193
+ "router_name": self.router_name,
194
+ }
195
+
196
+
197
+ @dataclass(frozen=True)
198
+ class AnalysisContext:
199
+ file_path: str
200
+ module: str
201
+ imports_modules: set[str]
202
+ scope_qname: str
203
+ scope_canonical_id: str
204
+ scope_kind: str
205
+
206
+
207
+ class FrameworkAnalyzer(Protocol):
208
+ framework: str
209
+
210
+ def analyze_python_node(
211
+ self,
212
+ node: ast.AST,
213
+ context: AnalysisContext,
214
+ ) -> list[RouteDetection]: ...
215
+
216
+ def analyze_python_mounts(
217
+ self,
218
+ node: ast.AST,
219
+ context: AnalysisContext,
220
+ ) -> list[RouterMountDetection]: ...
221
+
222
+ def analyze_python_definitions(
223
+ self,
224
+ node: ast.AST,
225
+ context: AnalysisContext,
226
+ ) -> list[RouterDefinition]: ...
227
+
228
+
229
+ # ---------------------------------------------------------------------------
230
+ # Python Framework Analyzers
231
+ # ---------------------------------------------------------------------------
232
+
233
+ _FLASK_METHODS = {"route", "get", "post", "put", "delete", "patch"}
234
+
235
+
236
+ class FlaskAnalyzer:
237
+ framework = "flask"
238
+
239
+ def analyze_python_node(
240
+ self,
241
+ node: ast.AST,
242
+ context: AnalysisContext,
243
+ ) -> list[RouteDetection]:
244
+ routes: list[RouteDetection] = []
245
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
246
+ return routes
247
+
248
+ for dec in node.decorator_list:
249
+ if not isinstance(dec, ast.Call) or not isinstance(dec.func, ast.Attribute):
250
+ continue
251
+ attr = dec.func.attr.lower()
252
+ if attr not in _FLASK_METHODS or not dec.args:
253
+ continue
254
+
255
+ first_arg = dec.args[0]
256
+ if not isinstance(first_arg, ast.Constant) or not isinstance(first_arg.value, str):
257
+ continue
258
+
259
+ route_path = first_arg.value
260
+ methods = [attr.upper()] if attr != "route" else ["GET"]
261
+
262
+ for kw in dec.keywords:
263
+ if kw.arg == "methods" and isinstance(kw.value, (ast.List, ast.Tuple)):
264
+ methods = [
265
+ e.value.upper()
266
+ for e in kw.value.elts
267
+ if isinstance(e, ast.Constant) and isinstance(e.value, str)
268
+ ] or ["GET"]
269
+
270
+ col = getattr(dec, "col_offset", None)
271
+ norm = normalize_route_path(route_path, "flask")
272
+ handler_canon = (
273
+ f"{context.scope_canonical_id}.{node.name}"
274
+ if context.scope_qname
275
+ else f"{context.module}.{node.name}"
276
+ )
277
+ router_name = ast.unparse(dec.func.value) if hasattr(ast, "unparse") else "app"
278
+
279
+ for method in methods:
280
+ routes.append(
281
+ RouteDetection(
282
+ framework="flask",
283
+ http_method=method,
284
+ route_path=route_path,
285
+ normalized_route=norm,
286
+ handler_name=node.name,
287
+ handler_canonical_id=handler_canon,
288
+ file_path=context.file_path,
289
+ line=dec.lineno,
290
+ column=col,
291
+ confidence="HIGH",
292
+ resolution_status="RESOLVED",
293
+ evidence=f"Flask decorator @{ast.unparse(dec.func)}('{route_path}') on {node.name}",
294
+ module=context.module,
295
+ router_name=router_name,
296
+ )
297
+ )
298
+ return routes
299
+
300
+ def analyze_python_mounts(
301
+ self,
302
+ node: ast.AST,
303
+ context: AnalysisContext,
304
+ ) -> list[RouterMountDetection]:
305
+ mounts: list[RouterMountDetection] = []
306
+ if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
307
+ return mounts
308
+
309
+ if node.func.attr == "register_blueprint" and node.args:
310
+ parent_expr = ast.unparse(node.func.value) if hasattr(ast, "unparse") else "app"
311
+ child_expr = ast.unparse(node.args[0]) if hasattr(ast, "unparse") else "blueprint"
312
+ prefix = ""
313
+ for kw in node.keywords:
314
+ if kw.arg == "url_prefix" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
315
+ prefix = kw.value.value
316
+ col = getattr(node, "col_offset", None)
317
+ mounts.append(
318
+ RouterMountDetection(
319
+ framework="flask",
320
+ parent_router=parent_expr,
321
+ child_router=child_expr,
322
+ prefix=prefix,
323
+ file_path=context.file_path,
324
+ line=node.lineno,
325
+ column=col,
326
+ confidence="HIGH",
327
+ evidence=f"Flask register_blueprint: {ast.unparse(node) if hasattr(ast, 'unparse') else 'register_blueprint'}",
328
+ module=context.module,
329
+ )
330
+ )
331
+ return mounts
332
+
333
+ def analyze_python_definitions(
334
+ self,
335
+ node: ast.AST,
336
+ context: AnalysisContext,
337
+ ) -> list[RouterDefinition]:
338
+ defs: list[RouterDefinition] = []
339
+ target_name = ""
340
+ call_node: ast.Call | None = None
341
+
342
+ if isinstance(node, ast.Assign) and node.targets and isinstance(node.value, ast.Call):
343
+ if isinstance(node.targets[0], ast.Name):
344
+ target_name = node.targets[0].id
345
+ call_node = node.value
346
+ elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and isinstance(node.value, ast.Call):
347
+ target_name = node.target.id
348
+ call_node = node.value
349
+
350
+ if call_node and hasattr(ast, "unparse"):
351
+ func_expr = ast.unparse(call_node.func)
352
+ if func_expr in ("Blueprint", "flask.Blueprint") or func_expr.endswith(".Blueprint"):
353
+ prefix = ""
354
+ for kw in call_node.keywords:
355
+ if kw.arg == "url_prefix" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
356
+ prefix = kw.value.value
357
+ line = getattr(node, "lineno", 1)
358
+ col = getattr(node, "col_offset", None)
359
+ defs.append(
360
+ RouterDefinition(
361
+ framework="flask",
362
+ router_name=target_name or "bp",
363
+ prefix=prefix,
364
+ file_path=context.file_path,
365
+ line=line,
366
+ column=col,
367
+ evidence=f"Flask Blueprint definition: {ast.unparse(node)}",
368
+ module=context.module,
369
+ )
370
+ )
371
+ return defs
372
+
373
+
374
+ _FASTAPI_METHODS = {"get", "post", "put", "delete", "patch", "options", "head", "api_route"}
375
+
376
+
377
+ class FastAPIAnalyzer:
378
+ framework = "fastapi"
379
+
380
+ def analyze_python_node(
381
+ self,
382
+ node: ast.AST,
383
+ context: AnalysisContext,
384
+ ) -> list[RouteDetection]:
385
+ routes: list[RouteDetection] = []
386
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
387
+ return routes
388
+
389
+ for dec in node.decorator_list:
390
+ if not isinstance(dec, ast.Call) or not isinstance(dec.func, ast.Attribute):
391
+ continue
392
+ attr = dec.func.attr.lower()
393
+ if attr not in _FASTAPI_METHODS or not dec.args:
394
+ continue
395
+
396
+ first_arg = dec.args[0]
397
+ if not isinstance(first_arg, ast.Constant) or not isinstance(first_arg.value, str):
398
+ continue
399
+
400
+ route_path = first_arg.value
401
+ methods = [attr.upper()] if attr != "api_route" else ["GET"]
402
+
403
+ for kw in dec.keywords:
404
+ if kw.arg == "methods" and isinstance(kw.value, (ast.List, ast.Tuple)):
405
+ methods = [
406
+ e.value.upper()
407
+ for e in kw.value.elts
408
+ if isinstance(e, ast.Constant) and isinstance(e.value, str)
409
+ ] or ["GET"]
410
+
411
+ col = getattr(dec, "col_offset", None)
412
+ norm = normalize_route_path(route_path, "fastapi")
413
+ handler_canon = (
414
+ f"{context.scope_canonical_id}.{node.name}"
415
+ if context.scope_qname
416
+ else f"{context.module}.{node.name}"
417
+ )
418
+ router_name = ast.unparse(dec.func.value) if hasattr(ast, "unparse") else "app"
419
+
420
+ for method in methods:
421
+ routes.append(
422
+ RouteDetection(
423
+ framework="fastapi",
424
+ http_method=method,
425
+ route_path=route_path,
426
+ normalized_route=norm,
427
+ handler_name=node.name,
428
+ handler_canonical_id=handler_canon,
429
+ file_path=context.file_path,
430
+ line=dec.lineno,
431
+ column=col,
432
+ confidence="HIGH",
433
+ resolution_status="RESOLVED",
434
+ evidence=f"FastAPI decorator @{ast.unparse(dec.func)}('{route_path}') on {node.name}",
435
+ module=context.module,
436
+ router_name=router_name,
437
+ )
438
+ )
439
+ return routes
440
+
441
+ def analyze_python_mounts(
442
+ self,
443
+ node: ast.AST,
444
+ context: AnalysisContext,
445
+ ) -> list[RouterMountDetection]:
446
+ mounts: list[RouterMountDetection] = []
447
+ if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
448
+ return mounts
449
+
450
+ if node.func.attr == "include_router" and node.args:
451
+ parent_expr = ast.unparse(node.func.value) if hasattr(ast, "unparse") else "app"
452
+ child_expr = ast.unparse(node.args[0]) if hasattr(ast, "unparse") else "router"
453
+ prefix = ""
454
+ for kw in node.keywords:
455
+ if kw.arg == "prefix" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
456
+ prefix = kw.value.value
457
+ col = getattr(node, "col_offset", None)
458
+ mounts.append(
459
+ RouterMountDetection(
460
+ framework="fastapi",
461
+ parent_router=parent_expr,
462
+ child_router=child_expr,
463
+ prefix=prefix,
464
+ file_path=context.file_path,
465
+ line=node.lineno,
466
+ column=col,
467
+ confidence="HIGH",
468
+ evidence=f"FastAPI include_router: {ast.unparse(node) if hasattr(ast, 'unparse') else 'include_router'}",
469
+ module=context.module,
470
+ )
471
+ )
472
+ return mounts
473
+
474
+ def analyze_python_definitions(
475
+ self,
476
+ node: ast.AST,
477
+ context: AnalysisContext,
478
+ ) -> list[RouterDefinition]:
479
+ defs: list[RouterDefinition] = []
480
+ target_name = ""
481
+ call_node: ast.Call | None = None
482
+
483
+ if isinstance(node, ast.Assign) and node.targets and isinstance(node.value, ast.Call):
484
+ if isinstance(node.targets[0], ast.Name):
485
+ target_name = node.targets[0].id
486
+ call_node = node.value
487
+ elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and isinstance(node.value, ast.Call):
488
+ target_name = node.target.id
489
+ call_node = node.value
490
+
491
+ if call_node and hasattr(ast, "unparse"):
492
+ func_expr = ast.unparse(call_node.func)
493
+ if "APIRouter" in func_expr or func_expr.endswith("Router"):
494
+ prefix = ""
495
+ for kw in call_node.keywords:
496
+ if kw.arg == "prefix" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
497
+ prefix = kw.value.value
498
+ line = getattr(node, "lineno", 1)
499
+ col = getattr(node, "col_offset", None)
500
+ defs.append(
501
+ RouterDefinition(
502
+ framework="fastapi",
503
+ router_name=target_name or "router",
504
+ prefix=prefix,
505
+ file_path=context.file_path,
506
+ line=line,
507
+ column=col,
508
+ evidence=f"FastAPI APIRouter definition: {ast.unparse(node)}",
509
+ module=context.module,
510
+ )
511
+ )
512
+ return defs
513
+
514
+
515
+ class DjangoAnalyzer:
516
+ framework = "django"
517
+
518
+ def analyze_python_node(
519
+ self,
520
+ node: ast.AST,
521
+ context: AnalysisContext,
522
+ ) -> list[RouteDetection]:
523
+ routes: list[RouteDetection] = []
524
+ if not isinstance(node, ast.Call):
525
+ return routes
526
+
527
+ func_name = ""
528
+ if isinstance(node.func, ast.Name):
529
+ func_name = node.func.id
530
+ elif isinstance(node.func, ast.Attribute):
531
+ func_name = node.func.attr
532
+
533
+ if func_name in ("path", "re_path") and len(node.args) >= 2:
534
+ arg0, arg1 = node.args[0], node.args[1]
535
+
536
+ # If second argument is an include(...) call, do NOT record as a direct route
537
+ if isinstance(arg1, ast.Call) and hasattr(ast, "unparse"):
538
+ callee = ast.unparse(arg1.func)
539
+ if callee in ("include", "django.urls.include") or callee.endswith(".include"):
540
+ return routes
541
+
542
+ if isinstance(arg0, ast.Constant) and isinstance(arg0.value, str):
543
+ route_path = "/" + arg0.value.lstrip("/")
544
+ handler_expr = ast.unparse(arg1) if hasattr(ast, "unparse") else "handler"
545
+ handler_clean = handler_expr.replace(".as_view()", "").strip()
546
+ handler_name = handler_clean.split(".")[-1]
547
+ handler_canon = (
548
+ f"{context.module}.{handler_clean}"
549
+ if not handler_clean.startswith(context.module)
550
+ else handler_clean
551
+ )
552
+ col = getattr(node, "col_offset", None)
553
+ norm = normalize_route_path(route_path, "django")
554
+
555
+ routes.append(
556
+ RouteDetection(
557
+ framework="django",
558
+ http_method="ANY",
559
+ route_path=route_path,
560
+ normalized_route=norm,
561
+ handler_name=handler_name,
562
+ handler_canonical_id=handler_canon,
563
+ file_path=context.file_path,
564
+ line=node.lineno,
565
+ column=col,
566
+ confidence="HIGH",
567
+ resolution_status="RESOLVED",
568
+ evidence=f"Django {func_name}('{arg0.value}', {handler_expr})",
569
+ module=context.module,
570
+ router_name="urlpatterns",
571
+ )
572
+ )
573
+ return routes
574
+
575
+ def analyze_python_mounts(
576
+ self,
577
+ node: ast.AST,
578
+ context: AnalysisContext,
579
+ ) -> list[RouterMountDetection]:
580
+ mounts: list[RouterMountDetection] = []
581
+ if not isinstance(node, ast.Call):
582
+ return mounts
583
+
584
+ func_name = ""
585
+ if isinstance(node.func, ast.Name):
586
+ func_name = node.func.id
587
+ elif isinstance(node.func, ast.Attribute):
588
+ func_name = node.func.attr
589
+
590
+ if func_name in ("path", "re_path") and len(node.args) >= 2:
591
+ arg0, arg1 = node.args[0], node.args[1]
592
+ if isinstance(arg1, ast.Call) and hasattr(ast, "unparse"):
593
+ callee = ast.unparse(arg1.func)
594
+ if callee in ("include", "django.urls.include") or callee.endswith(".include"):
595
+ prefix = arg0.value if isinstance(arg0, ast.Constant) and isinstance(arg0.value, str) else ""
596
+ child = ""
597
+ if arg1.args:
598
+ first = arg1.args[0]
599
+ if isinstance(first, ast.Constant) and isinstance(first.value, str):
600
+ child = first.value
601
+ elif isinstance(first, (ast.Tuple, ast.List)) and first.elts:
602
+ c0 = first.elts[0]
603
+ child = c0.value if isinstance(c0, ast.Constant) and isinstance(c0.value, str) else ast.unparse(c0)
604
+ else:
605
+ child = ast.unparse(first)
606
+ if child:
607
+ col = getattr(node, "col_offset", None)
608
+ mounts.append(
609
+ RouterMountDetection(
610
+ framework="django",
611
+ parent_router="urlpatterns",
612
+ child_router=child,
613
+ prefix=prefix,
614
+ file_path=context.file_path,
615
+ line=node.lineno,
616
+ column=col,
617
+ confidence="HIGH",
618
+ evidence=f"Django include: {ast.unparse(node)}",
619
+ module=context.module,
620
+ )
621
+ )
622
+ return mounts
623
+
624
+ def analyze_python_definitions(
625
+ self,
626
+ node: ast.AST,
627
+ context: AnalysisContext,
628
+ ) -> list[RouterDefinition]:
629
+ return []
630
+
631
+
632
+ def get_python_analyzers(imports_modules: set[str]) -> list[FrameworkAnalyzer]:
633
+ """Select relevant Python framework analyzers based on concrete source import evidence."""
634
+ analyzers: list[FrameworkAnalyzer] = []
635
+ has_flask = any("flask" in m for m in imports_modules)
636
+ has_fastapi = any("fastapi" in m for m in imports_modules)
637
+ has_django = any("django" in m for m in imports_modules)
638
+
639
+ if has_flask:
640
+ analyzers.append(FlaskAnalyzer())
641
+ if has_fastapi:
642
+ analyzers.append(FastAPIAnalyzer())
643
+ if has_django:
644
+ analyzers.append(DjangoAnalyzer())
645
+
646
+ # If no framework import is explicitly present, enable all 3 conservatively so route decorators
647
+ # in files where imports are implicit or aliased are still recognized
648
+ if not analyzers:
649
+ analyzers = [FlaskAnalyzer(), FastAPIAnalyzer(), DjangoAnalyzer()]
650
+
651
+ return analyzers
652
+
653
+
654
+ # ---------------------------------------------------------------------------
655
+ # ---------------------------------------------------------------------------
656
+ # JavaScript / TypeScript Framework Analyzers
657
+ # ---------------------------------------------------------------------------
658
+
659
+ _EXPRESS_CALL_RE = re.compile(
660
+ r"\b([A-Za-z_$][\w$]*)\.(get|post|put|delete|patch|options|head|all)\s*\(\s*['\"]([^'\"]+)['\"]\s*,\s*(.+)",
661
+ re.IGNORECASE,
662
+ )
663
+
664
+ _EXPRESS_USE_RE = re.compile(
665
+ r"\b([A-Za-z_$][\w$]*)\.use\s*\(\s*(?:['\"]([^'\"]+)['\"]\s*,\s*)?([A-Za-z_$][\w$.]*)\s*\)",
666
+ re.IGNORECASE,
667
+ )
668
+
669
+ _EXPRESS_IGNORE_USE = {
670
+ "express.json",
671
+ "express.urlencoded",
672
+ "express.static",
673
+ "cors",
674
+ "helmet",
675
+ "morgan",
676
+ "cookieparser",
677
+ "bodyparser",
678
+ }
679
+
680
+ _NON_HANDLER_CALLS = {
681
+ "async",
682
+ "await",
683
+ "function",
684
+ "if",
685
+ "return",
686
+ "try",
687
+ "catch",
688
+ "switch",
689
+ "while",
690
+ "for",
691
+ "throw",
692
+ "res.send",
693
+ "res.json",
694
+ "res.status",
695
+ "res.sendstatus",
696
+ "res.redirect",
697
+ "res.setheader",
698
+ "res.cookie",
699
+ "res.end",
700
+ "next",
701
+ "console.log",
702
+ "console.error",
703
+ "console.warn",
704
+ "console.info",
705
+ "object.assign",
706
+ "promise.all",
707
+ "promise.resolve",
708
+ "promise.reject",
709
+ "require",
710
+ "typeof",
711
+ "then",
712
+ "settimeout",
713
+ "cleartimeout",
714
+ "setinterval",
715
+ "clearinterval",
716
+ }
717
+
718
+
719
+ def split_js_arguments(args_str: str) -> list[str]:
720
+ """Split comma-separated JavaScript arguments respecting nested parens, brackets, braces, and quotes."""
721
+ args: list[str] = []
722
+ current: list[str] = []
723
+ depth_paren = 0
724
+ depth_bracket = 0
725
+ depth_brace = 0
726
+ in_quote = False
727
+ quote_char = ""
728
+ escape = False
729
+
730
+ for ch in args_str:
731
+ if escape:
732
+ current.append(ch)
733
+ escape = False
734
+ continue
735
+ if ch == "\\":
736
+ current.append(ch)
737
+ escape = True
738
+ continue
739
+ if in_quote:
740
+ current.append(ch)
741
+ if ch == quote_char:
742
+ in_quote = False
743
+ continue
744
+ if ch in ('"', "'", "`"):
745
+ in_quote = True
746
+ quote_char = ch
747
+ current.append(ch)
748
+ continue
749
+
750
+ if ch == "(":
751
+ depth_paren += 1
752
+ elif ch == ")":
753
+ if depth_paren == 0 and depth_bracket == 0 and depth_brace == 0:
754
+ # Trailing closing paren of router call reached
755
+ break
756
+ depth_paren = max(0, depth_paren - 1)
757
+ elif ch == "[":
758
+ depth_bracket += 1
759
+ elif ch == "]":
760
+ depth_bracket = max(0, depth_bracket - 1)
761
+ elif ch == "{":
762
+ depth_brace += 1
763
+ elif ch == "}":
764
+ depth_brace = max(0, depth_brace - 1)
765
+
766
+ if ch == "," and depth_paren == 0 and depth_bracket == 0 and depth_brace == 0:
767
+ arg = "".join(current).strip()
768
+ if arg:
769
+ args.append(arg)
770
+ current = []
771
+ else:
772
+ current.append(ch)
773
+
774
+ arg = "".join(current).strip()
775
+ if arg:
776
+ args.append(arg)
777
+ return args
778
+
779
+
780
+ def _clean_middleware_token(raw_tok: str) -> str:
781
+ """Extract clean identifier from middleware expression, e.g. requireRoles('admin') -> requireRoles."""
782
+ tok = raw_tok.strip()
783
+ if tok.startswith("[") and tok.endswith("]"):
784
+ tok = tok[1:-1].strip()
785
+ m = re.match(r"^([A-Za-z_$][\w$]*)\s*(?:\(|$)", tok)
786
+ if m:
787
+ return m.group(1)
788
+ return tok.split("(")[0].strip()
789
+
790
+
791
+ def _fallback_handler_name(route_path: str) -> str:
792
+ """Generate a readable fallback handler name from the route path."""
793
+ clean_parts = [p for p in route_path.strip("/").split("/") if p and not p.startswith(":") and not p.startswith("{")]
794
+ if clean_parts:
795
+ return f"{clean_parts[-1]}_handler"
796
+ return "root_handler"
797
+
798
+
799
+ def _resolve_express_handler_and_middleware(
800
+ raw_args: str,
801
+ route_path: str,
802
+ ) -> tuple[str, str, list[str]]:
803
+ """Look past middleware to resolve the terminal route handler and list of middlewares."""
804
+ args = split_js_arguments(raw_args)
805
+ if not args:
806
+ name = _fallback_handler_name(route_path)
807
+ return name, name, []
808
+
809
+ # Intermediate arguments are middleware
810
+ middlewares: list[str] = []
811
+ for mw in args[:-1]:
812
+ clean_mw = _clean_middleware_token(mw)
813
+ if clean_mw:
814
+ middlewares.append(clean_mw)
815
+
816
+ last_arg = args[-1].strip()
817
+
818
+ # Case 1: Simple named identifier or member access, e.g. uploadInvoiceFile or controller.uploadInvoiceFile
819
+ id_match = re.match(r"^([A-Za-z_$][\w$.]*)$", last_arg)
820
+ if id_match:
821
+ expr = id_match.group(1)
822
+ name = expr.split(".")[-1]
823
+ return name, expr, middlewares
824
+
825
+ # Case 2: Arrow function or function expression, e.g. async (req, res) => uploadInvoiceFile(req, res)
826
+ # Search for business function calls inside the function body
827
+ calls = re.findall(r"\b([A-Za-z_$][\w$.]*)\s*\(", last_arg)
828
+ mw_set = set(middlewares)
829
+ for call in calls:
830
+ clean_call = call.split(".")[-1]
831
+ if (
832
+ call.lower() not in _NON_HANDLER_CALLS
833
+ and clean_call.lower() not in _NON_HANDLER_CALLS
834
+ and clean_call not in mw_set
835
+ ):
836
+ return clean_call, call, middlewares
837
+
838
+ # Case 3: Anonymous inline handler without distinct call
839
+ name = _fallback_handler_name(route_path)
840
+ return name, name, middlewares
841
+
842
+
843
+ _NEXT_ROUTE_EXPORT_RE = re.compile(
844
+ r"export\s+(?:async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)\s*\("
845
+ )
846
+
847
+
848
+ def analyze_js_ts_line(
849
+ line: str,
850
+ lineno: int,
851
+ file_path: str,
852
+ module: str,
853
+ imports_modules: set[str],
854
+ ) -> list[RouteDetection]:
855
+ """Analyze a single JS/TS line during single-pass scan for Express and Next.js routes."""
856
+ routes: list[RouteDetection] = []
857
+
858
+ # 1. Express route registration
859
+ for m in _EXPRESS_CALL_RE.finditer(line):
860
+ router_obj = m.group(1)
861
+ # Check if caller looks like an Express router or app
862
+ if router_obj.lower() not in ("app", "router") and not router_obj.lower().endswith("router") and not router_obj.lower().endswith("routes"):
863
+ continue
864
+
865
+ method = m.group(2).upper()
866
+ if method == "ALL":
867
+ method = "ANY"
868
+ route_path = m.group(3)
869
+ raw_args = m.group(4)
870
+ handler_name, handler_expr, middlewares = _resolve_express_handler_and_middleware(raw_args, route_path)
871
+ handler_canon = f"{module}.{handler_expr}"
872
+ norm = normalize_route_path(route_path, "express")
873
+ mw_str = f" (middleware: {', '.join(middlewares)})" if middlewares else ""
874
+
875
+ routes.append(
876
+ RouteDetection(
877
+ framework="express",
878
+ http_method=method,
879
+ route_path=route_path,
880
+ normalized_route=norm,
881
+ handler_name=handler_name,
882
+ handler_canonical_id=handler_canon,
883
+ file_path=file_path,
884
+ line=lineno,
885
+ column=m.start(),
886
+ confidence="HIGH",
887
+ resolution_status="RESOLVED",
888
+ evidence=f"Express route registration {router_obj}.{method.lower()}('{route_path}', ...){mw_str}",
889
+ module=module,
890
+ router_name=router_obj,
891
+ )
892
+ )
893
+
894
+ # 2. Next.js App Router route handlers (e.g. app/api/login/route.ts)
895
+ norm_path = file_path.replace("\\", "/")
896
+ is_next_app_route = "/route." in norm_path or norm_path.startswith("route.")
897
+ has_next_import = any("next" in m.lower() for m in imports_modules)
898
+
899
+ if is_next_app_route or has_next_import:
900
+ for m in _NEXT_ROUTE_EXPORT_RE.finditer(line):
901
+ method = m.group(1).upper()
902
+ route_path = "/" + norm_path.rsplit("/", 1)[0] if "/" in norm_path else "/"
903
+ for prefix in ("/src/app", "/app", "/src/pages", "/pages"):
904
+ if route_path.startswith(prefix):
905
+ route_path = route_path[len(prefix) :] or "/"
906
+ break
907
+ norm = normalize_route_path(route_path, "nextjs")
908
+ routes.append(
909
+ RouteDetection(
910
+ framework="nextjs",
911
+ http_method=method,
912
+ route_path=route_path,
913
+ normalized_route=norm,
914
+ handler_name=method,
915
+ handler_canonical_id=f"{module}.{method}",
916
+ file_path=file_path,
917
+ line=lineno,
918
+ column=m.start(),
919
+ confidence="HIGH",
920
+ resolution_status="RESOLVED",
921
+ evidence=f"Next.js App Router HTTP export {method} in {file_path}",
922
+ module=module,
923
+ router_name="export",
924
+ )
925
+ )
926
+
927
+ return routes
928
+
929
+
930
+ def analyze_js_ts_mounts(
931
+ line: str,
932
+ lineno: int,
933
+ file_path: str,
934
+ module: str,
935
+ ) -> list[RouterMountDetection]:
936
+ """Analyze a single JS/TS line for Express router mounting statements (e.g. app.use('/api', router))."""
937
+ mounts: list[RouterMountDetection] = []
938
+ for m in _EXPRESS_USE_RE.finditer(line):
939
+ parent = m.group(1)
940
+ prefix = m.group(2) or ""
941
+ child = m.group(3)
942
+
943
+ if child.lower() in _EXPRESS_IGNORE_USE or parent.lower() in _EXPRESS_IGNORE_USE:
944
+ continue
945
+
946
+ mounts.append(
947
+ RouterMountDetection(
948
+ framework="express",
949
+ parent_router=parent,
950
+ child_router=child,
951
+ prefix=prefix,
952
+ file_path=file_path,
953
+ line=lineno,
954
+ column=m.start(),
955
+ confidence="HIGH",
956
+ evidence=f"Express mount {m.group(0)}",
957
+ module=module,
958
+ )
959
+ )
960
+ return mounts