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.
Files changed (111) hide show
  1. apisec_code_bolt/__init__.py +42 -0
  2. apisec_code_bolt/__main__.py +11 -0
  3. apisec_code_bolt/analysis/__init__.py +96 -0
  4. apisec_code_bolt/analysis/analyzer.py +2309 -0
  5. apisec_code_bolt/analysis/binding_tracker.py +341 -0
  6. apisec_code_bolt/analysis/call_graph.py +1197 -0
  7. apisec_code_bolt/analysis/call_graph_types.py +332 -0
  8. apisec_code_bolt/analysis/call_resolver.py +988 -0
  9. apisec_code_bolt/analysis/capability_tagger.py +322 -0
  10. apisec_code_bolt/analysis/config_scanner.py +197 -0
  11. apisec_code_bolt/analysis/data_flow.py +1883 -0
  12. apisec_code_bolt/analysis/dependency_extractor.py +959 -0
  13. apisec_code_bolt/analysis/flow_analysis.py +1406 -0
  14. apisec_code_bolt/analysis/hof_catalog.py +61 -0
  15. apisec_code_bolt/analysis/integration_detector.py +1399 -0
  16. apisec_code_bolt/analysis/literal_scanner.py +300 -0
  17. apisec_code_bolt/analysis/path_normalizer.py +55 -0
  18. apisec_code_bolt/analysis/read_site_detector.py +310 -0
  19. apisec_code_bolt/analysis/request_patterns.py +162 -0
  20. apisec_code_bolt/analysis/sensitivity_classifier.py +224 -0
  21. apisec_code_bolt/analysis/sink_evidence.py +333 -0
  22. apisec_code_bolt/analysis/url_prefix_resolver.py +338 -0
  23. apisec_code_bolt/cli/__init__.py +5 -0
  24. apisec_code_bolt/cli/exit_codes.py +17 -0
  25. apisec_code_bolt/cli/main.py +1069 -0
  26. apisec_code_bolt/cloud/__init__.py +1 -0
  27. apisec_code_bolt/cloud/apisec_client.py +118 -0
  28. apisec_code_bolt/cloud/client.py +255 -0
  29. apisec_code_bolt/core/__init__.py +75 -0
  30. apisec_code_bolt/core/config.py +528 -0
  31. apisec_code_bolt/core/credentials.py +65 -0
  32. apisec_code_bolt/core/discovery.py +433 -0
  33. apisec_code_bolt/core/log_format.py +115 -0
  34. apisec_code_bolt/core/manifest.py +1009 -0
  35. apisec_code_bolt/core/repo.py +280 -0
  36. apisec_code_bolt/core/state.py +59 -0
  37. apisec_code_bolt/core/telemetry.py +451 -0
  38. apisec_code_bolt/core/types.py +587 -0
  39. apisec_code_bolt/fingerprinting/__init__.py +1 -0
  40. apisec_code_bolt/frameworks/__init__.py +29 -0
  41. apisec_code_bolt/frameworks/_jwt_common.py +50 -0
  42. apisec_code_bolt/frameworks/auth_helpers.py +437 -0
  43. apisec_code_bolt/frameworks/base.py +608 -0
  44. apisec_code_bolt/frameworks/dotnet/__init__.py +17 -0
  45. apisec_code_bolt/frameworks/dotnet/_path_helpers.py +43 -0
  46. apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +2546 -0
  47. apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +559 -0
  48. apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +545 -0
  49. apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +732 -0
  50. apisec_code_bolt/frameworks/dotnet/refit_plugin.py +374 -0
  51. apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +1239 -0
  52. apisec_code_bolt/frameworks/java/__init__.py +6 -0
  53. apisec_code_bolt/frameworks/java/_annotations.py +167 -0
  54. apisec_code_bolt/frameworks/java/_constraints.py +128 -0
  55. apisec_code_bolt/frameworks/java/graphql_plugin.py +287 -0
  56. apisec_code_bolt/frameworks/java/jaxrs_plugin.py +748 -0
  57. apisec_code_bolt/frameworks/java/jwt_config_extractor.py +361 -0
  58. apisec_code_bolt/frameworks/java/micronaut_plugin.py +1059 -0
  59. apisec_code_bolt/frameworks/java/spring_plugin.py +1293 -0
  60. apisec_code_bolt/frameworks/js/__init__.py +8 -0
  61. apisec_code_bolt/frameworks/js/express_plugin.py +391 -0
  62. apisec_code_bolt/frameworks/js/fastify_plugin.py +381 -0
  63. apisec_code_bolt/frameworks/js/graphql_plugin.py +198 -0
  64. apisec_code_bolt/frameworks/js/nestjs_plugin.py +423 -0
  65. apisec_code_bolt/frameworks/python/__init__.py +19 -0
  66. apisec_code_bolt/frameworks/python/celery_plugin.py +393 -0
  67. apisec_code_bolt/frameworks/python/click_plugin.py +427 -0
  68. apisec_code_bolt/frameworks/python/django_plugin.py +867 -0
  69. apisec_code_bolt/frameworks/python/fastapi/__init__.py +28 -0
  70. apisec_code_bolt/frameworks/python/fastapi/plugin.py +1390 -0
  71. apisec_code_bolt/frameworks/python/flask_plugin.py +205 -0
  72. apisec_code_bolt/frameworks/python/graphql_plugin.py +274 -0
  73. apisec_code_bolt/frameworks/python/prefect_plugin.py +251 -0
  74. apisec_code_bolt/frameworks/python/webhook_plugin.py +255 -0
  75. apisec_code_bolt/parsing/__init__.py +62 -0
  76. apisec_code_bolt/parsing/base.py +554 -0
  77. apisec_code_bolt/parsing/csharp/__init__.py +5 -0
  78. apisec_code_bolt/parsing/csharp/language_services.py +203 -0
  79. apisec_code_bolt/parsing/csharp/literals.py +72 -0
  80. apisec_code_bolt/parsing/csharp/parser.py +1158 -0
  81. apisec_code_bolt/parsing/csharp/type_resolver.py +568 -0
  82. apisec_code_bolt/parsing/js/__init__.py +5 -0
  83. apisec_code_bolt/parsing/js/language_services.py +118 -0
  84. apisec_code_bolt/parsing/js/parser.py +622 -0
  85. apisec_code_bolt/parsing/jvm/__init__.py +7 -0
  86. apisec_code_bolt/parsing/jvm/language_services.py +270 -0
  87. apisec_code_bolt/parsing/jvm/parser.py +774 -0
  88. apisec_code_bolt/parsing/jvm/type_resolver.py +422 -0
  89. apisec_code_bolt/parsing/python/__init__.py +150 -0
  90. apisec_code_bolt/parsing/python/cbv_extractor.py +606 -0
  91. apisec_code_bolt/parsing/python/constant_resolver.py +500 -0
  92. apisec_code_bolt/parsing/python/cross_file_resolver.py +1054 -0
  93. apisec_code_bolt/parsing/python/dynamic_route_detector.py +532 -0
  94. apisec_code_bolt/parsing/python/expression_utils.py +221 -0
  95. apisec_code_bolt/parsing/python/extraction_types.py +271 -0
  96. apisec_code_bolt/parsing/python/language_services.py +487 -0
  97. apisec_code_bolt/parsing/python/parameter_analyzer.py +789 -0
  98. apisec_code_bolt/parsing/python/parser.py +719 -0
  99. apisec_code_bolt/parsing/python/path_resolver.py +576 -0
  100. apisec_code_bolt/parsing/python/router_registry.py +806 -0
  101. apisec_code_bolt/parsing/python/type_resolver.py +730 -0
  102. apisec_code_bolt/parsing/python/visitors.py +1544 -0
  103. apisec_code_bolt/parsing/services.py +544 -0
  104. apisec_code_bolt/query/__init__.py +1 -0
  105. apisec_code_bolt/query/ast_cache.py +182 -0
  106. apisec_code_bolt/query/executor.py +283 -0
  107. apisec_code_bolt/query/handlers.py +832 -0
  108. apisec_code_bolt-0.1.0.dist-info/METADATA +230 -0
  109. apisec_code_bolt-0.1.0.dist-info/RECORD +111 -0
  110. apisec_code_bolt-0.1.0.dist-info/WHEEL +4 -0
  111. 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())
@@ -0,0 +1,7 @@
1
+ """JVM (Java/Kotlin) parsing using the javalang Python library."""
2
+
3
+ from ..base import ParserRegistry
4
+ from .parser import JavaParser
5
+
6
+ # Register the Java parser so the analyzer can dispatch .java files
7
+ ParserRegistry.register(JavaParser())