commiter-cli 0.3.0__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.
- commiter/__init__.py +3 -0
- commiter/adapters/__init__.py +0 -0
- commiter/adapters/base.py +96 -0
- commiter/adapters/django_rest.py +247 -0
- commiter/adapters/express.py +204 -0
- commiter/adapters/fastapi.py +170 -0
- commiter/adapters/flask.py +169 -0
- commiter/adapters/nextjs.py +180 -0
- commiter/adapters/prisma.py +76 -0
- commiter/adapters/raw_sql.py +191 -0
- commiter/adapters/react.py +129 -0
- commiter/adapters/sqlalchemy.py +99 -0
- commiter/adapters/supabase.py +68 -0
- commiter/auth.py +130 -0
- commiter/cli.py +667 -0
- commiter/correlator.py +208 -0
- commiter/extractors/__init__.py +0 -0
- commiter/extractors/api_calls.py +91 -0
- commiter/extractors/api_endpoints.py +354 -0
- commiter/extractors/backend_files.py +33 -0
- commiter/extractors/base.py +40 -0
- commiter/extractors/db_operations.py +69 -0
- commiter/extractors/dependencies.py +219 -0
- commiter/generic_resolver.py +204 -0
- commiter/handler_index.py +97 -0
- commiter/lib.py +63 -0
- commiter/middleware_index.py +350 -0
- commiter/models.py +117 -0
- commiter/parser.py +1283 -0
- commiter/prefix_index.py +211 -0
- commiter/report/__init__.py +0 -0
- commiter/report/ai.py +120 -0
- commiter/report/api_guide.py +217 -0
- commiter/report/architecture.py +930 -0
- commiter/report/console.py +254 -0
- commiter/report/json_output.py +122 -0
- commiter/report/markdown.py +163 -0
- commiter/scanner.py +383 -0
- commiter/type_index.py +304 -0
- commiter/uploader.py +46 -0
- commiter/utils/__init__.py +0 -0
- commiter/utils/env_reader.py +78 -0
- commiter/utils/file_classifier.py +187 -0
- commiter/utils/path_helpers.py +73 -0
- commiter/utils/tsconfig_resolver.py +281 -0
- commiter/wrapper_index.py +288 -0
- commiter_cli-0.3.0.dist-info/METADATA +14 -0
- commiter_cli-0.3.0.dist-info/RECORD +96 -0
- commiter_cli-0.3.0.dist-info/WHEEL +5 -0
- commiter_cli-0.3.0.dist-info/entry_points.txt +2 -0
- commiter_cli-0.3.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/fixtures/arch_backend/app.py +22 -0
- tests/fixtures/arch_backend/middleware/__init__.py +0 -0
- tests/fixtures/arch_backend/middleware/rate_limit.py +4 -0
- tests/fixtures/arch_backend/routes/__init__.py +0 -0
- tests/fixtures/arch_backend/routes/analytics.py +20 -0
- tests/fixtures/arch_backend/routes/auth.py +29 -0
- tests/fixtures/arch_backend/routes/projects.py +60 -0
- tests/fixtures/arch_backend/routes/users.py +55 -0
- tests/fixtures/arch_monorepo/apps/api/app.py +30 -0
- tests/fixtures/arch_monorepo/apps/api/middleware/__init__.py +0 -0
- tests/fixtures/arch_monorepo/apps/api/middleware/auth.py +17 -0
- tests/fixtures/arch_monorepo/apps/api/middleware/rate_limit.py +10 -0
- tests/fixtures/arch_monorepo/apps/api/routes/__init__.py +0 -0
- tests/fixtures/arch_monorepo/apps/api/routes/auth.py +46 -0
- tests/fixtures/arch_monorepo/apps/api/routes/invites.py +30 -0
- tests/fixtures/arch_monorepo/apps/api/routes/notifications.py +25 -0
- tests/fixtures/arch_monorepo/apps/api/routes/projects.py +80 -0
- tests/fixtures/arch_monorepo/apps/api/routes/tasks.py +91 -0
- tests/fixtures/arch_monorepo/apps/api/routes/users.py +48 -0
- tests/fixtures/arch_monorepo/apps/api/services/__init__.py +0 -0
- tests/fixtures/arch_monorepo/apps/api/services/email.py +11 -0
- tests/fixtures/backend_b/app.py +17 -0
- tests/fixtures/fastapi_app/app.py +48 -0
- tests/fixtures/fastapi_crossfile/routes.py +18 -0
- tests/fixtures/fastapi_crossfile/schemas.py +21 -0
- tests/fixtures/flask_app/app.py +33 -0
- tests/fixtures/flask_blueprint/app.py +7 -0
- tests/fixtures/flask_blueprint/routes/items.py +13 -0
- tests/fixtures/flask_blueprint/routes/users.py +20 -0
- tests/fixtures/middleware_test_flask/routes/public.py +8 -0
- tests/fixtures/middleware_test_flask/routes/users.py +26 -0
- tests/fixtures/python_deep_imports/app/__init__.py +0 -0
- tests/fixtures/python_deep_imports/app/api/__init__.py +0 -0
- tests/fixtures/python_deep_imports/app/api/health.py +11 -0
- tests/fixtures/python_deep_imports/app/api/v1/__init__.py +0 -0
- tests/fixtures/python_deep_imports/app/api/v1/items.py +18 -0
- tests/fixtures/python_deep_imports/app/api/v1/users.py +27 -0
- tests/fixtures/python_deep_imports/app/schemas/__init__.py +0 -0
- tests/fixtures/python_deep_imports/app/schemas/item.py +13 -0
- tests/fixtures/python_deep_imports/app/schemas/user.py +15 -0
- tests/fixtures/python_deep_imports/app/shared/__init__.py +0 -0
- tests/fixtures/python_deep_imports/app/shared/models.py +7 -0
- tests/fixtures/raw_sql_test/app.py +54 -0
- tests/test_architecture.py +757 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Cross-file middleware index: maps middleware to the routes they cover.
|
|
2
|
+
|
|
3
|
+
Detects middleware registrations across files and resolves which routes
|
|
4
|
+
are covered by which middleware based on scope, path prefix, and line ordering.
|
|
5
|
+
|
|
6
|
+
Patterns detected:
|
|
7
|
+
Express: app.use(authMiddleware)
|
|
8
|
+
app.use("/api", authMiddleware)
|
|
9
|
+
router.use(authMiddleware)
|
|
10
|
+
Flask: @app.before_request / @bp.before_request
|
|
11
|
+
app.before_request(func)
|
|
12
|
+
FastAPI: app.add_middleware(AuthMiddleware)
|
|
13
|
+
@app.middleware("http")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from tree_sitter import Tree
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Patterns that indicate a variable is a router, not middleware
|
|
29
|
+
_ROUTER_PATTERNS = re.compile(
|
|
30
|
+
r'.*[Rr]outer$|.*[Rr]outes$|.*_bp$|.*_blueprint$|.*[Hh]andler$|.*[Cc]ontroller$'
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _is_router_name(name: str) -> bool:
|
|
35
|
+
"""Check if a variable name looks like a router rather than middleware."""
|
|
36
|
+
return bool(_ROUTER_PATTERNS.match(name))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class MiddlewareBinding:
|
|
41
|
+
"""A middleware registration found in source code."""
|
|
42
|
+
name: str # middleware function/class name
|
|
43
|
+
scope: str # "global", "path", "router"
|
|
44
|
+
path_prefix: str # "/api" for path-scoped, "" for global/router
|
|
45
|
+
router_var: str # "app", "router", "bp" — which object it's mounted on
|
|
46
|
+
file_path: str
|
|
47
|
+
line: int
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MiddlewareIndex:
|
|
51
|
+
"""Index of middleware registrations across all files.
|
|
52
|
+
|
|
53
|
+
Built during Pass 1, queried during Pass 2 to attach middleware to routes.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
# file_path -> list of bindings found in that file
|
|
58
|
+
self._bindings_by_file: dict[str, list[MiddlewareBinding]] = {}
|
|
59
|
+
# router_var -> list of bindings (for cross-file: find middleware on a router)
|
|
60
|
+
self._bindings_by_router: dict[str, list[MiddlewareBinding]] = {}
|
|
61
|
+
# For cross-file: imported router name -> source file
|
|
62
|
+
self._router_imports: dict[str, dict[str, str]] = {}
|
|
63
|
+
# Track where routers are mounted: file -> {router_var: mount_line}
|
|
64
|
+
self._router_mount_lines: dict[str, dict[str, int]] = {}
|
|
65
|
+
|
|
66
|
+
def index_file(self, file_path: str, tree: "Tree", source: bytes, language: str) -> None:
|
|
67
|
+
"""Scan a file for middleware registrations."""
|
|
68
|
+
abs_path = os.path.abspath(file_path)
|
|
69
|
+
text = source.decode("utf-8", errors="replace")
|
|
70
|
+
|
|
71
|
+
bindings: list[MiddlewareBinding] = []
|
|
72
|
+
|
|
73
|
+
if language == "python":
|
|
74
|
+
bindings.extend(self._find_python_middleware(abs_path, text))
|
|
75
|
+
elif language in ("javascript", "typescript", "tsx"):
|
|
76
|
+
bindings.extend(self._find_js_middleware(abs_path, text))
|
|
77
|
+
|
|
78
|
+
if bindings:
|
|
79
|
+
self._bindings_by_file[abs_path] = bindings
|
|
80
|
+
for b in bindings:
|
|
81
|
+
self._bindings_by_router.setdefault(b.router_var, []).append(b)
|
|
82
|
+
|
|
83
|
+
# Track imports for cross-file resolution
|
|
84
|
+
self._track_imports(abs_path, text, language)
|
|
85
|
+
|
|
86
|
+
def get_middleware_for_route(
|
|
87
|
+
self,
|
|
88
|
+
file_path: str,
|
|
89
|
+
route_pattern: str,
|
|
90
|
+
router_var: str,
|
|
91
|
+
route_line: int,
|
|
92
|
+
) -> list[str]:
|
|
93
|
+
"""Get middleware names that cover a specific route.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
file_path: File containing the route.
|
|
97
|
+
route_pattern: The route's URL pattern (e.g. "/api/users").
|
|
98
|
+
router_var: The variable the route is defined on (e.g. "router", "bp").
|
|
99
|
+
route_line: Line number of the route definition.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
List of middleware names covering this route.
|
|
103
|
+
"""
|
|
104
|
+
abs_path = os.path.abspath(file_path)
|
|
105
|
+
middleware: list[str] = []
|
|
106
|
+
seen: set[str] = set()
|
|
107
|
+
|
|
108
|
+
def _add(name: str) -> None:
|
|
109
|
+
if name not in seen:
|
|
110
|
+
seen.add(name)
|
|
111
|
+
middleware.append(name)
|
|
112
|
+
|
|
113
|
+
# 1. Same-file middleware on the same router, registered before this line
|
|
114
|
+
for binding in self._bindings_by_file.get(abs_path, []):
|
|
115
|
+
if binding.router_var == router_var and binding.line < route_line:
|
|
116
|
+
if binding.scope == "router":
|
|
117
|
+
_add(binding.name)
|
|
118
|
+
elif binding.scope == "path" and route_pattern.startswith(binding.path_prefix):
|
|
119
|
+
_add(binding.name)
|
|
120
|
+
elif binding.scope == "global":
|
|
121
|
+
_add(binding.name)
|
|
122
|
+
|
|
123
|
+
# 2. Same-file app-level middleware (covers all routes regardless of router_var)
|
|
124
|
+
for binding in self._bindings_by_file.get(abs_path, []):
|
|
125
|
+
if binding.scope == "global" and binding.router_var != router_var:
|
|
126
|
+
if binding.line < route_line:
|
|
127
|
+
_add(binding.name)
|
|
128
|
+
|
|
129
|
+
# 3. Cross-file: find middleware registered on 'app' in other files
|
|
130
|
+
# Use router mount lines to check ordering (middleware must be before the router mount)
|
|
131
|
+
for other_file, bindings in self._bindings_by_file.items():
|
|
132
|
+
if other_file == abs_path:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
# Find the mount line for the router that serves our routes
|
|
136
|
+
mount_lines = self._router_mount_lines.get(other_file, {})
|
|
137
|
+
# The router from our file could be imported under various names
|
|
138
|
+
router_mount_line = None
|
|
139
|
+
for var_name, mount_line in mount_lines.items():
|
|
140
|
+
# Check if this import points to our file
|
|
141
|
+
imports = self._router_imports.get(other_file, {})
|
|
142
|
+
source = imports.get(var_name, "")
|
|
143
|
+
file_stem = Path(abs_path).stem
|
|
144
|
+
if file_stem in source or router_var in source:
|
|
145
|
+
router_mount_line = mount_line
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
for binding in bindings:
|
|
149
|
+
# Global app-level middleware — must be before router mount (if we know the mount line)
|
|
150
|
+
if binding.scope == "global" and binding.router_var in ("app", "server"):
|
|
151
|
+
if router_mount_line is None or binding.line < router_mount_line:
|
|
152
|
+
_add(binding.name)
|
|
153
|
+
# Path-scoped middleware — must match path AND be before router mount
|
|
154
|
+
elif binding.scope == "path" and route_pattern.startswith(binding.path_prefix):
|
|
155
|
+
if router_mount_line is None or binding.line < router_mount_line:
|
|
156
|
+
_add(binding.name)
|
|
157
|
+
|
|
158
|
+
# 4. Cross-file: middleware on the router variable that imports this file's router
|
|
159
|
+
self._resolve_cross_file_router_middleware(abs_path, router_var, middleware, seen)
|
|
160
|
+
|
|
161
|
+
return middleware
|
|
162
|
+
|
|
163
|
+
def _find_python_middleware(self, abs_path: str, text: str) -> list[MiddlewareBinding]:
|
|
164
|
+
"""Find Flask/FastAPI middleware patterns in Python code."""
|
|
165
|
+
bindings = []
|
|
166
|
+
|
|
167
|
+
# @app.before_request or @bp.before_request
|
|
168
|
+
for match in re.finditer(r'@(\w+)\.before_request\b', text):
|
|
169
|
+
var_name = match.group(1)
|
|
170
|
+
line = text[:match.start()].count("\n") + 1
|
|
171
|
+
# Find the function name on the next line
|
|
172
|
+
func_match = re.search(r'def\s+(\w+)', text[match.end():match.end() + 200])
|
|
173
|
+
func_name = func_match.group(1) if func_match else "before_request"
|
|
174
|
+
bindings.append(MiddlewareBinding(
|
|
175
|
+
name=func_name, scope="router", path_prefix="",
|
|
176
|
+
router_var=var_name, file_path=abs_path, line=line,
|
|
177
|
+
))
|
|
178
|
+
|
|
179
|
+
# app.before_request(func) — non-decorator form
|
|
180
|
+
for match in re.finditer(r'(\w+)\.before_request\s*\(\s*(\w+)\s*\)', text):
|
|
181
|
+
var_name = match.group(1)
|
|
182
|
+
func_name = match.group(2)
|
|
183
|
+
line = text[:match.start()].count("\n") + 1
|
|
184
|
+
bindings.append(MiddlewareBinding(
|
|
185
|
+
name=func_name, scope="router", path_prefix="",
|
|
186
|
+
router_var=var_name, file_path=abs_path, line=line,
|
|
187
|
+
))
|
|
188
|
+
|
|
189
|
+
# app.add_middleware(AuthMiddleware) — FastAPI
|
|
190
|
+
for match in re.finditer(r'(\w+)\.add_middleware\s*\(\s*(\w+)', text):
|
|
191
|
+
var_name = match.group(1)
|
|
192
|
+
class_name = match.group(2)
|
|
193
|
+
line = text[:match.start()].count("\n") + 1
|
|
194
|
+
bindings.append(MiddlewareBinding(
|
|
195
|
+
name=class_name, scope="global", path_prefix="",
|
|
196
|
+
router_var=var_name, file_path=abs_path, line=line,
|
|
197
|
+
))
|
|
198
|
+
|
|
199
|
+
# @app.middleware("http") — FastAPI decorator
|
|
200
|
+
for match in re.finditer(r'@(\w+)\.middleware\s*\(\s*["\']http["\']\s*\)', text):
|
|
201
|
+
var_name = match.group(1)
|
|
202
|
+
line = text[:match.start()].count("\n") + 1
|
|
203
|
+
func_match = re.search(r'(?:async\s+)?def\s+(\w+)', text[match.end():match.end() + 200])
|
|
204
|
+
func_name = func_match.group(1) if func_match else "http_middleware"
|
|
205
|
+
bindings.append(MiddlewareBinding(
|
|
206
|
+
name=func_name, scope="global", path_prefix="",
|
|
207
|
+
router_var=var_name, file_path=abs_path, line=line,
|
|
208
|
+
))
|
|
209
|
+
|
|
210
|
+
return bindings
|
|
211
|
+
|
|
212
|
+
def _find_js_middleware(self, abs_path: str, text: str) -> list[MiddlewareBinding]:
|
|
213
|
+
"""Find Express middleware patterns in JS/TS code."""
|
|
214
|
+
bindings = []
|
|
215
|
+
|
|
216
|
+
# app.use(middleware), app.use("/path", middleware), router.use(middleware)
|
|
217
|
+
# Also: app.all("*", middleware) — another pattern for global middleware
|
|
218
|
+
# Need to distinguish from app.use("/path", router) which is a route mount
|
|
219
|
+
for match in re.finditer(
|
|
220
|
+
r'(\w+)\.(?:use|all)\s*\(([^)]+)\)',
|
|
221
|
+
text,
|
|
222
|
+
):
|
|
223
|
+
obj_name = match.group(1)
|
|
224
|
+
args_text = match.group(2).strip()
|
|
225
|
+
line = text[:match.start()].count("\n") + 1
|
|
226
|
+
|
|
227
|
+
# Parse arguments
|
|
228
|
+
parts = [p.strip() for p in self._split_args(args_text)]
|
|
229
|
+
if not parts:
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
path_prefix = ""
|
|
233
|
+
middleware_names = []
|
|
234
|
+
|
|
235
|
+
for part in parts:
|
|
236
|
+
# String argument = path prefix
|
|
237
|
+
if (part.startswith('"') or part.startswith("'") or part.startswith("`")):
|
|
238
|
+
path_prefix = part.strip("\"'`")
|
|
239
|
+
# Identifier = middleware or router
|
|
240
|
+
elif re.match(r'^[a-zA-Z_]\w*$', part):
|
|
241
|
+
middleware_names.append(part)
|
|
242
|
+
# Function call like cors() or helmet()
|
|
243
|
+
elif re.match(r'^[a-zA-Z_]\w*\s*\(', part):
|
|
244
|
+
func_name = re.match(r'^(\w+)', part).group(1)
|
|
245
|
+
middleware_names.append(f"{func_name}()")
|
|
246
|
+
|
|
247
|
+
# Heuristic: if there's only one identifier and it looks like a router
|
|
248
|
+
# (ends with Router, _router, etc.), record mount line and skip middleware
|
|
249
|
+
if len(middleware_names) == 1 and path_prefix:
|
|
250
|
+
name = middleware_names[0]
|
|
251
|
+
if _is_router_name(name):
|
|
252
|
+
self._router_mount_lines.setdefault(abs_path, {})[name] = line
|
|
253
|
+
continue # This is a route mount, handled by PrefixIndex
|
|
254
|
+
|
|
255
|
+
# If there are multiple identifiers with a path, the last might be a router
|
|
256
|
+
# and the others are middleware. Record mount line for router-like ones.
|
|
257
|
+
if path_prefix and len(middleware_names) > 1:
|
|
258
|
+
last = middleware_names[-1]
|
|
259
|
+
if _is_router_name(last):
|
|
260
|
+
self._router_mount_lines.setdefault(abs_path, {})[last] = line
|
|
261
|
+
middleware_names = middleware_names[:-1] # remove router, keep middleware
|
|
262
|
+
|
|
263
|
+
for mw_name in middleware_names:
|
|
264
|
+
scope = "path" if path_prefix else "router"
|
|
265
|
+
# app.use with no path = global; app.all("*", ...) is also global
|
|
266
|
+
if obj_name in ("app", "server") and (not path_prefix or path_prefix == "*"):
|
|
267
|
+
scope = "global"
|
|
268
|
+
path_prefix = ""
|
|
269
|
+
bindings.append(MiddlewareBinding(
|
|
270
|
+
name=mw_name, scope=scope, path_prefix=path_prefix,
|
|
271
|
+
router_var=obj_name, file_path=abs_path, line=line,
|
|
272
|
+
))
|
|
273
|
+
|
|
274
|
+
return bindings
|
|
275
|
+
|
|
276
|
+
def _split_args(self, args_text: str) -> list[str]:
|
|
277
|
+
"""Split function arguments, respecting nested parens and quotes."""
|
|
278
|
+
parts = []
|
|
279
|
+
depth = 0
|
|
280
|
+
current = ""
|
|
281
|
+
in_string = None
|
|
282
|
+
|
|
283
|
+
for ch in args_text:
|
|
284
|
+
if in_string:
|
|
285
|
+
current += ch
|
|
286
|
+
if ch == in_string:
|
|
287
|
+
in_string = None
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
if ch in ("'", '"', '`'):
|
|
291
|
+
in_string = ch
|
|
292
|
+
current += ch
|
|
293
|
+
elif ch == '(' :
|
|
294
|
+
depth += 1
|
|
295
|
+
current += ch
|
|
296
|
+
elif ch == ')':
|
|
297
|
+
depth -= 1
|
|
298
|
+
current += ch
|
|
299
|
+
elif ch == ',' and depth == 0:
|
|
300
|
+
if current.strip():
|
|
301
|
+
parts.append(current.strip())
|
|
302
|
+
current = ""
|
|
303
|
+
else:
|
|
304
|
+
current += ch
|
|
305
|
+
|
|
306
|
+
if current.strip():
|
|
307
|
+
parts.append(current.strip())
|
|
308
|
+
return parts
|
|
309
|
+
|
|
310
|
+
def _track_imports(self, abs_path: str, text: str, language: str) -> None:
|
|
311
|
+
"""Track router imports for cross-file middleware resolution."""
|
|
312
|
+
if language in ("javascript", "typescript", "tsx"):
|
|
313
|
+
for match in re.finditer(r'import\s+(\w+)\s+from\s+["\']([^"\']+)["\']', text):
|
|
314
|
+
self._router_imports.setdefault(abs_path, {})[match.group(1)] = match.group(2)
|
|
315
|
+
for match in re.finditer(r'(?:const|let|var)\s+(\w+)\s*=\s*require\s*\(\s*["\']([^"\']+)["\']\s*\)', text):
|
|
316
|
+
self._router_imports.setdefault(abs_path, {})[match.group(1)] = match.group(2)
|
|
317
|
+
elif language == "python":
|
|
318
|
+
for match in re.finditer(r'from\s+(\S+)\s+import\s+(.+)', text):
|
|
319
|
+
module = match.group(1)
|
|
320
|
+
names = [n.strip().split(" as ")[-1].strip() for n in match.group(2).split(",")]
|
|
321
|
+
for name in names:
|
|
322
|
+
if name:
|
|
323
|
+
self._router_imports.setdefault(abs_path, {})[name] = module
|
|
324
|
+
|
|
325
|
+
def _resolve_cross_file_router_middleware(
|
|
326
|
+
self, route_file: str, router_var: str, middleware: list[str], seen: set[str]
|
|
327
|
+
) -> None:
|
|
328
|
+
"""Check if other files mount this router with middleware."""
|
|
329
|
+
route_file_stem = Path(route_file).stem
|
|
330
|
+
|
|
331
|
+
for other_file, imports in self._router_imports.items():
|
|
332
|
+
if other_file == route_file:
|
|
333
|
+
continue
|
|
334
|
+
for imported_name, source_path in imports.items():
|
|
335
|
+
if route_file_stem in source_path or router_var in source_path:
|
|
336
|
+
# Find where this router is mounted
|
|
337
|
+
mount_line = self._router_mount_lines.get(other_file, {}).get(imported_name)
|
|
338
|
+
|
|
339
|
+
for binding in self._bindings_by_file.get(other_file, []):
|
|
340
|
+
if binding.router_var == imported_name or binding.scope == "global":
|
|
341
|
+
# Respect line ordering: middleware must be before router mount
|
|
342
|
+
if mount_line is not None and binding.line >= mount_line:
|
|
343
|
+
continue
|
|
344
|
+
if binding.name not in seen:
|
|
345
|
+
seen.add(binding.name)
|
|
346
|
+
middleware.append(binding.name)
|
|
347
|
+
|
|
348
|
+
@property
|
|
349
|
+
def binding_count(self) -> int:
|
|
350
|
+
return sum(len(b) for b in self._bindings_by_file.values())
|
commiter/models.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Data models for all extracted documentation artifacts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FileRole(Enum):
|
|
10
|
+
BACKEND = "backend"
|
|
11
|
+
FRONTEND = "frontend"
|
|
12
|
+
CONFIG = "config"
|
|
13
|
+
TEST = "test"
|
|
14
|
+
MIGRATION = "migration"
|
|
15
|
+
SHARED = "shared"
|
|
16
|
+
UNKNOWN = "unknown"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ParamSource(Enum):
|
|
20
|
+
PATH = "path"
|
|
21
|
+
QUERY = "query"
|
|
22
|
+
HEADER = "header"
|
|
23
|
+
BODY = "body"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Param:
|
|
28
|
+
name: str
|
|
29
|
+
source: ParamSource
|
|
30
|
+
type_hint: str | None = None
|
|
31
|
+
required: bool = True
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class APIEndpoint:
|
|
36
|
+
repo: str
|
|
37
|
+
file_path: str
|
|
38
|
+
line: int
|
|
39
|
+
http_method: str
|
|
40
|
+
route_pattern: str
|
|
41
|
+
handler_name: str
|
|
42
|
+
framework: str
|
|
43
|
+
parameters: list[Param] = field(default_factory=list)
|
|
44
|
+
request_body_fields: list[str] = field(default_factory=list)
|
|
45
|
+
response_fields: list[str] = field(default_factory=list)
|
|
46
|
+
auth_decorators: list[str] = field(default_factory=list)
|
|
47
|
+
db_tables: list[str] = field(default_factory=list)
|
|
48
|
+
request_body_type: str | None = None
|
|
49
|
+
response_type: str | None = None
|
|
50
|
+
middleware: list[str] = field(default_factory=list)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class APICall:
|
|
55
|
+
repo: str
|
|
56
|
+
file_path: str
|
|
57
|
+
line: int
|
|
58
|
+
component_or_page: str
|
|
59
|
+
http_method: str
|
|
60
|
+
url_pattern: str
|
|
61
|
+
client_library: str
|
|
62
|
+
body_fields: list[str] = field(default_factory=list)
|
|
63
|
+
traced_from: str | None = None
|
|
64
|
+
response_type: str | None = None
|
|
65
|
+
body_type: str | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class Dependency:
|
|
70
|
+
repo: str
|
|
71
|
+
name: str
|
|
72
|
+
version_constraint: str
|
|
73
|
+
source_file: str
|
|
74
|
+
dev_only: bool = False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class FileClassification:
|
|
79
|
+
file_path: str
|
|
80
|
+
role: FileRole
|
|
81
|
+
language: str
|
|
82
|
+
framework_hints: list[str] = field(default_factory=list)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class DBOperation:
|
|
87
|
+
repo: str
|
|
88
|
+
file_path: str
|
|
89
|
+
line: int
|
|
90
|
+
operation_type: str # select, insert, update, delete
|
|
91
|
+
table_name: str
|
|
92
|
+
orm_library: str
|
|
93
|
+
filters: list[str] = field(default_factory=list)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class ServiceRelationship:
|
|
98
|
+
source_repo: str
|
|
99
|
+
target_repo: str
|
|
100
|
+
connection_type: str # api_call, shared_db, shared_package
|
|
101
|
+
source_file: str
|
|
102
|
+
target_endpoint: str
|
|
103
|
+
confidence: float = 0.0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class RepoDocumentation:
|
|
108
|
+
repo_name: str
|
|
109
|
+
repo_path: str
|
|
110
|
+
languages: list[str] = field(default_factory=list)
|
|
111
|
+
frameworks: list[str] = field(default_factory=list)
|
|
112
|
+
endpoints: list[APIEndpoint] = field(default_factory=list)
|
|
113
|
+
api_calls: list[APICall] = field(default_factory=list)
|
|
114
|
+
dependencies: list[Dependency] = field(default_factory=list)
|
|
115
|
+
file_classifications: list[FileClassification] = field(default_factory=list)
|
|
116
|
+
db_operations: list[DBOperation] = field(default_factory=list)
|
|
117
|
+
service_relationships: list[ServiceRelationship] = field(default_factory=list)
|