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,422 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Java type resolver for cross-file type resolution.
|
|
3
|
+
|
|
4
|
+
Implements the TypeResolver protocol for Java projects.
|
|
5
|
+
Builds a symbol table from parsed Java files and resolves:
|
|
6
|
+
- Simple class names via import declarations
|
|
7
|
+
- Qualified names directly
|
|
8
|
+
- Same-package implicit visibility (no import required)
|
|
9
|
+
- Inheritance chains for model detection
|
|
10
|
+
- Generic type parameters (List<User> → User)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import re
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from ..base import ParsedClass, ParsedFile
|
|
20
|
+
from ..services import ResolvedField, ResolvedType
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Strip Java generic type parameters: "List<User>" → "List", "Map<K,V>" → "Map"
|
|
25
|
+
_GENERIC_RE = re.compile(r"<[^>]*>")
|
|
26
|
+
|
|
27
|
+
# Annotations that indicate a class is a "model" (DTO, entity, value object)
|
|
28
|
+
_MODEL_ANNOTATIONS = frozenset(
|
|
29
|
+
{
|
|
30
|
+
# JPA / Hibernate
|
|
31
|
+
"Entity",
|
|
32
|
+
"Embeddable",
|
|
33
|
+
"MappedSuperclass",
|
|
34
|
+
# Lombok
|
|
35
|
+
"Data",
|
|
36
|
+
"Value",
|
|
37
|
+
# Jackson
|
|
38
|
+
"JsonProperty",
|
|
39
|
+
# Spring
|
|
40
|
+
"RequestBody",
|
|
41
|
+
"ResponseBody",
|
|
42
|
+
# Generic / custom
|
|
43
|
+
"DTO",
|
|
44
|
+
"Record",
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Annotations that indicate a Spring-managed bean
|
|
49
|
+
_BEAN_ANNOTATIONS = frozenset(
|
|
50
|
+
{
|
|
51
|
+
"Component",
|
|
52
|
+
"Service",
|
|
53
|
+
"Repository",
|
|
54
|
+
"Controller",
|
|
55
|
+
"RestController",
|
|
56
|
+
"Configuration",
|
|
57
|
+
"Bean",
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Built-in Java types that are never in the symbol table
|
|
62
|
+
_JAVA_BUILTINS = frozenset(
|
|
63
|
+
{
|
|
64
|
+
"String",
|
|
65
|
+
"Integer",
|
|
66
|
+
"Long",
|
|
67
|
+
"Double",
|
|
68
|
+
"Float",
|
|
69
|
+
"Boolean",
|
|
70
|
+
"Byte",
|
|
71
|
+
"Short",
|
|
72
|
+
"Character",
|
|
73
|
+
"Object",
|
|
74
|
+
"Number",
|
|
75
|
+
"Comparable",
|
|
76
|
+
"Serializable",
|
|
77
|
+
"Iterable",
|
|
78
|
+
"Collection",
|
|
79
|
+
"List",
|
|
80
|
+
"Set",
|
|
81
|
+
"Map",
|
|
82
|
+
"Queue",
|
|
83
|
+
"Deque",
|
|
84
|
+
"ArrayList",
|
|
85
|
+
"LinkedList",
|
|
86
|
+
"HashMap",
|
|
87
|
+
"HashSet",
|
|
88
|
+
"TreeMap",
|
|
89
|
+
"TreeSet",
|
|
90
|
+
"Optional",
|
|
91
|
+
"void",
|
|
92
|
+
"int",
|
|
93
|
+
"long",
|
|
94
|
+
"double",
|
|
95
|
+
"float",
|
|
96
|
+
"boolean",
|
|
97
|
+
"byte",
|
|
98
|
+
"short",
|
|
99
|
+
"char",
|
|
100
|
+
"Enum",
|
|
101
|
+
"Exception",
|
|
102
|
+
"RuntimeException",
|
|
103
|
+
"Throwable",
|
|
104
|
+
"Error",
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _strip_generics(name: str) -> str:
|
|
110
|
+
"""Strip generic type parameters from a Java type name.
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
"List<User>" → "User" (unwrap single-param container)
|
|
114
|
+
"Map<String, User>" → "Map" (ambiguous — return outer type)
|
|
115
|
+
"ResponseEntity<Dto>" → "Dto" (unwrap single-param wrapper)
|
|
116
|
+
"CreateUserRequest" → "CreateUserRequest"
|
|
117
|
+
"""
|
|
118
|
+
name = name.strip()
|
|
119
|
+
if "<" not in name:
|
|
120
|
+
return name
|
|
121
|
+
|
|
122
|
+
# Extract the outer type and the raw inner type (preserve inner generics for recursion)
|
|
123
|
+
outer = name[: name.index("<")].strip()
|
|
124
|
+
inner_raw = name[name.index("<") + 1 : name.rindex(">")].strip()
|
|
125
|
+
|
|
126
|
+
# Common single-param containers/wrappers — prefer the inner type
|
|
127
|
+
_WRAPPER_TYPES = frozenset(
|
|
128
|
+
{
|
|
129
|
+
"Optional",
|
|
130
|
+
"List",
|
|
131
|
+
"Set",
|
|
132
|
+
"Collection",
|
|
133
|
+
"Iterable",
|
|
134
|
+
"ResponseEntity",
|
|
135
|
+
"HttpEntity",
|
|
136
|
+
"CompletableFuture",
|
|
137
|
+
"Future",
|
|
138
|
+
"Mono",
|
|
139
|
+
"Flux", # Project Reactor
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
if outer in _WRAPPER_TYPES and "," not in inner_raw and inner_raw:
|
|
143
|
+
# Recurse so ResponseEntity<List<User>> → List<User> → User
|
|
144
|
+
return _strip_generics(inner_raw)
|
|
145
|
+
|
|
146
|
+
# Multi-param or unknown — return outer (Map<K,V> → Map)
|
|
147
|
+
return outer
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class JavaTypeResolver:
|
|
151
|
+
"""
|
|
152
|
+
Cross-file type resolver for Java projects.
|
|
153
|
+
|
|
154
|
+
Implements the TypeResolver protocol.
|
|
155
|
+
|
|
156
|
+
Resolution order:
|
|
157
|
+
1. Direct qualified-name lookup in symbol table
|
|
158
|
+
2. Generic stripping + retry (List<User> → User)
|
|
159
|
+
3. File-local explicit import map
|
|
160
|
+
4. Wildcard package scan (import com.example.*)
|
|
161
|
+
5. Same-package implicit visibility (no import needed)
|
|
162
|
+
6. Project-wide simple-name index (unambiguous)
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def __init__(self, parsed_files: list[ParsedFile]) -> None:
|
|
166
|
+
# symbol_table: qualified_name → ParsedClass
|
|
167
|
+
self._symbol_table: dict[str, ParsedClass] = {}
|
|
168
|
+
# simple_name → list of qualified names (for ambiguous resolution)
|
|
169
|
+
self._simple_name_index: dict[str, list[str]] = {}
|
|
170
|
+
# per-file import map: file_path → { simple_name → qualified_name }
|
|
171
|
+
self._file_imports: dict[Path, dict[str, str]] = {}
|
|
172
|
+
# per-file wildcard packages: file_path → [ "com.example.dto" ]
|
|
173
|
+
self._file_wildcards: dict[Path, list[str]] = {}
|
|
174
|
+
# per-file package: file_path → "com.example.controller"
|
|
175
|
+
self._file_package: dict[Path, str] = {}
|
|
176
|
+
# package → list of qualified names of classes in that package
|
|
177
|
+
self._package_index: dict[str, list[str]] = {}
|
|
178
|
+
# ancestor cache: avoids repeated O(n) scans per class
|
|
179
|
+
self._ancestor_cache: dict[str, list[str]] = {}
|
|
180
|
+
|
|
181
|
+
self._build(parsed_files)
|
|
182
|
+
|
|
183
|
+
# -------------------------------------------------------------------------
|
|
184
|
+
# Build
|
|
185
|
+
# -------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
def _build(self, parsed_files: list[ParsedFile]) -> None:
|
|
188
|
+
# Phase 1: build symbol table + package index
|
|
189
|
+
for pf in parsed_files:
|
|
190
|
+
if not pf.success:
|
|
191
|
+
continue
|
|
192
|
+
for cls in pf.classes:
|
|
193
|
+
qname = cls.qualified_name.full
|
|
194
|
+
self._symbol_table[qname] = cls
|
|
195
|
+
simple = cls.name
|
|
196
|
+
self._simple_name_index.setdefault(simple, []).append(qname)
|
|
197
|
+
|
|
198
|
+
# Track package membership for same-package resolution
|
|
199
|
+
if "." in qname:
|
|
200
|
+
pkg = qname.rsplit(".", 1)[0]
|
|
201
|
+
self._package_index.setdefault(pkg, []).append(qname)
|
|
202
|
+
|
|
203
|
+
# Phase 2: build per-file import maps + package names
|
|
204
|
+
for pf in parsed_files:
|
|
205
|
+
if not pf.success:
|
|
206
|
+
continue
|
|
207
|
+
import_map: dict[str, str] = {}
|
|
208
|
+
wildcards: list[str] = []
|
|
209
|
+
pkg = ""
|
|
210
|
+
for imp in pf.imports:
|
|
211
|
+
if imp.names:
|
|
212
|
+
# "import com.example.User" → { "User": "com.example.User" }
|
|
213
|
+
for name in imp.names:
|
|
214
|
+
qn = f"{imp.module}.{name}" if imp.module else name
|
|
215
|
+
import_map[name] = qn
|
|
216
|
+
else:
|
|
217
|
+
# Wildcard: "import com.example.*"
|
|
218
|
+
if imp.module:
|
|
219
|
+
wildcards.append(imp.module)
|
|
220
|
+
# Infer package from first class's qualified name
|
|
221
|
+
for cls in pf.classes:
|
|
222
|
+
qn = cls.qualified_name.full
|
|
223
|
+
if "." in qn:
|
|
224
|
+
pkg = qn.rsplit(".", 1)[0]
|
|
225
|
+
break
|
|
226
|
+
self._file_imports[pf.path] = import_map
|
|
227
|
+
self._file_wildcards[pf.path] = wildcards
|
|
228
|
+
self._file_package[pf.path] = pkg
|
|
229
|
+
|
|
230
|
+
# -------------------------------------------------------------------------
|
|
231
|
+
# TypeResolver protocol
|
|
232
|
+
# -------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
def resolve_type(
|
|
235
|
+
self,
|
|
236
|
+
name: str,
|
|
237
|
+
in_file: Path | None = None,
|
|
238
|
+
) -> ResolvedType | None:
|
|
239
|
+
"""Resolve a type name to its definition."""
|
|
240
|
+
if not name or name in _JAVA_BUILTINS:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
# Strip array suffix ("User[]" → "User")
|
|
244
|
+
name = name.rstrip("[]").strip()
|
|
245
|
+
|
|
246
|
+
# 1. Direct qualified name lookup
|
|
247
|
+
if name in self._symbol_table:
|
|
248
|
+
return self._to_resolved_type(self._symbol_table[name])
|
|
249
|
+
|
|
250
|
+
# 2. Strip generics and retry ("List<User>" → "User", then resolve "User")
|
|
251
|
+
stripped = _strip_generics(name)
|
|
252
|
+
if stripped != name:
|
|
253
|
+
return self.resolve_type(stripped, in_file)
|
|
254
|
+
|
|
255
|
+
# 3. File-local import resolution
|
|
256
|
+
if in_file is not None:
|
|
257
|
+
import_map = self._file_imports.get(in_file, {})
|
|
258
|
+
qn = import_map.get(name)
|
|
259
|
+
if qn and qn in self._symbol_table:
|
|
260
|
+
return self._to_resolved_type(self._symbol_table[qn])
|
|
261
|
+
|
|
262
|
+
# 4. Wildcard package scan ("import com.example.*")
|
|
263
|
+
for pkg in self._file_wildcards.get(in_file, []):
|
|
264
|
+
candidate = f"{pkg}.{name}"
|
|
265
|
+
if candidate in self._symbol_table:
|
|
266
|
+
return self._to_resolved_type(self._symbol_table[candidate])
|
|
267
|
+
|
|
268
|
+
# 5. Same-package implicit visibility (no import needed in Java)
|
|
269
|
+
file_pkg = self._file_package.get(in_file, "")
|
|
270
|
+
if file_pkg:
|
|
271
|
+
candidate = f"{file_pkg}.{name}"
|
|
272
|
+
if candidate in self._symbol_table:
|
|
273
|
+
return self._to_resolved_type(self._symbol_table[candidate])
|
|
274
|
+
|
|
275
|
+
# 6. Project-wide simple name index (unambiguous cross-project lookup)
|
|
276
|
+
candidates = self._simple_name_index.get(name, [])
|
|
277
|
+
if len(candidates) == 1:
|
|
278
|
+
return self._to_resolved_type(self._symbol_table[candidates[0]])
|
|
279
|
+
if len(candidates) > 1:
|
|
280
|
+
logger.debug("Ambiguous type '%s': %s", name, candidates)
|
|
281
|
+
return self._to_resolved_type(self._symbol_table[candidates[0]])
|
|
282
|
+
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
def is_model_type(
|
|
286
|
+
self,
|
|
287
|
+
name: str,
|
|
288
|
+
in_file: Path | None = None,
|
|
289
|
+
) -> bool:
|
|
290
|
+
"""Check if a type is a data model."""
|
|
291
|
+
resolved = self.resolve_type(name, in_file)
|
|
292
|
+
if resolved is None:
|
|
293
|
+
return False
|
|
294
|
+
return resolved.is_model
|
|
295
|
+
|
|
296
|
+
def get_model_fields(
|
|
297
|
+
self,
|
|
298
|
+
model_name: str,
|
|
299
|
+
in_file: Path | None = None,
|
|
300
|
+
include_inherited: bool = True,
|
|
301
|
+
) -> list[ResolvedField]:
|
|
302
|
+
"""Get all fields for a model, optionally including inherited."""
|
|
303
|
+
resolved = self.resolve_type(model_name, in_file)
|
|
304
|
+
if resolved is None:
|
|
305
|
+
return []
|
|
306
|
+
if resolved.definition is None:
|
|
307
|
+
return []
|
|
308
|
+
|
|
309
|
+
fields = list(resolved.fields)
|
|
310
|
+
|
|
311
|
+
if include_inherited:
|
|
312
|
+
for base_name in resolved.base_classes:
|
|
313
|
+
base = self.resolve_type(base_name, in_file)
|
|
314
|
+
if base and base.fields:
|
|
315
|
+
for f in base.fields:
|
|
316
|
+
if not any(ef.name == f.name for ef in fields):
|
|
317
|
+
fields.append(f)
|
|
318
|
+
|
|
319
|
+
return fields
|
|
320
|
+
|
|
321
|
+
def is_subclass_of(
|
|
322
|
+
self,
|
|
323
|
+
class_name: str,
|
|
324
|
+
base_name: str,
|
|
325
|
+
in_file: Path | None = None,
|
|
326
|
+
) -> bool:
|
|
327
|
+
"""Check if class_name inherits from base_name."""
|
|
328
|
+
resolved = self.resolve_type(class_name, in_file)
|
|
329
|
+
if resolved is None:
|
|
330
|
+
return False
|
|
331
|
+
if base_name in resolved.base_classes:
|
|
332
|
+
return True
|
|
333
|
+
return base_name in resolved.all_ancestors
|
|
334
|
+
|
|
335
|
+
def get_all_models(self) -> dict[str, ResolvedType]:
|
|
336
|
+
"""Return all detected model types."""
|
|
337
|
+
result: dict[str, ResolvedType] = {}
|
|
338
|
+
for qname, cls in self._symbol_table.items():
|
|
339
|
+
rt = self._to_resolved_type(cls)
|
|
340
|
+
if rt.is_model:
|
|
341
|
+
result[qname] = rt
|
|
342
|
+
return result
|
|
343
|
+
|
|
344
|
+
# -------------------------------------------------------------------------
|
|
345
|
+
# Helpers
|
|
346
|
+
# -------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
def _to_resolved_type(self, cls: ParsedClass) -> ResolvedType:
|
|
349
|
+
"""Convert a ParsedClass to a ResolvedType."""
|
|
350
|
+
qname = cls.qualified_name.full
|
|
351
|
+
annotation_names = {d.name for d in cls.decorators}
|
|
352
|
+
|
|
353
|
+
is_model = bool(annotation_names & _MODEL_ANNOTATIONS) or self._has_model_fields(cls)
|
|
354
|
+
is_enum = cls.is_enum
|
|
355
|
+
|
|
356
|
+
all_ancestors = self._collect_ancestors(cls.base_classes)
|
|
357
|
+
|
|
358
|
+
resolved_fields: list[ResolvedField] = [
|
|
359
|
+
ResolvedField(
|
|
360
|
+
name=f.name,
|
|
361
|
+
type_annotation=f.type_annotation,
|
|
362
|
+
default_value=f.default_value,
|
|
363
|
+
is_required=f.default_value is None,
|
|
364
|
+
defined_in_class=qname,
|
|
365
|
+
)
|
|
366
|
+
for f in cls.fields
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
return ResolvedType(
|
|
370
|
+
name=cls.name,
|
|
371
|
+
qualified_name=qname,
|
|
372
|
+
file_path=cls.location.file if cls.location else None,
|
|
373
|
+
is_class=True,
|
|
374
|
+
is_model=is_model,
|
|
375
|
+
is_enum=is_enum,
|
|
376
|
+
base_classes=list(cls.base_classes),
|
|
377
|
+
all_ancestors=all_ancestors,
|
|
378
|
+
fields=resolved_fields,
|
|
379
|
+
definition=cls,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
def _has_model_fields(self, cls: ParsedClass) -> bool:
|
|
383
|
+
"""Heuristic: classes with multiple typed fields are likely models."""
|
|
384
|
+
return len(cls.fields) >= 2
|
|
385
|
+
|
|
386
|
+
def _collect_ancestors(self, base_names: list[str]) -> list[str]:
|
|
387
|
+
"""Recursively collect all ancestor class names.
|
|
388
|
+
|
|
389
|
+
Uses a cache and direct dict lookup (O(1) per step) rather than
|
|
390
|
+
the original O(n) linear scan of the symbol table per ancestor.
|
|
391
|
+
"""
|
|
392
|
+
cache_key = ",".join(sorted(base_names))
|
|
393
|
+
if cache_key in self._ancestor_cache:
|
|
394
|
+
return list(self._ancestor_cache[cache_key])
|
|
395
|
+
|
|
396
|
+
ancestors: list[str] = []
|
|
397
|
+
seen: set[str] = set()
|
|
398
|
+
queue = list(base_names)
|
|
399
|
+
|
|
400
|
+
# Build a reverse index: simple_name → qualified_name for fast lookup
|
|
401
|
+
# (reuse _simple_name_index which is already built)
|
|
402
|
+
while queue:
|
|
403
|
+
name = queue.pop(0)
|
|
404
|
+
if name in seen:
|
|
405
|
+
continue
|
|
406
|
+
seen.add(name)
|
|
407
|
+
ancestors.append(name)
|
|
408
|
+
|
|
409
|
+
# Fast lookup: try direct qname first, then simple name index
|
|
410
|
+
cls = self._symbol_table.get(name)
|
|
411
|
+
if cls is None:
|
|
412
|
+
candidates = self._simple_name_index.get(name, [])
|
|
413
|
+
if candidates:
|
|
414
|
+
cls = self._symbol_table.get(candidates[0])
|
|
415
|
+
|
|
416
|
+
if cls is not None:
|
|
417
|
+
for base in cls.base_classes:
|
|
418
|
+
if base not in seen:
|
|
419
|
+
queue.append(base)
|
|
420
|
+
|
|
421
|
+
self._ancestor_cache[cache_key] = ancestors
|
|
422
|
+
return ancestors
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Python parsing using LibCST."""
|
|
2
|
+
|
|
3
|
+
from .cbv_extractor import (
|
|
4
|
+
CBVClass,
|
|
5
|
+
CBVExtractor,
|
|
6
|
+
CBVRoute,
|
|
7
|
+
extract_cbv_routes,
|
|
8
|
+
)
|
|
9
|
+
from .constant_resolver import (
|
|
10
|
+
ConfigClass,
|
|
11
|
+
ConstantResolver,
|
|
12
|
+
ResolvedConstant,
|
|
13
|
+
build_constant_resolver,
|
|
14
|
+
)
|
|
15
|
+
from .cross_file_resolver import (
|
|
16
|
+
CrossFileResolver,
|
|
17
|
+
ImportGraph,
|
|
18
|
+
ResolvedImport,
|
|
19
|
+
ResolvedSymbol,
|
|
20
|
+
build_cross_file_resolver,
|
|
21
|
+
detect_src_layout,
|
|
22
|
+
infer_module_path,
|
|
23
|
+
)
|
|
24
|
+
from .dynamic_route_detector import (
|
|
25
|
+
DynamicRoute,
|
|
26
|
+
DynamicRouteDetector,
|
|
27
|
+
RouteFactory,
|
|
28
|
+
detect_dynamic_routes,
|
|
29
|
+
)
|
|
30
|
+
from .language_services import (
|
|
31
|
+
PythonConstantResolverAdapter,
|
|
32
|
+
PythonLanguageServices,
|
|
33
|
+
PythonPathResolverAdapter,
|
|
34
|
+
PythonRouterRegistryAdapter,
|
|
35
|
+
PythonTypeResolverAdapter,
|
|
36
|
+
get_python_language_services,
|
|
37
|
+
)
|
|
38
|
+
from .parameter_analyzer import (
|
|
39
|
+
AnalyzedParameter,
|
|
40
|
+
DefaultValueAnalyzer,
|
|
41
|
+
ParameterAnalyzer,
|
|
42
|
+
TypeAnnotationAnalyzer,
|
|
43
|
+
extract_path_params,
|
|
44
|
+
)
|
|
45
|
+
from .parser import PythonParser, PythonProjectParser, get_python_parser
|
|
46
|
+
from .path_resolver import (
|
|
47
|
+
PathResolver,
|
|
48
|
+
ResolvedPath,
|
|
49
|
+
TrackedVariable,
|
|
50
|
+
resolve_route_path,
|
|
51
|
+
)
|
|
52
|
+
from .router_registry import (
|
|
53
|
+
RouterDefinition,
|
|
54
|
+
RouterInclusion,
|
|
55
|
+
RouterRegistry,
|
|
56
|
+
RouterTree,
|
|
57
|
+
build_router_registry,
|
|
58
|
+
)
|
|
59
|
+
from .type_resolver import (
|
|
60
|
+
ResolvedField,
|
|
61
|
+
ResolvedModel,
|
|
62
|
+
ResolvedType,
|
|
63
|
+
SchemaBuilder,
|
|
64
|
+
TypeAnnotationParser,
|
|
65
|
+
TypeResolver,
|
|
66
|
+
)
|
|
67
|
+
from .visitors import (
|
|
68
|
+
ExtractedArgument,
|
|
69
|
+
ExtractedAssignment,
|
|
70
|
+
ExtractedCall,
|
|
71
|
+
ExtractedClass,
|
|
72
|
+
ExtractedDecorator,
|
|
73
|
+
ExtractedField,
|
|
74
|
+
ExtractedFunction,
|
|
75
|
+
ExtractedImport,
|
|
76
|
+
ExtractedParameter,
|
|
77
|
+
PythonExtractor,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
# Parser
|
|
82
|
+
"PythonParser",
|
|
83
|
+
"PythonProjectParser",
|
|
84
|
+
"get_python_parser",
|
|
85
|
+
# Visitor
|
|
86
|
+
"PythonExtractor",
|
|
87
|
+
"ExtractedFunction",
|
|
88
|
+
"ExtractedClass",
|
|
89
|
+
"ExtractedImport",
|
|
90
|
+
"ExtractedCall",
|
|
91
|
+
"ExtractedArgument",
|
|
92
|
+
"ExtractedAssignment",
|
|
93
|
+
"ExtractedDecorator",
|
|
94
|
+
"ExtractedParameter",
|
|
95
|
+
"ExtractedField",
|
|
96
|
+
# Type Resolution
|
|
97
|
+
"TypeResolver",
|
|
98
|
+
"TypeAnnotationParser",
|
|
99
|
+
"SchemaBuilder",
|
|
100
|
+
"ResolvedType",
|
|
101
|
+
"ResolvedField",
|
|
102
|
+
"ResolvedModel",
|
|
103
|
+
# Cross-file Resolution
|
|
104
|
+
"CrossFileResolver",
|
|
105
|
+
"build_cross_file_resolver",
|
|
106
|
+
"ResolvedSymbol",
|
|
107
|
+
"ResolvedImport",
|
|
108
|
+
"ImportGraph",
|
|
109
|
+
"infer_module_path",
|
|
110
|
+
"detect_src_layout",
|
|
111
|
+
# Parameter Analysis
|
|
112
|
+
"ParameterAnalyzer",
|
|
113
|
+
"AnalyzedParameter",
|
|
114
|
+
"extract_path_params",
|
|
115
|
+
"DefaultValueAnalyzer",
|
|
116
|
+
"TypeAnnotationAnalyzer",
|
|
117
|
+
# Router Registry
|
|
118
|
+
"RouterRegistry",
|
|
119
|
+
"RouterDefinition",
|
|
120
|
+
"RouterInclusion",
|
|
121
|
+
"RouterTree",
|
|
122
|
+
"build_router_registry",
|
|
123
|
+
# Path Resolution
|
|
124
|
+
"PathResolver",
|
|
125
|
+
"ResolvedPath",
|
|
126
|
+
"TrackedVariable",
|
|
127
|
+
"resolve_route_path",
|
|
128
|
+
# Dynamic Route Detection
|
|
129
|
+
"DynamicRouteDetector",
|
|
130
|
+
"DynamicRoute",
|
|
131
|
+
"RouteFactory",
|
|
132
|
+
"detect_dynamic_routes",
|
|
133
|
+
# Class-Based Views
|
|
134
|
+
"CBVExtractor",
|
|
135
|
+
"CBVClass",
|
|
136
|
+
"CBVRoute",
|
|
137
|
+
"extract_cbv_routes",
|
|
138
|
+
# Constant Resolution
|
|
139
|
+
"ConstantResolver",
|
|
140
|
+
"ResolvedConstant",
|
|
141
|
+
"ConfigClass",
|
|
142
|
+
"build_constant_resolver",
|
|
143
|
+
# Language Services
|
|
144
|
+
"PythonLanguageServices",
|
|
145
|
+
"PythonTypeResolverAdapter",
|
|
146
|
+
"PythonConstantResolverAdapter",
|
|
147
|
+
"PythonPathResolverAdapter",
|
|
148
|
+
"PythonRouterRegistryAdapter",
|
|
149
|
+
"get_python_language_services",
|
|
150
|
+
]
|