apisec-code-bolt 0.1.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.
- apisec_code_bolt/__init__.py +42 -0
- apisec_code_bolt/__main__.py +11 -0
- apisec_code_bolt/analysis/__init__.py +96 -0
- apisec_code_bolt/analysis/analyzer.py +2309 -0
- apisec_code_bolt/analysis/binding_tracker.py +341 -0
- apisec_code_bolt/analysis/call_graph.py +1197 -0
- apisec_code_bolt/analysis/call_graph_types.py +332 -0
- apisec_code_bolt/analysis/call_resolver.py +988 -0
- apisec_code_bolt/analysis/capability_tagger.py +322 -0
- apisec_code_bolt/analysis/config_scanner.py +197 -0
- apisec_code_bolt/analysis/data_flow.py +1883 -0
- apisec_code_bolt/analysis/dependency_extractor.py +959 -0
- apisec_code_bolt/analysis/flow_analysis.py +1406 -0
- apisec_code_bolt/analysis/hof_catalog.py +61 -0
- apisec_code_bolt/analysis/integration_detector.py +1399 -0
- apisec_code_bolt/analysis/literal_scanner.py +300 -0
- apisec_code_bolt/analysis/path_normalizer.py +55 -0
- apisec_code_bolt/analysis/read_site_detector.py +310 -0
- apisec_code_bolt/analysis/request_patterns.py +162 -0
- apisec_code_bolt/analysis/sensitivity_classifier.py +224 -0
- apisec_code_bolt/analysis/sink_evidence.py +333 -0
- apisec_code_bolt/analysis/url_prefix_resolver.py +338 -0
- apisec_code_bolt/cli/__init__.py +5 -0
- apisec_code_bolt/cli/exit_codes.py +17 -0
- apisec_code_bolt/cli/main.py +1069 -0
- apisec_code_bolt/cloud/__init__.py +1 -0
- apisec_code_bolt/cloud/apisec_client.py +118 -0
- apisec_code_bolt/cloud/client.py +255 -0
- apisec_code_bolt/core/__init__.py +75 -0
- apisec_code_bolt/core/config.py +528 -0
- apisec_code_bolt/core/credentials.py +65 -0
- apisec_code_bolt/core/discovery.py +433 -0
- apisec_code_bolt/core/log_format.py +115 -0
- apisec_code_bolt/core/manifest.py +1009 -0
- apisec_code_bolt/core/repo.py +280 -0
- apisec_code_bolt/core/state.py +59 -0
- apisec_code_bolt/core/telemetry.py +451 -0
- apisec_code_bolt/core/types.py +587 -0
- apisec_code_bolt/fingerprinting/__init__.py +1 -0
- apisec_code_bolt/frameworks/__init__.py +29 -0
- apisec_code_bolt/frameworks/_jwt_common.py +50 -0
- apisec_code_bolt/frameworks/auth_helpers.py +437 -0
- apisec_code_bolt/frameworks/base.py +608 -0
- apisec_code_bolt/frameworks/dotnet/__init__.py +17 -0
- apisec_code_bolt/frameworks/dotnet/_path_helpers.py +43 -0
- apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +2546 -0
- apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +559 -0
- apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +545 -0
- apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +732 -0
- apisec_code_bolt/frameworks/dotnet/refit_plugin.py +374 -0
- apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +1239 -0
- apisec_code_bolt/frameworks/java/__init__.py +6 -0
- apisec_code_bolt/frameworks/java/_annotations.py +167 -0
- apisec_code_bolt/frameworks/java/_constraints.py +128 -0
- apisec_code_bolt/frameworks/java/graphql_plugin.py +287 -0
- apisec_code_bolt/frameworks/java/jaxrs_plugin.py +748 -0
- apisec_code_bolt/frameworks/java/jwt_config_extractor.py +361 -0
- apisec_code_bolt/frameworks/java/micronaut_plugin.py +1059 -0
- apisec_code_bolt/frameworks/java/spring_plugin.py +1293 -0
- apisec_code_bolt/frameworks/js/__init__.py +8 -0
- apisec_code_bolt/frameworks/js/express_plugin.py +391 -0
- apisec_code_bolt/frameworks/js/fastify_plugin.py +381 -0
- apisec_code_bolt/frameworks/js/graphql_plugin.py +198 -0
- apisec_code_bolt/frameworks/js/nestjs_plugin.py +423 -0
- apisec_code_bolt/frameworks/python/__init__.py +19 -0
- apisec_code_bolt/frameworks/python/celery_plugin.py +393 -0
- apisec_code_bolt/frameworks/python/click_plugin.py +427 -0
- apisec_code_bolt/frameworks/python/django_plugin.py +867 -0
- apisec_code_bolt/frameworks/python/fastapi/__init__.py +28 -0
- apisec_code_bolt/frameworks/python/fastapi/plugin.py +1390 -0
- apisec_code_bolt/frameworks/python/flask_plugin.py +205 -0
- apisec_code_bolt/frameworks/python/graphql_plugin.py +274 -0
- apisec_code_bolt/frameworks/python/prefect_plugin.py +251 -0
- apisec_code_bolt/frameworks/python/webhook_plugin.py +255 -0
- apisec_code_bolt/parsing/__init__.py +62 -0
- apisec_code_bolt/parsing/base.py +554 -0
- apisec_code_bolt/parsing/csharp/__init__.py +5 -0
- apisec_code_bolt/parsing/csharp/language_services.py +203 -0
- apisec_code_bolt/parsing/csharp/literals.py +72 -0
- apisec_code_bolt/parsing/csharp/parser.py +1158 -0
- apisec_code_bolt/parsing/csharp/type_resolver.py +568 -0
- apisec_code_bolt/parsing/js/__init__.py +5 -0
- apisec_code_bolt/parsing/js/language_services.py +118 -0
- apisec_code_bolt/parsing/js/parser.py +622 -0
- apisec_code_bolt/parsing/jvm/__init__.py +7 -0
- apisec_code_bolt/parsing/jvm/language_services.py +270 -0
- apisec_code_bolt/parsing/jvm/parser.py +774 -0
- apisec_code_bolt/parsing/jvm/type_resolver.py +422 -0
- apisec_code_bolt/parsing/python/__init__.py +150 -0
- apisec_code_bolt/parsing/python/cbv_extractor.py +606 -0
- apisec_code_bolt/parsing/python/constant_resolver.py +500 -0
- apisec_code_bolt/parsing/python/cross_file_resolver.py +1054 -0
- apisec_code_bolt/parsing/python/dynamic_route_detector.py +532 -0
- apisec_code_bolt/parsing/python/expression_utils.py +221 -0
- apisec_code_bolt/parsing/python/extraction_types.py +271 -0
- apisec_code_bolt/parsing/python/language_services.py +487 -0
- apisec_code_bolt/parsing/python/parameter_analyzer.py +789 -0
- apisec_code_bolt/parsing/python/parser.py +719 -0
- apisec_code_bolt/parsing/python/path_resolver.py +576 -0
- apisec_code_bolt/parsing/python/router_registry.py +806 -0
- apisec_code_bolt/parsing/python/type_resolver.py +730 -0
- apisec_code_bolt/parsing/python/visitors.py +1544 -0
- apisec_code_bolt/parsing/services.py +544 -0
- apisec_code_bolt/query/__init__.py +1 -0
- apisec_code_bolt/query/ast_cache.py +182 -0
- apisec_code_bolt/query/executor.py +283 -0
- apisec_code_bolt/query/handlers.py +832 -0
- apisec_code_bolt-0.1.0.dist-info/METADATA +230 -0
- apisec_code_bolt-0.1.0.dist-info/RECORD +111 -0
- apisec_code_bolt-0.1.0.dist-info/WHEEL +4 -0
- apisec_code_bolt-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JavaScript/TypeScript parser using tree-sitter.
|
|
3
|
+
|
|
4
|
+
Supports:
|
|
5
|
+
- ES modules (import/export)
|
|
6
|
+
- CommonJS (require/module.exports)
|
|
7
|
+
- TypeScript (.ts/.tsx) via tree-sitter-typescript
|
|
8
|
+
- NestJS decorators (@Controller, @Get, @Post, etc.)
|
|
9
|
+
- Express.js route registration (app.get, router.post, etc.)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
import tree_sitter_javascript as tsjs
|
|
19
|
+
from tree_sitter import Language as TSLanguage
|
|
20
|
+
from tree_sitter import Node, Parser
|
|
21
|
+
|
|
22
|
+
from ...core.types import CodeLocation, Language, QualifiedName
|
|
23
|
+
from ..base import (
|
|
24
|
+
BaseParser,
|
|
25
|
+
ParsedArgument,
|
|
26
|
+
ParsedCallSite,
|
|
27
|
+
ParsedClass,
|
|
28
|
+
ParsedDecorator,
|
|
29
|
+
ParsedFile,
|
|
30
|
+
ParsedFunction,
|
|
31
|
+
ParsedImport,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
import tree_sitter_typescript as tsts
|
|
36
|
+
|
|
37
|
+
_TS_LANGUAGE = TSLanguage(tsts.language_typescript())
|
|
38
|
+
_TS_PARSER: Parser | None = Parser(_TS_LANGUAGE)
|
|
39
|
+
except Exception:
|
|
40
|
+
_TS_LANGUAGE = None # type: ignore
|
|
41
|
+
_TS_PARSER = None
|
|
42
|
+
|
|
43
|
+
_JS_LANGUAGE = TSLanguage(tsjs.language())
|
|
44
|
+
_PARSER = Parser(_JS_LANGUAGE)
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# NestJS HTTP verb decorator names
|
|
53
|
+
VERB_DECORATOR_NAMES: frozenset[str] = frozenset(
|
|
54
|
+
{
|
|
55
|
+
"Get",
|
|
56
|
+
"Post",
|
|
57
|
+
"Put",
|
|
58
|
+
"Patch",
|
|
59
|
+
"Delete",
|
|
60
|
+
"Options",
|
|
61
|
+
"Head",
|
|
62
|
+
"All",
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# TypeScript accessibility/other modifier keywords to skip when parsing method names
|
|
67
|
+
_MODIFIERS: frozenset[str] = frozenset(
|
|
68
|
+
{
|
|
69
|
+
"public",
|
|
70
|
+
"private",
|
|
71
|
+
"protected",
|
|
72
|
+
"static",
|
|
73
|
+
"abstract",
|
|
74
|
+
"override",
|
|
75
|
+
"readonly",
|
|
76
|
+
"async",
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class JavaScriptParser(BaseParser):
|
|
82
|
+
"""
|
|
83
|
+
Tree-sitter based JavaScript/TypeScript parser.
|
|
84
|
+
|
|
85
|
+
Extracts imports, classes (with decorators and methods), functions,
|
|
86
|
+
and call sites from JS/TS source files.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
LANGUAGE: Language = Language.JAVASCRIPT
|
|
90
|
+
SUPPORTED_EXTENSIONS: frozenset[str] = frozenset(
|
|
91
|
+
{
|
|
92
|
+
".js",
|
|
93
|
+
".mjs",
|
|
94
|
+
".cjs",
|
|
95
|
+
".ts",
|
|
96
|
+
".tsx",
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def parse_file(self, file_path: Path) -> ParsedFile:
|
|
101
|
+
"""Parse a JS/TS source file."""
|
|
102
|
+
try:
|
|
103
|
+
source_bytes = file_path.read_bytes()
|
|
104
|
+
return self._parse(source_bytes, file_path)
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
from ...core.types import ParseError
|
|
107
|
+
|
|
108
|
+
pf = ParsedFile(path=file_path, language=self.LANGUAGE, success=False)
|
|
109
|
+
pf.error = ParseError(str(exc), file_path=file_path)
|
|
110
|
+
return pf
|
|
111
|
+
|
|
112
|
+
def parse_source(self, source: str, file_path: Path | None = None) -> ParsedFile:
|
|
113
|
+
"""Parse JS/TS source code from a string."""
|
|
114
|
+
source_bytes = source.encode("utf-8", errors="replace")
|
|
115
|
+
fp = file_path or Path("<unknown>.js")
|
|
116
|
+
return self._parse(source_bytes, fp)
|
|
117
|
+
|
|
118
|
+
def _parse(self, source_bytes: bytes, file_path: Path) -> ParsedFile:
|
|
119
|
+
"""Core parsing logic — selects TS or JS parser based on extension."""
|
|
120
|
+
ext = file_path.suffix.lower()
|
|
121
|
+
if ext in (".ts", ".tsx") and _TS_PARSER is not None:
|
|
122
|
+
parser = _TS_PARSER
|
|
123
|
+
is_ts = True
|
|
124
|
+
else:
|
|
125
|
+
parser = _PARSER
|
|
126
|
+
is_ts = False
|
|
127
|
+
|
|
128
|
+
tree = parser.parse(source_bytes)
|
|
129
|
+
root = tree.root_node
|
|
130
|
+
|
|
131
|
+
visitor = _Visitor(source_bytes, file_path, is_ts=is_ts)
|
|
132
|
+
visitor.visit(root)
|
|
133
|
+
|
|
134
|
+
pf = ParsedFile(path=file_path, language=self.LANGUAGE)
|
|
135
|
+
pf.imports = visitor.imports
|
|
136
|
+
pf.functions = visitor.functions
|
|
137
|
+
pf.classes = visitor.classes
|
|
138
|
+
pf.call_sites = visitor.call_sites
|
|
139
|
+
pf.line_count = source_bytes.count(b"\n") + 1
|
|
140
|
+
|
|
141
|
+
module_stem = file_path.stem
|
|
142
|
+
pf.module_name = module_stem
|
|
143
|
+
|
|
144
|
+
return pf
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class _Visitor:
|
|
148
|
+
"""Walk a tree-sitter AST and extract parsed symbols."""
|
|
149
|
+
|
|
150
|
+
def __init__(self, source: bytes, file_path: Path, is_ts: bool = False) -> None:
|
|
151
|
+
self._src = source
|
|
152
|
+
self._file = file_path
|
|
153
|
+
self._is_ts = is_ts
|
|
154
|
+
self._module = file_path.stem
|
|
155
|
+
|
|
156
|
+
self.imports: list[ParsedImport] = []
|
|
157
|
+
self.functions: list[ParsedFunction] = []
|
|
158
|
+
self.classes: list[ParsedClass] = []
|
|
159
|
+
self.call_sites: list[ParsedCallSite] = []
|
|
160
|
+
|
|
161
|
+
def _text(self, node: Node) -> str:
|
|
162
|
+
return self._src[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
|
163
|
+
|
|
164
|
+
def _loc(self, node: Node) -> CodeLocation:
|
|
165
|
+
return CodeLocation(
|
|
166
|
+
file=self._file,
|
|
167
|
+
line=node.start_point[0] + 1,
|
|
168
|
+
column=node.start_point[1],
|
|
169
|
+
end_line=node.end_point[0] + 1,
|
|
170
|
+
end_column=node.end_point[1],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def visit(self, node: Node) -> None:
|
|
174
|
+
"""Dispatch node to the appropriate visitor method."""
|
|
175
|
+
handler_name = f"_visit_{node.type}"
|
|
176
|
+
handler = getattr(self, handler_name, None)
|
|
177
|
+
if handler:
|
|
178
|
+
handler(node)
|
|
179
|
+
else:
|
|
180
|
+
for child in node.children:
|
|
181
|
+
self.visit(child)
|
|
182
|
+
|
|
183
|
+
# =========================================================================
|
|
184
|
+
# Import extraction
|
|
185
|
+
# =========================================================================
|
|
186
|
+
|
|
187
|
+
def _visit_import_statement(self, node: Node) -> None:
|
|
188
|
+
"""Handle ES import statement: import X from 'module'"""
|
|
189
|
+
source_node = node.child_by_field_name("source")
|
|
190
|
+
if source_node is None:
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
module = self._text(source_node).strip("'\"")
|
|
194
|
+
names: list[str] = []
|
|
195
|
+
|
|
196
|
+
for child in node.children:
|
|
197
|
+
if child.type == "import_clause":
|
|
198
|
+
for sub in child.children:
|
|
199
|
+
if sub.type == "identifier":
|
|
200
|
+
names.append(self._text(sub))
|
|
201
|
+
elif sub.type in ("named_imports", "namespace_import"):
|
|
202
|
+
for item in sub.children:
|
|
203
|
+
if item.type in ("import_specifier", "identifier"):
|
|
204
|
+
name_node = item.child_by_field_name("name") or item
|
|
205
|
+
if name_node.type == "identifier":
|
|
206
|
+
names.append(self._text(name_node))
|
|
207
|
+
|
|
208
|
+
self.imports.append(
|
|
209
|
+
ParsedImport(
|
|
210
|
+
module=module,
|
|
211
|
+
names=names,
|
|
212
|
+
location=self._loc(node),
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def _visit_lexical_declaration(self, node: Node) -> None:
|
|
217
|
+
"""Handle const/let declarations — extract require() as imports."""
|
|
218
|
+
self._extract_require_import(node)
|
|
219
|
+
for child in node.children:
|
|
220
|
+
self.visit(child)
|
|
221
|
+
|
|
222
|
+
def _visit_variable_declaration(self, node: Node) -> None:
|
|
223
|
+
"""Handle var declarations — extract require() as imports."""
|
|
224
|
+
self._extract_require_import(node)
|
|
225
|
+
for child in node.children:
|
|
226
|
+
self.visit(child)
|
|
227
|
+
|
|
228
|
+
def _extract_require_import(self, node: Node) -> None:
|
|
229
|
+
"""Extract CommonJS require() import from declaration node."""
|
|
230
|
+
src = self._text(node)
|
|
231
|
+
if "require(" not in src:
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
import re
|
|
235
|
+
|
|
236
|
+
# const foo = require('bar') or const { foo } = require('bar')
|
|
237
|
+
m = re.search(r'require\s*\(\s*[\'"]([^\'"]+)[\'"]\s*\)', src)
|
|
238
|
+
if not m:
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
module = m.group(1)
|
|
242
|
+
names: list[str] = []
|
|
243
|
+
|
|
244
|
+
# Try to extract destructured names: const { Router } = require('express')
|
|
245
|
+
dest = re.search(r"const\s*\{([^}]+)\}\s*=", src)
|
|
246
|
+
if dest:
|
|
247
|
+
for raw_name in dest.group(1).split(","):
|
|
248
|
+
name = raw_name.strip()
|
|
249
|
+
if name:
|
|
250
|
+
names.append(name)
|
|
251
|
+
else:
|
|
252
|
+
# const express = require('express') → just record as "express"
|
|
253
|
+
id_m = re.match(r"(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)", src)
|
|
254
|
+
if id_m:
|
|
255
|
+
names.append(id_m.group(1))
|
|
256
|
+
|
|
257
|
+
self.imports.append(
|
|
258
|
+
ParsedImport(
|
|
259
|
+
module=module,
|
|
260
|
+
names=names,
|
|
261
|
+
location=self._loc(node),
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# =========================================================================
|
|
266
|
+
# Export statement (for `export class Foo ...`)
|
|
267
|
+
# =========================================================================
|
|
268
|
+
|
|
269
|
+
def _visit_export_statement(self, node: Node) -> None:
|
|
270
|
+
"""Handle export statements — delegate class/function to inner visitor."""
|
|
271
|
+
for child in node.children:
|
|
272
|
+
if child.type == "class_declaration":
|
|
273
|
+
self._visit_class_declaration(child, parent_node=node)
|
|
274
|
+
elif child.type == "function_declaration":
|
|
275
|
+
self._visit_function_declaration(child)
|
|
276
|
+
else:
|
|
277
|
+
self.visit(child)
|
|
278
|
+
|
|
279
|
+
# =========================================================================
|
|
280
|
+
# Class extraction
|
|
281
|
+
# =========================================================================
|
|
282
|
+
|
|
283
|
+
def _visit_class_declaration(self, node: Node, parent_node: Node | None = None) -> None:
|
|
284
|
+
"""Extract class with decorators and methods."""
|
|
285
|
+
name_node = node.child_by_field_name("name")
|
|
286
|
+
if name_node is None:
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
class_name = self._text(name_node)
|
|
290
|
+
|
|
291
|
+
# Collect decorators from the node itself and from parent_node (export statement)
|
|
292
|
+
decorators: list[ParsedDecorator] = []
|
|
293
|
+
for search_node in [parent_node, node] if parent_node else [node]:
|
|
294
|
+
if search_node is None:
|
|
295
|
+
continue
|
|
296
|
+
for child in search_node.children:
|
|
297
|
+
if child.type == "decorator":
|
|
298
|
+
dec = self._parse_decorator(child)
|
|
299
|
+
if dec:
|
|
300
|
+
decorators.append(dec)
|
|
301
|
+
|
|
302
|
+
# Build class object
|
|
303
|
+
cls = ParsedClass(
|
|
304
|
+
name=class_name,
|
|
305
|
+
qualified_name=QualifiedName(module=self._module, name=class_name),
|
|
306
|
+
location=self._loc(node),
|
|
307
|
+
decorators=decorators,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Visit the class body for methods
|
|
311
|
+
body = node.child_by_field_name("body")
|
|
312
|
+
if body:
|
|
313
|
+
# In the TS parser, decorators on methods appear as sibling statements
|
|
314
|
+
# BEFORE the method_definition in the class body. We accumulate them.
|
|
315
|
+
pending_decorators: list[ParsedDecorator] = []
|
|
316
|
+
for child in body.children:
|
|
317
|
+
if child.type == "decorator":
|
|
318
|
+
dec = self._parse_decorator(child)
|
|
319
|
+
if dec:
|
|
320
|
+
pending_decorators.append(dec)
|
|
321
|
+
elif child.type == "method_definition":
|
|
322
|
+
method = self._visit_method_definition(
|
|
323
|
+
child, extra_decorators=pending_decorators
|
|
324
|
+
)
|
|
325
|
+
if method:
|
|
326
|
+
method.owner_type = class_name
|
|
327
|
+
cls.methods.append(method)
|
|
328
|
+
pending_decorators = []
|
|
329
|
+
elif child.type == "field_definition" or child.type == "public_field_definition":
|
|
330
|
+
method = self._visit_field_definition(
|
|
331
|
+
child, extra_decorators=pending_decorators
|
|
332
|
+
)
|
|
333
|
+
if method:
|
|
334
|
+
method.owner_type = class_name
|
|
335
|
+
cls.methods.append(method)
|
|
336
|
+
pending_decorators = []
|
|
337
|
+
else:
|
|
338
|
+
if pending_decorators and child.type not in (
|
|
339
|
+
"{",
|
|
340
|
+
"}",
|
|
341
|
+
";",
|
|
342
|
+
"comment",
|
|
343
|
+
):
|
|
344
|
+
pending_decorators = []
|
|
345
|
+
|
|
346
|
+
self.classes.append(cls)
|
|
347
|
+
|
|
348
|
+
def _visit_method_definition(
|
|
349
|
+
self,
|
|
350
|
+
node: Node,
|
|
351
|
+
extra_decorators: list[ParsedDecorator] | None = None,
|
|
352
|
+
) -> ParsedFunction | None:
|
|
353
|
+
"""Extract a method definition from a class body."""
|
|
354
|
+
name_node = node.child_by_field_name("name")
|
|
355
|
+
if name_node is None:
|
|
356
|
+
# TS sometimes wraps `public methodName()` as ERROR node; try text fallback
|
|
357
|
+
name = self._extract_method_name_from_text(node)
|
|
358
|
+
if not name:
|
|
359
|
+
return None
|
|
360
|
+
else:
|
|
361
|
+
name = self._text(name_node)
|
|
362
|
+
# Strip TS accessibility modifiers that appear as part of the name node text
|
|
363
|
+
if name in _MODIFIERS:
|
|
364
|
+
# The real name is after the modifier
|
|
365
|
+
rest = self._text(node)
|
|
366
|
+
import re
|
|
367
|
+
|
|
368
|
+
m = re.search(
|
|
369
|
+
r"\b(?:" + "|".join(_MODIFIERS) + r")\s+([A-Za-z_$][A-Za-z0-9_$]*)", rest
|
|
370
|
+
)
|
|
371
|
+
if m:
|
|
372
|
+
name = m.group(1)
|
|
373
|
+
else:
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
# Collect decorators directly on the method node
|
|
377
|
+
decorators: list[ParsedDecorator] = list(extra_decorators or [])
|
|
378
|
+
for child in node.children:
|
|
379
|
+
if child.type == "decorator":
|
|
380
|
+
dec = self._parse_decorator(child)
|
|
381
|
+
if dec:
|
|
382
|
+
decorators.append(dec)
|
|
383
|
+
|
|
384
|
+
return ParsedFunction(
|
|
385
|
+
name=name,
|
|
386
|
+
qualified_name=QualifiedName(module=self._module, name=name),
|
|
387
|
+
location=self._loc(node),
|
|
388
|
+
decorators=decorators,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def _extract_method_name_from_text(self, node: Node) -> str | None:
|
|
392
|
+
"""Fallback: extract method name from raw text of an ERROR or modifier node."""
|
|
393
|
+
import re
|
|
394
|
+
|
|
395
|
+
text = self._text(node)
|
|
396
|
+
# Remove modifiers and find the first identifier
|
|
397
|
+
for mod in sorted(_MODIFIERS, key=len, reverse=True):
|
|
398
|
+
text = re.sub(r"\b" + mod + r"\b\s*", "", text)
|
|
399
|
+
m = re.match(r"\s*([A-Za-z_$][A-Za-z0-9_$]*)", text.strip())
|
|
400
|
+
return m.group(1) if m else None
|
|
401
|
+
|
|
402
|
+
def _visit_field_definition(
|
|
403
|
+
self,
|
|
404
|
+
node: Node,
|
|
405
|
+
extra_decorators: list[ParsedDecorator] | None = None,
|
|
406
|
+
) -> ParsedFunction | None:
|
|
407
|
+
"""
|
|
408
|
+
Handle field_definition nodes that have verb decorators.
|
|
409
|
+
|
|
410
|
+
tree-sitter sometimes parses NestJS methods as field_definitions when
|
|
411
|
+
the TypeScript parser encounters methods with HTTP verb decorators.
|
|
412
|
+
We treat them as methods for route extraction purposes.
|
|
413
|
+
"""
|
|
414
|
+
# Only care about fields that have verb decorators
|
|
415
|
+
decs = list(extra_decorators or [])
|
|
416
|
+
has_verb = any(d.name in VERB_DECORATOR_NAMES for d in decs)
|
|
417
|
+
if not has_verb:
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
name_node = node.child_by_field_name("name") or node.child_by_field_name("property")
|
|
421
|
+
if name_node is None:
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
name = self._text(name_node)
|
|
425
|
+
if name in _MODIFIERS:
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
return ParsedFunction(
|
|
429
|
+
name=name,
|
|
430
|
+
qualified_name=QualifiedName(module=self._module, name=name),
|
|
431
|
+
location=self._loc(node),
|
|
432
|
+
decorators=decs,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# =========================================================================
|
|
436
|
+
# Function extraction
|
|
437
|
+
# =========================================================================
|
|
438
|
+
|
|
439
|
+
def _visit_function_declaration(self, node: Node) -> None:
|
|
440
|
+
"""Extract a top-level function declaration."""
|
|
441
|
+
name_node = node.child_by_field_name("name")
|
|
442
|
+
if name_node is None:
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
name = self._text(name_node)
|
|
446
|
+
func = ParsedFunction(
|
|
447
|
+
name=name,
|
|
448
|
+
qualified_name=QualifiedName(module=self._module, name=name),
|
|
449
|
+
location=self._loc(node),
|
|
450
|
+
)
|
|
451
|
+
self.functions.append(func)
|
|
452
|
+
# Also visit the body for nested calls
|
|
453
|
+
body = node.child_by_field_name("body")
|
|
454
|
+
if body:
|
|
455
|
+
for child in body.children:
|
|
456
|
+
self.visit(child)
|
|
457
|
+
|
|
458
|
+
# =========================================================================
|
|
459
|
+
# Call site extraction
|
|
460
|
+
# =========================================================================
|
|
461
|
+
|
|
462
|
+
def _visit_expression_statement(self, node: Node) -> None:
|
|
463
|
+
"""Visit expression statement — extract call sites."""
|
|
464
|
+
for child in node.children:
|
|
465
|
+
self._try_call_site(child)
|
|
466
|
+
self.visit(child)
|
|
467
|
+
|
|
468
|
+
def _try_call_site(self, node: Node) -> None:
|
|
469
|
+
"""Attempt to extract a call site from an expression node."""
|
|
470
|
+
if node.type not in ("call_expression", "await_expression"):
|
|
471
|
+
return
|
|
472
|
+
|
|
473
|
+
target = node
|
|
474
|
+
if node.type == "await_expression":
|
|
475
|
+
# await someCall(...) — dig into the call
|
|
476
|
+
for child in node.children:
|
|
477
|
+
if child.type == "call_expression":
|
|
478
|
+
target = child
|
|
479
|
+
break
|
|
480
|
+
if target is node:
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
fn = target.child_by_field_name("function")
|
|
484
|
+
args_node = target.child_by_field_name("arguments")
|
|
485
|
+
if fn is None:
|
|
486
|
+
return
|
|
487
|
+
|
|
488
|
+
callee_name = ""
|
|
489
|
+
is_method_call = False
|
|
490
|
+
receiver_expression: str | None = None
|
|
491
|
+
|
|
492
|
+
if fn.type == "member_expression":
|
|
493
|
+
obj = fn.child_by_field_name("object")
|
|
494
|
+
prop = fn.child_by_field_name("property")
|
|
495
|
+
if obj and prop:
|
|
496
|
+
receiver_expression = self._text(obj)
|
|
497
|
+
callee_name = self._text(prop)
|
|
498
|
+
is_method_call = True
|
|
499
|
+
elif fn.type == "identifier":
|
|
500
|
+
callee_name = self._text(fn)
|
|
501
|
+
else:
|
|
502
|
+
callee_name = self._text(fn)
|
|
503
|
+
|
|
504
|
+
if not callee_name:
|
|
505
|
+
return
|
|
506
|
+
|
|
507
|
+
# Parse arguments
|
|
508
|
+
arguments: list[ParsedArgument] = []
|
|
509
|
+
if args_node:
|
|
510
|
+
pos = 0
|
|
511
|
+
for child in args_node.children:
|
|
512
|
+
if child.type in (",", "(", ")"):
|
|
513
|
+
continue
|
|
514
|
+
arg = self._parse_argument(child, pos)
|
|
515
|
+
arguments.append(arg)
|
|
516
|
+
pos += 1
|
|
517
|
+
|
|
518
|
+
call = ParsedCallSite(
|
|
519
|
+
callee_name=callee_name,
|
|
520
|
+
location=self._loc(target),
|
|
521
|
+
is_method_call=is_method_call,
|
|
522
|
+
receiver_expression=receiver_expression,
|
|
523
|
+
arguments=arguments,
|
|
524
|
+
)
|
|
525
|
+
self.call_sites.append(call)
|
|
526
|
+
|
|
527
|
+
# =========================================================================
|
|
528
|
+
# Decorator parsing
|
|
529
|
+
# =========================================================================
|
|
530
|
+
|
|
531
|
+
def _parse_decorator(self, node: Node) -> ParsedDecorator | None:
|
|
532
|
+
"""Parse a decorator node into a ParsedDecorator."""
|
|
533
|
+
|
|
534
|
+
# The decorator node has children: '@' + expression
|
|
535
|
+
# e.g. @Controller('prefix') or @Get() or @Module({...})
|
|
536
|
+
text = self._text(node)
|
|
537
|
+
# Strip leading @
|
|
538
|
+
text = text.lstrip("@").strip()
|
|
539
|
+
|
|
540
|
+
# Name is everything before the first '('
|
|
541
|
+
name = text[: text.index("(")].strip() if "(" in text else text.strip()
|
|
542
|
+
|
|
543
|
+
dec = ParsedDecorator(
|
|
544
|
+
name=name,
|
|
545
|
+
location=self._loc(node),
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
# Try to parse arguments
|
|
549
|
+
# Find the arguments node inside the call_expression within the decorator
|
|
550
|
+
for child in node.children:
|
|
551
|
+
if child.type in ("call_expression",):
|
|
552
|
+
args = child.child_by_field_name("arguments")
|
|
553
|
+
if args:
|
|
554
|
+
pos = 0
|
|
555
|
+
for arg_child in args.children:
|
|
556
|
+
if arg_child.type in (",", "(", ")"):
|
|
557
|
+
continue
|
|
558
|
+
arg_text = self._text(arg_child).strip("'\"")
|
|
559
|
+
if arg_child.type in ("string", "template_string"):
|
|
560
|
+
dec.arguments["value"] = arg_text
|
|
561
|
+
dec.positional_args.append(arg_text)
|
|
562
|
+
elif arg_child.type == "object":
|
|
563
|
+
# Object literal: { path: 'x', version: 1 }
|
|
564
|
+
obj_text = self._text(arg_child)
|
|
565
|
+
dec.arguments["value"] = obj_text
|
|
566
|
+
dec.positional_args.append(obj_text)
|
|
567
|
+
elif arg_child.type == "identifier" or arg_child.type in (
|
|
568
|
+
"number",
|
|
569
|
+
"true",
|
|
570
|
+
"false",
|
|
571
|
+
):
|
|
572
|
+
dec.arguments["value"] = arg_text
|
|
573
|
+
dec.positional_args.append(arg_text)
|
|
574
|
+
elif arg_child.type == "member_expression":
|
|
575
|
+
# e.g. RoleEnum.admin, Permission.READ
|
|
576
|
+
dec.positional_args.append(self._text(arg_child))
|
|
577
|
+
elif arg_child.type == "call_expression":
|
|
578
|
+
# e.g. AuthGuard('jwt'), Roles('admin')
|
|
579
|
+
dec.positional_args.append(self._text(arg_child))
|
|
580
|
+
pos += 1
|
|
581
|
+
|
|
582
|
+
return dec
|
|
583
|
+
|
|
584
|
+
# =========================================================================
|
|
585
|
+
# Argument parsing
|
|
586
|
+
# =========================================================================
|
|
587
|
+
|
|
588
|
+
def _parse_argument(self, node: Node, position: int) -> ParsedArgument:
|
|
589
|
+
"""Parse a call argument node into a ParsedArgument."""
|
|
590
|
+
arg = ParsedArgument(position=position)
|
|
591
|
+
|
|
592
|
+
if node.type in ("string", "template_string"):
|
|
593
|
+
raw = self._text(node)
|
|
594
|
+
arg.is_literal = True
|
|
595
|
+
arg.literal_value = raw.strip("'\"")
|
|
596
|
+
arg.literal_type = "str"
|
|
597
|
+
elif node.type in ("number",):
|
|
598
|
+
arg.is_literal = True
|
|
599
|
+
raw = self._text(node)
|
|
600
|
+
arg.literal_value = raw
|
|
601
|
+
arg.literal_type = "number"
|
|
602
|
+
elif node.type in ("true", "false"):
|
|
603
|
+
arg.is_literal = True
|
|
604
|
+
arg.literal_value = self._text(node) == "true"
|
|
605
|
+
arg.literal_type = "bool"
|
|
606
|
+
elif node.type == "identifier":
|
|
607
|
+
arg.is_variable = True
|
|
608
|
+
arg.variable_name = self._text(node)
|
|
609
|
+
elif node.type == "arrow_function":
|
|
610
|
+
arg.is_expression = True
|
|
611
|
+
arg.expression_text = self._text(node)
|
|
612
|
+
else:
|
|
613
|
+
arg.is_expression = True
|
|
614
|
+
arg.expression_text = self._text(node)
|
|
615
|
+
|
|
616
|
+
return arg
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
# Auto-register
|
|
620
|
+
from ..base import ParserRegistry as _PR # noqa: E402
|
|
621
|
+
|
|
622
|
+
_PR.register(JavaScriptParser())
|