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.
- codegraph/__init__.py +37 -0
- codegraph/agent.py +26 -0
- codegraph/agent_brain.py +2212 -0
- codegraph/agent_capabilities.py +4058 -0
- codegraph/agent_rules.py +531 -0
- codegraph/api_drift.py +455 -0
- codegraph/architecture.py +371 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/audit.py +106 -0
- codegraph/binding_resolver.py +616 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/cache.py +95 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +2436 -0
- codegraph/cli_output.py +205 -0
- codegraph/config.py +45 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +2182 -0
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +72 -0
- codegraph/database/extractor.py +3651 -0
- codegraph/database/interrogation.py +1494 -0
- codegraph/database/models.py +310 -0
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +200 -0
- codegraph/errors.py +282 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +418 -0
- codegraph/evidence_contract.py +486 -0
- codegraph/frameworks.py +960 -0
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +335 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +362 -0
- codegraph/git_state.py +545 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +56 -0
- codegraph/graph/traversal.py +1993 -0
- codegraph/indexing/__init__.py +24 -0
- codegraph/indexing/classifier.py +564 -0
- codegraph/indexing/indexer.py +2578 -0
- codegraph/indexing/models.py +427 -0
- codegraph/indexing/parser.py +3404 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +211 -0
- codegraph/indexing/telemetry.py +326 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/installer.py +1702 -0
- codegraph/interrogation.py +2355 -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 +1476 -0
- codegraph/mcp_diagnostics.py +882 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +310 -0
- codegraph/monorepo.py +925 -0
- codegraph/observability.py +262 -0
- codegraph/optimizer.py +1014 -0
- codegraph/planner.py +417 -0
- codegraph/process_lifecycle.py +1153 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +408 -0
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/resolver.py +3968 -0
- codegraph/resources/__init__.py +46 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +219 -0
- codegraph/resources/monitor.py +102 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +320 -0
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +363 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +51 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +692 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/runtime/runner.py +353 -0
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/__init__.py +25 -0
- codegraph/search/hybrid.py +846 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +51 -0
- codegraph/security/paths.py +189 -0
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +372 -0
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph/tool_selection_eval.py +1158 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +755 -0
- codegraph/watcher.py +323 -0
- codegraph_engine-3.0.0.dist-info/METADATA +859 -0
- codegraph_engine-3.0.0.dist-info/RECORD +130 -0
- codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
- codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
- codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"""Deterministic NestJS framework and dependency injection analyzer.
|
|
2
|
+
|
|
3
|
+
Inspects TypeScript/JavaScript classes for:
|
|
4
|
+
- @Controller('prefix') class decorators and HTTP method decorators (@Get, @Post, @Put, @Delete, @Patch, @Options, @Head, @All).
|
|
5
|
+
- Constructor dependency injection: typed parameters (e.g. `private readonly service: InvoicesService`)
|
|
6
|
+
and @Inject('TOKEN') / @Inject(TokenClass) parameter decorators.
|
|
7
|
+
- @Module({ controllers: [...], providers: [...], imports: [...] }) module declarations.
|
|
8
|
+
|
|
9
|
+
Emits:
|
|
10
|
+
- RouteDetection records for HTTP endpoints.
|
|
11
|
+
- BindingRef records with expr_kind='NESTJS_INJECTS' (for DI) and expr_kind='NESTJS_CONTROLLER' / 'DI_PROVIDES'.
|
|
12
|
+
- RouterMountDetection records for module-controller wiring.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
|
|
20
|
+
from codegraph.frameworks import (
|
|
21
|
+
RouteDetection,
|
|
22
|
+
RouterMountDetection,
|
|
23
|
+
join_route_prefixes,
|
|
24
|
+
normalize_route_path,
|
|
25
|
+
)
|
|
26
|
+
from codegraph.indexing.models import BindingRef, build_canonical_id
|
|
27
|
+
|
|
28
|
+
_NESTJS_HTTP_METHODS: dict[str, str] = {
|
|
29
|
+
"get": "GET",
|
|
30
|
+
"post": "POST",
|
|
31
|
+
"put": "PUT",
|
|
32
|
+
"delete": "DELETE",
|
|
33
|
+
"patch": "PATCH",
|
|
34
|
+
"options": "OPTIONS",
|
|
35
|
+
"head": "HEAD",
|
|
36
|
+
"all": "ANY",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_TS_PRIMITIVE_TYPES = frozenset({
|
|
40
|
+
"string",
|
|
41
|
+
"number",
|
|
42
|
+
"boolean",
|
|
43
|
+
"any",
|
|
44
|
+
"void",
|
|
45
|
+
"unknown",
|
|
46
|
+
"object",
|
|
47
|
+
"never",
|
|
48
|
+
"null",
|
|
49
|
+
"undefined",
|
|
50
|
+
"symbol",
|
|
51
|
+
"bigint",
|
|
52
|
+
"function",
|
|
53
|
+
"record",
|
|
54
|
+
"array",
|
|
55
|
+
"promise",
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
# Matches @Controller('path') or @Controller()
|
|
59
|
+
_CONTROLLER_DEC_RE = re.compile(
|
|
60
|
+
r"@Controller\s*\(\s*(?:['\"`]([^'\"`]*)['\"`])?\s*\)"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Matches @Module({ ... })
|
|
64
|
+
_MODULE_DEC_RE = re.compile(r"@Module\s*\(")
|
|
65
|
+
|
|
66
|
+
# Matches @Injectable()
|
|
67
|
+
_INJECTABLE_DEC_RE = re.compile(r"@Injectable\s*\(")
|
|
68
|
+
|
|
69
|
+
# Matches @Get('path'), @Post(), etc.
|
|
70
|
+
_METHOD_DEC_RE = re.compile(
|
|
71
|
+
r"@(Get|Post|Put|Delete|Patch|Options|Head|All)\s*\(\s*(?:['\"`]([^'\"`]*)['\"`])?\s*\)",
|
|
72
|
+
re.IGNORECASE,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Matches class declaration
|
|
76
|
+
_CLASS_DECL_RE = re.compile(
|
|
77
|
+
r"\b(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Matches method declaration inside class
|
|
81
|
+
_METHOD_DECL_RE = re.compile(
|
|
82
|
+
r"^\s*(?:public\s+|private\s+|protected\s+)?(?:async\s+)?(?:static\s+)?(?:get\s+|set\s+)?([A-Za-z_$][\w$]*)\s*\("
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Matches constructor declaration
|
|
86
|
+
_CONSTRUCTOR_START_RE = re.compile(r"\bconstructor\s*\(")
|
|
87
|
+
|
|
88
|
+
# Matches @Inject('TOKEN') or @Inject(TOKEN) inside constructor param
|
|
89
|
+
_INJECT_PARAM_DEC_RE = re.compile(
|
|
90
|
+
r"@Inject\s*\(\s*(?:['\"`]([^'\"`]*)['\"`]|([A-Za-z_$][\w$]*))\s*\)"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class NestjsAnalysisResult:
|
|
96
|
+
routes: list[RouteDetection] = field(default_factory=list)
|
|
97
|
+
mounts: list[RouterMountDetection] = field(default_factory=list)
|
|
98
|
+
bindings: list[BindingRef] = field(default_factory=list)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _split_constructor_params(params_text: str) -> list[str]:
|
|
102
|
+
"""Split comma-separated constructor parameters respecting nested generics, parens, brackets, braces."""
|
|
103
|
+
params: list[str] = []
|
|
104
|
+
current: list[str] = []
|
|
105
|
+
depth_paren = 0
|
|
106
|
+
depth_bracket = 0
|
|
107
|
+
depth_brace = 0
|
|
108
|
+
depth_angle = 0
|
|
109
|
+
in_quote = False
|
|
110
|
+
quote_char = ""
|
|
111
|
+
|
|
112
|
+
for ch in params_text:
|
|
113
|
+
if in_quote:
|
|
114
|
+
current.append(ch)
|
|
115
|
+
if ch == quote_char:
|
|
116
|
+
in_quote = False
|
|
117
|
+
continue
|
|
118
|
+
if ch in ('"', "'", "`"):
|
|
119
|
+
in_quote = True
|
|
120
|
+
quote_char = ch
|
|
121
|
+
current.append(ch)
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
if ch == "(":
|
|
125
|
+
depth_paren += 1
|
|
126
|
+
elif ch == ")":
|
|
127
|
+
depth_paren = max(0, depth_paren - 1)
|
|
128
|
+
elif ch == "[":
|
|
129
|
+
depth_bracket += 1
|
|
130
|
+
elif ch == "]":
|
|
131
|
+
depth_bracket = max(0, depth_bracket - 1)
|
|
132
|
+
elif ch == "{":
|
|
133
|
+
depth_brace += 1
|
|
134
|
+
elif ch == "}":
|
|
135
|
+
depth_brace = max(0, depth_brace - 1)
|
|
136
|
+
elif ch == "<":
|
|
137
|
+
depth_angle += 1
|
|
138
|
+
elif ch == ">":
|
|
139
|
+
depth_angle = max(0, depth_angle - 1)
|
|
140
|
+
|
|
141
|
+
if (
|
|
142
|
+
ch == ","
|
|
143
|
+
and depth_paren == 0
|
|
144
|
+
and depth_bracket == 0
|
|
145
|
+
and depth_brace == 0
|
|
146
|
+
and depth_angle == 0
|
|
147
|
+
):
|
|
148
|
+
p = "".join(current).strip()
|
|
149
|
+
if p:
|
|
150
|
+
params.append(p)
|
|
151
|
+
current = []
|
|
152
|
+
else:
|
|
153
|
+
current.append(ch)
|
|
154
|
+
|
|
155
|
+
p = "".join(current).strip()
|
|
156
|
+
if p:
|
|
157
|
+
params.append(p)
|
|
158
|
+
return params
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _extract_module_array(body: str, key: str) -> list[str]:
|
|
162
|
+
"""Extract list of identifiers or objects from a @Module({ key: [...] }) configuration."""
|
|
163
|
+
m = re.search(rf"\b{key}\s*:\s*\[([\s\S]*?)\]", body)
|
|
164
|
+
if not m:
|
|
165
|
+
return []
|
|
166
|
+
items_raw = m.group(1)
|
|
167
|
+
results: list[str] = []
|
|
168
|
+
tokens: list[str] = []
|
|
169
|
+
current: list[str] = []
|
|
170
|
+
brace_depth = 0
|
|
171
|
+
for ch in items_raw:
|
|
172
|
+
if ch == "{":
|
|
173
|
+
brace_depth += 1
|
|
174
|
+
elif ch == "}":
|
|
175
|
+
brace_depth = max(0, brace_depth - 1)
|
|
176
|
+
if ch == "," and brace_depth == 0:
|
|
177
|
+
item = "".join(current).strip()
|
|
178
|
+
if item:
|
|
179
|
+
tokens.append(item)
|
|
180
|
+
current = []
|
|
181
|
+
else:
|
|
182
|
+
current.append(ch)
|
|
183
|
+
item = "".join(current).strip()
|
|
184
|
+
if item:
|
|
185
|
+
tokens.append(item)
|
|
186
|
+
|
|
187
|
+
for clean in tokens:
|
|
188
|
+
clean = re.sub(r"//.*$", "", clean).strip()
|
|
189
|
+
if not clean or clean.startswith("{"):
|
|
190
|
+
continue
|
|
191
|
+
ident_m = re.match(r"^([A-Za-z_$][\w$]*)", clean)
|
|
192
|
+
if ident_m:
|
|
193
|
+
results.append(ident_m.group(1))
|
|
194
|
+
return results
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _extract_custom_providers(body: str) -> list[tuple[str, str]]:
|
|
198
|
+
"""Extract { provide: 'TOKEN' | Type, useClass: Impl } custom providers."""
|
|
199
|
+
providers: list[tuple[str, str]] = []
|
|
200
|
+
for m in re.finditer(r"\{\s*provide\s*:\s*(['\"`]?[\w$]+['\"`]?)\s*,\s*useClass\s*:\s*([A-Za-z_$][\w$]*)\s*,?\s*\}", body, re.DOTALL):
|
|
201
|
+
token = m.group(1).strip("'\"`")
|
|
202
|
+
use_class = m.group(2)
|
|
203
|
+
if token and use_class:
|
|
204
|
+
providers.append((token, use_class))
|
|
205
|
+
return providers
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def analyze_nestjs_file(
|
|
209
|
+
content: str,
|
|
210
|
+
file_path: str,
|
|
211
|
+
module: str,
|
|
212
|
+
) -> NestjsAnalysisResult:
|
|
213
|
+
"""Analyze a TypeScript/JavaScript file for NestJS controllers, routes, modules, and constructor DI."""
|
|
214
|
+
res = NestjsAnalysisResult()
|
|
215
|
+
|
|
216
|
+
if "@" not in content:
|
|
217
|
+
return res
|
|
218
|
+
|
|
219
|
+
lines = content.splitlines()
|
|
220
|
+
total_lines = len(lines)
|
|
221
|
+
|
|
222
|
+
# 1. Parse @Module definitions
|
|
223
|
+
for mod_m in _MODULE_DEC_RE.finditer(content):
|
|
224
|
+
start_idx = mod_m.start()
|
|
225
|
+
line_no = content[:start_idx].count("\n") + 1
|
|
226
|
+
# Extract the object literal argument for @Module({ ... })
|
|
227
|
+
brace_start = content.find("{", start_idx)
|
|
228
|
+
if brace_start != -1:
|
|
229
|
+
depth = 1
|
|
230
|
+
idx = brace_start + 1
|
|
231
|
+
while idx < len(content) and depth > 0:
|
|
232
|
+
if content[idx] == "{":
|
|
233
|
+
depth += 1
|
|
234
|
+
elif content[idx] == "}":
|
|
235
|
+
depth -= 1
|
|
236
|
+
idx += 1
|
|
237
|
+
mod_body = content[brace_start + 1 : idx - 1]
|
|
238
|
+
|
|
239
|
+
# Find following class name
|
|
240
|
+
post_content = content[idx:]
|
|
241
|
+
cls_m = _CLASS_DECL_RE.search(post_content)
|
|
242
|
+
mod_class = cls_m.group(1) if cls_m else "AppModule"
|
|
243
|
+
mod_canon = build_canonical_id(module, "", mod_class)
|
|
244
|
+
|
|
245
|
+
# Controllers
|
|
246
|
+
controllers = _extract_module_array(mod_body, "controllers")
|
|
247
|
+
for ctrl in controllers:
|
|
248
|
+
res.bindings.append(
|
|
249
|
+
BindingRef(
|
|
250
|
+
target_name=mod_canon,
|
|
251
|
+
source_expr=ctrl,
|
|
252
|
+
file_path=file_path,
|
|
253
|
+
line=line_no,
|
|
254
|
+
scope=mod_class,
|
|
255
|
+
expr_kind="NESTJS_CONTROLLER",
|
|
256
|
+
base_expr="nestjs_module",
|
|
257
|
+
attr_name="controller",
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Class providers
|
|
262
|
+
class_providers = _extract_module_array(mod_body, "providers")
|
|
263
|
+
for prov in class_providers:
|
|
264
|
+
res.bindings.append(
|
|
265
|
+
BindingRef(
|
|
266
|
+
target_name=mod_canon,
|
|
267
|
+
source_expr=prov,
|
|
268
|
+
file_path=file_path,
|
|
269
|
+
line=line_no,
|
|
270
|
+
scope=mod_class,
|
|
271
|
+
expr_kind="DI_PROVIDES",
|
|
272
|
+
base_expr="nestjs_module",
|
|
273
|
+
attr_name="provider",
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Custom providers
|
|
278
|
+
custom_providers = _extract_custom_providers(mod_body)
|
|
279
|
+
for token, use_class in custom_providers:
|
|
280
|
+
res.bindings.append(
|
|
281
|
+
BindingRef(
|
|
282
|
+
target_name=token,
|
|
283
|
+
source_expr=use_class,
|
|
284
|
+
file_path=file_path,
|
|
285
|
+
line=line_no,
|
|
286
|
+
scope=mod_class,
|
|
287
|
+
expr_kind="DI_PROVIDES",
|
|
288
|
+
base_expr="nestjs_provider",
|
|
289
|
+
attr_name="useClass",
|
|
290
|
+
subscript_key=json.dumps({"token": token, "provider": use_class}),
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
res.bindings.append(
|
|
294
|
+
BindingRef(
|
|
295
|
+
target_name=mod_canon,
|
|
296
|
+
source_expr=use_class,
|
|
297
|
+
file_path=file_path,
|
|
298
|
+
line=line_no,
|
|
299
|
+
scope=mod_class,
|
|
300
|
+
expr_kind="DI_PROVIDES",
|
|
301
|
+
base_expr="nestjs_module",
|
|
302
|
+
attr_name="custom_provider",
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# 2. Parse Classes, Controllers, Route Methods, and Constructor DI
|
|
307
|
+
i = 0
|
|
308
|
+
pending_class_decorators: list[tuple[str, str, int]] = [] # (decorator_name, arg, line_no)
|
|
309
|
+
|
|
310
|
+
while i < total_lines:
|
|
311
|
+
line = lines[i]
|
|
312
|
+
stripped = line.strip()
|
|
313
|
+
|
|
314
|
+
# Check for class-level decorators
|
|
315
|
+
ctrl_m = _CONTROLLER_DEC_RE.search(stripped)
|
|
316
|
+
if ctrl_m:
|
|
317
|
+
prefix = ctrl_m.group(1) or ""
|
|
318
|
+
pending_class_decorators.append(("Controller", prefix, i + 1))
|
|
319
|
+
i += 1
|
|
320
|
+
continue
|
|
321
|
+
|
|
322
|
+
if _INJECTABLE_DEC_RE.search(stripped):
|
|
323
|
+
pending_class_decorators.append(("Injectable", "", i + 1))
|
|
324
|
+
i += 1
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
# Check for class declaration
|
|
328
|
+
class_m = _CLASS_DECL_RE.search(stripped)
|
|
329
|
+
if class_m:
|
|
330
|
+
class_name = class_m.group(1)
|
|
331
|
+
class_canon = build_canonical_id(module, "", class_name)
|
|
332
|
+
|
|
333
|
+
# Determine if this class is a Controller
|
|
334
|
+
is_controller = False
|
|
335
|
+
controller_prefix = ""
|
|
336
|
+
for dec_name, dec_arg, _ in pending_class_decorators:
|
|
337
|
+
if dec_name == "Controller":
|
|
338
|
+
is_controller = True
|
|
339
|
+
controller_prefix = dec_arg
|
|
340
|
+
break
|
|
341
|
+
|
|
342
|
+
pending_class_decorators.clear()
|
|
343
|
+
|
|
344
|
+
# Now scan inside the class body for methods, decorators, and constructor
|
|
345
|
+
# Find class closing brace
|
|
346
|
+
class_depth = line.count("{") - line.count("}")
|
|
347
|
+
j = i + 1
|
|
348
|
+
pending_method_decorators: list[tuple[str, str, int]] = []
|
|
349
|
+
|
|
350
|
+
while j < total_lines and class_depth > 0:
|
|
351
|
+
inner_line = lines[j]
|
|
352
|
+
inner_stripped = inner_line.strip()
|
|
353
|
+
class_depth += inner_line.count("{") - inner_line.count("}")
|
|
354
|
+
|
|
355
|
+
# Method decorators: @Get, @Post, etc.
|
|
356
|
+
method_dec_m = _METHOD_DEC_RE.search(inner_stripped)
|
|
357
|
+
if method_dec_m:
|
|
358
|
+
m_http = method_dec_m.group(1).lower()
|
|
359
|
+
m_path = method_dec_m.group(2) or ""
|
|
360
|
+
pending_method_decorators.append((m_http, m_path, j + 1))
|
|
361
|
+
j += 1
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
# Constructor declaration
|
|
365
|
+
if _CONSTRUCTOR_START_RE.search(inner_stripped):
|
|
366
|
+
const_line = j + 1
|
|
367
|
+
# Collect constructor parameter block up to matching ')'
|
|
368
|
+
paren_depth = inner_line.count("(") - inner_line.count(")")
|
|
369
|
+
const_lines = [inner_line]
|
|
370
|
+
k = j + 1
|
|
371
|
+
while k < total_lines and paren_depth > 0:
|
|
372
|
+
const_lines.append(lines[k])
|
|
373
|
+
paren_depth += lines[k].count("(") - lines[k].count(")")
|
|
374
|
+
k += 1
|
|
375
|
+
|
|
376
|
+
full_const_sig = " ".join(c_line.strip() for c_line in const_lines)
|
|
377
|
+
start_paren = full_const_sig.find("(")
|
|
378
|
+
if start_paren != -1:
|
|
379
|
+
p_depth = 1
|
|
380
|
+
idx_c = start_paren + 1
|
|
381
|
+
while idx_c < len(full_const_sig) and p_depth > 0:
|
|
382
|
+
if full_const_sig[idx_c] == "(":
|
|
383
|
+
p_depth += 1
|
|
384
|
+
elif full_const_sig[idx_c] == ")":
|
|
385
|
+
p_depth -= 1
|
|
386
|
+
idx_c += 1
|
|
387
|
+
raw_params = full_const_sig[start_paren + 1 : idx_c - 1].strip()
|
|
388
|
+
if raw_params:
|
|
389
|
+
params = _split_constructor_params(raw_params)
|
|
390
|
+
for param_str in params:
|
|
391
|
+
# Check for @Inject('TOKEN') or @Inject(TOKEN)
|
|
392
|
+
inject_m = _INJECT_PARAM_DEC_RE.search(param_str)
|
|
393
|
+
inject_token = (
|
|
394
|
+
inject_m.group(1) or inject_m.group(2)
|
|
395
|
+
if inject_m
|
|
396
|
+
else None
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Clean out decorators and access modifiers to extract param_name: Type
|
|
400
|
+
clean_p = _INJECT_PARAM_DEC_RE.sub("", param_str).strip()
|
|
401
|
+
clean_p = re.sub(
|
|
402
|
+
r"\b(private|public|protected|readonly)\b",
|
|
403
|
+
"",
|
|
404
|
+
clean_p,
|
|
405
|
+
).strip()
|
|
406
|
+
|
|
407
|
+
# Match name: Type
|
|
408
|
+
name_type_m = re.match(
|
|
409
|
+
r"^([A-Za-z_$][\w$]*)\s*(?:\??\s*:\s*([^=,]+))?",
|
|
410
|
+
clean_p,
|
|
411
|
+
)
|
|
412
|
+
if name_type_m:
|
|
413
|
+
p_name = name_type_m.group(1)
|
|
414
|
+
p_type_raw = name_type_m.group(2)
|
|
415
|
+
p_type = p_type_raw.strip().split("<")[0].strip() if p_type_raw else ""
|
|
416
|
+
|
|
417
|
+
if inject_token:
|
|
418
|
+
res.bindings.append(
|
|
419
|
+
BindingRef(
|
|
420
|
+
target_name=class_canon,
|
|
421
|
+
source_expr=inject_token,
|
|
422
|
+
file_path=file_path,
|
|
423
|
+
line=const_line,
|
|
424
|
+
scope=class_name,
|
|
425
|
+
expr_kind="NESTJS_INJECTS",
|
|
426
|
+
base_expr="nestjs_inject",
|
|
427
|
+
attr_name=p_name,
|
|
428
|
+
subscript_key=json.dumps({
|
|
429
|
+
"param": p_name,
|
|
430
|
+
"type": p_type,
|
|
431
|
+
"token": inject_token,
|
|
432
|
+
}),
|
|
433
|
+
)
|
|
434
|
+
)
|
|
435
|
+
elif p_type and p_type.lower() not in _TS_PRIMITIVE_TYPES:
|
|
436
|
+
res.bindings.append(
|
|
437
|
+
BindingRef(
|
|
438
|
+
target_name=class_canon,
|
|
439
|
+
source_expr=p_type,
|
|
440
|
+
file_path=file_path,
|
|
441
|
+
line=const_line,
|
|
442
|
+
scope=class_name,
|
|
443
|
+
expr_kind="NESTJS_INJECTS",
|
|
444
|
+
base_expr="nestjs",
|
|
445
|
+
attr_name=p_name,
|
|
446
|
+
subscript_key=json.dumps({
|
|
447
|
+
"param": p_name,
|
|
448
|
+
"type": p_type,
|
|
449
|
+
}),
|
|
450
|
+
)
|
|
451
|
+
)
|
|
452
|
+
j = k
|
|
453
|
+
continue
|
|
454
|
+
|
|
455
|
+
# Method declaration inside controller
|
|
456
|
+
meth_m = _METHOD_DECL_RE.match(inner_stripped)
|
|
457
|
+
if meth_m and is_controller:
|
|
458
|
+
method_name = meth_m.group(1)
|
|
459
|
+
|
|
460
|
+
if method_name not in ("constructor", "if", "for", "while", "switch"):
|
|
461
|
+
for http_m_raw, path_arg, dec_line in pending_method_decorators:
|
|
462
|
+
method_upper = _NESTJS_HTTP_METHODS.get(http_m_raw.lower(), "GET")
|
|
463
|
+
full_route = join_route_prefixes(controller_prefix, path_arg)
|
|
464
|
+
norm_route = normalize_route_path(full_route, "nestjs")
|
|
465
|
+
handler_canon = f"{module}.{class_name}.{method_name}"
|
|
466
|
+
|
|
467
|
+
res.routes.append(
|
|
468
|
+
RouteDetection(
|
|
469
|
+
framework="nestjs",
|
|
470
|
+
http_method=method_upper,
|
|
471
|
+
route_path=full_route,
|
|
472
|
+
normalized_route=norm_route,
|
|
473
|
+
handler_name=method_name,
|
|
474
|
+
handler_canonical_id=handler_canon,
|
|
475
|
+
file_path=file_path,
|
|
476
|
+
line=dec_line,
|
|
477
|
+
confidence="HIGH",
|
|
478
|
+
resolution_status="RESOLVED",
|
|
479
|
+
evidence=f"NestJS @{http_m_raw.capitalize()}('{path_arg}') on {class_name}.{method_name}",
|
|
480
|
+
module=module,
|
|
481
|
+
router_name=class_name,
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
pending_method_decorators.clear()
|
|
486
|
+
|
|
487
|
+
elif not inner_stripped.startswith("@"):
|
|
488
|
+
if not inner_stripped.startswith("//") and inner_stripped:
|
|
489
|
+
pending_method_decorators.clear()
|
|
490
|
+
|
|
491
|
+
j += 1
|
|
492
|
+
|
|
493
|
+
i = j
|
|
494
|
+
continue
|
|
495
|
+
|
|
496
|
+
# Non-decorator, non-blank statement outside class clears pending decorators
|
|
497
|
+
if not stripped.startswith("@") and not stripped.startswith("//") and stripped:
|
|
498
|
+
pending_class_decorators.clear()
|
|
499
|
+
|
|
500
|
+
i += 1
|
|
501
|
+
|
|
502
|
+
return res
|