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,487 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python-specific implementation of LanguageServices.
|
|
3
|
+
|
|
4
|
+
This module provides the Python implementations of the abstract service
|
|
5
|
+
protocols defined in parsing/services.py. It adapts the existing Python
|
|
6
|
+
parsing infrastructure to the language-agnostic interfaces.
|
|
7
|
+
|
|
8
|
+
DESIGN:
|
|
9
|
+
- Implements LanguageServices for Python
|
|
10
|
+
- Wraps existing components (CrossFileResolver, etc.) in protocol adapters
|
|
11
|
+
- Provides framework-specific services (FastAPI router registry, etc.)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
from ..services import (
|
|
20
|
+
AnalysisContext,
|
|
21
|
+
LanguageServices,
|
|
22
|
+
ResolvedConstant,
|
|
23
|
+
ResolvedField,
|
|
24
|
+
ResolvedPath,
|
|
25
|
+
ResolvedType,
|
|
26
|
+
RouterInfo,
|
|
27
|
+
TypeResolver,
|
|
28
|
+
)
|
|
29
|
+
from ..services import (
|
|
30
|
+
ConstantResolver as ConstantResolverProtocol,
|
|
31
|
+
)
|
|
32
|
+
from ..services import (
|
|
33
|
+
PathResolver as PathResolverProtocol,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from ..base import ParsedFile
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# =============================================================================
|
|
41
|
+
# Protocol Adapters
|
|
42
|
+
# =============================================================================
|
|
43
|
+
|
|
44
|
+
# These adapters wrap existing Python components to implement the abstract protocols
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PythonTypeResolverAdapter:
|
|
48
|
+
"""
|
|
49
|
+
Adapter that wraps CrossFileResolver to implement TypeResolver protocol.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, cross_file_resolver):
|
|
53
|
+
"""
|
|
54
|
+
Args:
|
|
55
|
+
cross_file_resolver: Python CrossFileResolver instance
|
|
56
|
+
"""
|
|
57
|
+
self._resolver = cross_file_resolver
|
|
58
|
+
|
|
59
|
+
def resolve_type(
|
|
60
|
+
self,
|
|
61
|
+
name: str,
|
|
62
|
+
in_file: Path | None = None,
|
|
63
|
+
) -> ResolvedType | None:
|
|
64
|
+
"""Resolve a type reference."""
|
|
65
|
+
symbol = self._resolver.resolve_symbol(name, in_file)
|
|
66
|
+
if not symbol:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
return ResolvedType(
|
|
70
|
+
name=symbol.name,
|
|
71
|
+
qualified_name=symbol.qualified_name,
|
|
72
|
+
file_path=symbol.file_path,
|
|
73
|
+
is_class=symbol.is_class,
|
|
74
|
+
is_model=symbol.is_pydantic_model,
|
|
75
|
+
is_enum=symbol.is_enum if hasattr(symbol, "is_enum") else False,
|
|
76
|
+
is_external=symbol.is_external,
|
|
77
|
+
external_package=symbol.external_package,
|
|
78
|
+
base_classes=list(symbol.bases),
|
|
79
|
+
all_ancestors=list(symbol.all_bases) if hasattr(symbol, "all_bases") else [],
|
|
80
|
+
mro=list(symbol.mro) if hasattr(symbol, "mro") else [],
|
|
81
|
+
definition=symbol.definition,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def is_model_type(
|
|
85
|
+
self,
|
|
86
|
+
name: str,
|
|
87
|
+
in_file: Path | None = None,
|
|
88
|
+
) -> bool:
|
|
89
|
+
"""Check if type is a Pydantic model."""
|
|
90
|
+
return self._resolver.is_pydantic_model(name, in_file)
|
|
91
|
+
|
|
92
|
+
def get_model_fields(
|
|
93
|
+
self,
|
|
94
|
+
model_name: str,
|
|
95
|
+
in_file: Path | None = None,
|
|
96
|
+
include_inherited: bool = True,
|
|
97
|
+
) -> list[ResolvedField]:
|
|
98
|
+
"""Get fields for a model."""
|
|
99
|
+
raw_fields = self._resolver.get_model_fields(model_name, in_file, include_inherited)
|
|
100
|
+
|
|
101
|
+
return [
|
|
102
|
+
ResolvedField(
|
|
103
|
+
name=f.get("name", ""),
|
|
104
|
+
type_annotation=f.get("type_annotation"),
|
|
105
|
+
default_value=f.get("default_value"),
|
|
106
|
+
is_required=f.get("is_required", True),
|
|
107
|
+
nested_type_name=f.get("nested_model_name"),
|
|
108
|
+
alias=f.get("alias"),
|
|
109
|
+
description=f.get("description"),
|
|
110
|
+
constraints=f.get("constraints", {}),
|
|
111
|
+
defined_in_class=f.get("defined_in"),
|
|
112
|
+
)
|
|
113
|
+
for f in raw_fields
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
def is_subclass_of(
|
|
117
|
+
self,
|
|
118
|
+
class_name: str,
|
|
119
|
+
base_name: str,
|
|
120
|
+
in_file: Path | None = None,
|
|
121
|
+
) -> bool:
|
|
122
|
+
"""Check inheritance relationship."""
|
|
123
|
+
symbol = self._resolver.resolve_symbol(class_name, in_file)
|
|
124
|
+
if not symbol:
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
# Check direct bases and all ancestors
|
|
128
|
+
if base_name in symbol.bases:
|
|
129
|
+
return True
|
|
130
|
+
return bool(hasattr(symbol, "all_bases") and base_name in symbol.all_bases)
|
|
131
|
+
|
|
132
|
+
def get_all_models(self) -> dict[str, ResolvedType]:
|
|
133
|
+
"""Get all Pydantic models."""
|
|
134
|
+
raw_models = self._resolver.get_all_pydantic_models()
|
|
135
|
+
result = {}
|
|
136
|
+
|
|
137
|
+
for qname, symbol in raw_models.items():
|
|
138
|
+
# symbol may be ParsedClass (from get_all_pydantic_models) or ResolvedSymbol
|
|
139
|
+
file_path = getattr(symbol, "file_path", None) or (
|
|
140
|
+
symbol.location.file if hasattr(symbol, "location") and symbol.location else None
|
|
141
|
+
)
|
|
142
|
+
bases = getattr(symbol, "bases", None) or getattr(symbol, "base_classes", [])
|
|
143
|
+
definition = getattr(symbol, "definition", symbol) # ParsedClass is its own definition
|
|
144
|
+
result[qname] = ResolvedType(
|
|
145
|
+
name=symbol.name,
|
|
146
|
+
qualified_name=symbol.qualified_name.full
|
|
147
|
+
if hasattr(symbol.qualified_name, "full")
|
|
148
|
+
else str(symbol.qualified_name),
|
|
149
|
+
file_path=file_path,
|
|
150
|
+
is_class=True,
|
|
151
|
+
is_model=True,
|
|
152
|
+
base_classes=list(bases),
|
|
153
|
+
definition=definition,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class PythonConstantResolverAdapter:
|
|
160
|
+
"""
|
|
161
|
+
Adapter that wraps Python ConstantResolver to implement the protocol.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def __init__(self, constant_resolver):
|
|
165
|
+
"""
|
|
166
|
+
Args:
|
|
167
|
+
constant_resolver: Python ConstantResolver instance
|
|
168
|
+
"""
|
|
169
|
+
self._resolver = constant_resolver
|
|
170
|
+
|
|
171
|
+
def resolve(
|
|
172
|
+
self,
|
|
173
|
+
expression: str,
|
|
174
|
+
in_file: Path | None = None,
|
|
175
|
+
) -> ResolvedConstant | None:
|
|
176
|
+
"""Resolve a constant expression."""
|
|
177
|
+
result = self._resolver.resolve(expression, in_file)
|
|
178
|
+
if not result:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
return ResolvedConstant(
|
|
182
|
+
name=result.name,
|
|
183
|
+
value=result.value,
|
|
184
|
+
source_file=result.source_file,
|
|
185
|
+
source_line=result.source_line,
|
|
186
|
+
source_type=result.source_type,
|
|
187
|
+
is_default=result.is_default,
|
|
188
|
+
override_sources=result.override_sources,
|
|
189
|
+
confidence=result.confidence,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def resolve_value(
|
|
193
|
+
self,
|
|
194
|
+
expression: str,
|
|
195
|
+
in_file: Path | None = None,
|
|
196
|
+
) -> str | None:
|
|
197
|
+
"""Get just the value."""
|
|
198
|
+
return self._resolver.resolve_value(expression, in_file)
|
|
199
|
+
|
|
200
|
+
def get_all_constants(self) -> dict[str, str]:
|
|
201
|
+
"""Get all constants."""
|
|
202
|
+
return self._resolver.get_all_constants()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class PythonPathResolverAdapter:
|
|
206
|
+
"""
|
|
207
|
+
Adapter that wraps Python PathResolver to implement the protocol.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
def __init__(self, path_resolver):
|
|
211
|
+
"""
|
|
212
|
+
Args:
|
|
213
|
+
path_resolver: Python PathResolver instance
|
|
214
|
+
"""
|
|
215
|
+
self._resolver = path_resolver
|
|
216
|
+
|
|
217
|
+
def set_constant_resolver(self, resolver) -> None:
|
|
218
|
+
"""Set constant resolver for path variable resolution."""
|
|
219
|
+
# Unwrap if it's an adapter
|
|
220
|
+
if isinstance(resolver, PythonConstantResolverAdapter):
|
|
221
|
+
self._resolver.set_constant_resolver(resolver._resolver)
|
|
222
|
+
elif hasattr(self._resolver, "set_constant_resolver"):
|
|
223
|
+
self._resolver.set_constant_resolver(resolver)
|
|
224
|
+
|
|
225
|
+
def resolve(
|
|
226
|
+
self,
|
|
227
|
+
path_expression: str,
|
|
228
|
+
file_path: Path,
|
|
229
|
+
additional_context: dict[str, str] | None = None,
|
|
230
|
+
) -> ResolvedPath:
|
|
231
|
+
"""Resolve a path expression."""
|
|
232
|
+
result = self._resolver.resolve(path_expression, file_path, additional_context)
|
|
233
|
+
|
|
234
|
+
return ResolvedPath(
|
|
235
|
+
path=result.path,
|
|
236
|
+
confidence=result.resolution_confidence,
|
|
237
|
+
is_fully_resolved=len(result.unresolved_vars) == 0,
|
|
238
|
+
unresolved_vars=result.unresolved_vars,
|
|
239
|
+
notes=result.notes if hasattr(result, "notes") else [],
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def get_all_constants(self) -> dict[str, str]:
|
|
243
|
+
"""Get all path constants."""
|
|
244
|
+
return self._resolver.get_all_constants()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class PythonRouterRegistryAdapter:
|
|
248
|
+
"""
|
|
249
|
+
Adapter that wraps Python RouterRegistry to implement the protocol.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
def __init__(self, router_registry):
|
|
253
|
+
"""
|
|
254
|
+
Args:
|
|
255
|
+
router_registry: Python RouterRegistry instance (FastAPI-specific)
|
|
256
|
+
"""
|
|
257
|
+
self._registry = router_registry
|
|
258
|
+
|
|
259
|
+
def set_constant_resolver(self, resolver) -> None:
|
|
260
|
+
"""Set constant resolver for prefix resolution."""
|
|
261
|
+
if isinstance(resolver, PythonConstantResolverAdapter):
|
|
262
|
+
self._registry.set_constant_resolver(resolver._resolver)
|
|
263
|
+
elif hasattr(self._registry, "set_constant_resolver"):
|
|
264
|
+
self._registry.set_constant_resolver(resolver)
|
|
265
|
+
|
|
266
|
+
def resolve_path(
|
|
267
|
+
self,
|
|
268
|
+
router_var: str,
|
|
269
|
+
route_path: str,
|
|
270
|
+
file_path: Path,
|
|
271
|
+
) -> str:
|
|
272
|
+
"""Resolve full path with prefixes."""
|
|
273
|
+
return self._registry.resolve_path(router_var, route_path, file_path)
|
|
274
|
+
|
|
275
|
+
def get_router(self, name: str, file_path: Path) -> RouterInfo | None:
|
|
276
|
+
"""Get router info."""
|
|
277
|
+
routers = self._registry.get_all_routers()
|
|
278
|
+
|
|
279
|
+
# Try qualified lookup
|
|
280
|
+
for qname, router in routers.items():
|
|
281
|
+
if router.name == name and router.file_path == file_path:
|
|
282
|
+
return RouterInfo(
|
|
283
|
+
name=router.name,
|
|
284
|
+
qualified_name=qname,
|
|
285
|
+
file_path=router.file_path,
|
|
286
|
+
router_type=router.router_type,
|
|
287
|
+
prefix=router.prefix,
|
|
288
|
+
tags=router.tags,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
def get_all_routers(self) -> dict[str, RouterInfo]:
|
|
294
|
+
"""Get all routers."""
|
|
295
|
+
result = {}
|
|
296
|
+
for qname, router in self._registry.get_all_routers().items():
|
|
297
|
+
result[qname] = RouterInfo(
|
|
298
|
+
name=router.name,
|
|
299
|
+
qualified_name=qname,
|
|
300
|
+
file_path=router.file_path,
|
|
301
|
+
router_type=router.router_type,
|
|
302
|
+
prefix=router.prefix,
|
|
303
|
+
tags=router.tags,
|
|
304
|
+
)
|
|
305
|
+
return result
|
|
306
|
+
|
|
307
|
+
def get_router_dependencies(
|
|
308
|
+
self,
|
|
309
|
+
router_var: str,
|
|
310
|
+
file_path: Path,
|
|
311
|
+
) -> list[str]:
|
|
312
|
+
"""Return accumulated dependency names for a router from the tree."""
|
|
313
|
+
return self._registry.get_router_dependencies(router_var, file_path)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# =============================================================================
|
|
317
|
+
# Python Language Services
|
|
318
|
+
# =============================================================================
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class PythonLanguageServices(LanguageServices):
|
|
322
|
+
"""
|
|
323
|
+
Python-specific implementation of LanguageServices.
|
|
324
|
+
|
|
325
|
+
This factory builds all Python analysis services and wraps them
|
|
326
|
+
in protocol-compliant adapters.
|
|
327
|
+
|
|
328
|
+
Usage:
|
|
329
|
+
services = PythonLanguageServices()
|
|
330
|
+
context = services.build_context(
|
|
331
|
+
parsed_files,
|
|
332
|
+
project_root,
|
|
333
|
+
framework="fastapi"
|
|
334
|
+
)
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
def build_type_resolver(
|
|
338
|
+
self,
|
|
339
|
+
parsed_files: list[ParsedFile],
|
|
340
|
+
project_root: Path | None = None,
|
|
341
|
+
) -> TypeResolver:
|
|
342
|
+
"""Build Python type resolver (CrossFileResolver)."""
|
|
343
|
+
from .cross_file_resolver import build_cross_file_resolver
|
|
344
|
+
|
|
345
|
+
resolver = build_cross_file_resolver(parsed_files, project_root)
|
|
346
|
+
return PythonTypeResolverAdapter(resolver)
|
|
347
|
+
|
|
348
|
+
def build_constant_resolver(
|
|
349
|
+
self,
|
|
350
|
+
parsed_files: list[ParsedFile],
|
|
351
|
+
project_root: Path | None = None,
|
|
352
|
+
) -> ConstantResolverProtocol | None:
|
|
353
|
+
"""Build Python constant resolver."""
|
|
354
|
+
from .constant_resolver import build_constant_resolver
|
|
355
|
+
|
|
356
|
+
resolver = build_constant_resolver(parsed_files, project_root)
|
|
357
|
+
return PythonConstantResolverAdapter(resolver)
|
|
358
|
+
|
|
359
|
+
def build_path_resolver(
|
|
360
|
+
self,
|
|
361
|
+
parsed_files: list[ParsedFile],
|
|
362
|
+
project_root: Path | None = None,
|
|
363
|
+
) -> PathResolverProtocol | None:
|
|
364
|
+
"""Build Python path resolver."""
|
|
365
|
+
from .path_resolver import PathResolver as PythonPathResolver
|
|
366
|
+
|
|
367
|
+
resolver = PythonPathResolver(project_root)
|
|
368
|
+
for parsed in parsed_files:
|
|
369
|
+
resolver.process_file(parsed)
|
|
370
|
+
|
|
371
|
+
return PythonPathResolverAdapter(resolver)
|
|
372
|
+
|
|
373
|
+
def build_framework_services(
|
|
374
|
+
self,
|
|
375
|
+
framework: str,
|
|
376
|
+
parsed_files: list[ParsedFile],
|
|
377
|
+
project_root: Path | None = None,
|
|
378
|
+
) -> dict[str, Any]:
|
|
379
|
+
"""Build framework-specific services."""
|
|
380
|
+
services = {}
|
|
381
|
+
|
|
382
|
+
if framework.lower() == "fastapi":
|
|
383
|
+
services.update(self._build_fastapi_services(parsed_files, project_root))
|
|
384
|
+
elif framework.lower() == "flask":
|
|
385
|
+
services.update(self._build_flask_services(parsed_files, project_root))
|
|
386
|
+
# Add more frameworks as needed
|
|
387
|
+
|
|
388
|
+
return services
|
|
389
|
+
|
|
390
|
+
def _build_fastapi_services(
|
|
391
|
+
self,
|
|
392
|
+
parsed_files: list[ParsedFile],
|
|
393
|
+
project_root: Path | None,
|
|
394
|
+
) -> dict[str, Any]:
|
|
395
|
+
"""Build FastAPI-specific services."""
|
|
396
|
+
from .cbv_extractor import CBVExtractor
|
|
397
|
+
from .dynamic_route_detector import DynamicRouteDetector
|
|
398
|
+
from .router_registry import build_router_registry
|
|
399
|
+
|
|
400
|
+
# Router registry
|
|
401
|
+
registry = build_router_registry(parsed_files, project_root)
|
|
402
|
+
|
|
403
|
+
# Dynamic route detector
|
|
404
|
+
dynamic_detector = DynamicRouteDetector(project_root)
|
|
405
|
+
for parsed in parsed_files:
|
|
406
|
+
dynamic_detector.process_file(parsed)
|
|
407
|
+
|
|
408
|
+
# CBV extractor
|
|
409
|
+
cbv_extractor = CBVExtractor(project_root)
|
|
410
|
+
for parsed in parsed_files:
|
|
411
|
+
cbv_extractor.process_file(parsed)
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
"router_registry": PythonRouterRegistryAdapter(registry),
|
|
415
|
+
"dynamic_route_detector": dynamic_detector,
|
|
416
|
+
"cbv_extractor": cbv_extractor,
|
|
417
|
+
# Keep raw registry for direct access if needed
|
|
418
|
+
"_raw_router_registry": registry,
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
def _build_flask_services(
|
|
422
|
+
self,
|
|
423
|
+
parsed_files: list[ParsedFile],
|
|
424
|
+
project_root: Path | None,
|
|
425
|
+
) -> dict[str, Any]:
|
|
426
|
+
"""Build Flask-specific services (placeholder)."""
|
|
427
|
+
# TODO: Implement Flask blueprint registry
|
|
428
|
+
return {}
|
|
429
|
+
|
|
430
|
+
# Override build_context to wire up Python-specific dependencies
|
|
431
|
+
def build_context(
|
|
432
|
+
self,
|
|
433
|
+
parsed_files: list[ParsedFile],
|
|
434
|
+
project_root: Path | None = None,
|
|
435
|
+
framework: str | None = None,
|
|
436
|
+
) -> AnalysisContext:
|
|
437
|
+
"""
|
|
438
|
+
Build complete analysis context for Python.
|
|
439
|
+
|
|
440
|
+
Wires up all services with proper dependencies.
|
|
441
|
+
"""
|
|
442
|
+
# Build base services
|
|
443
|
+
type_resolver = self.build_type_resolver(parsed_files, project_root)
|
|
444
|
+
constant_resolver = self.build_constant_resolver(parsed_files, project_root)
|
|
445
|
+
path_resolver = self.build_path_resolver(parsed_files, project_root)
|
|
446
|
+
|
|
447
|
+
# Wire constant resolver into path resolver
|
|
448
|
+
if constant_resolver and path_resolver:
|
|
449
|
+
path_resolver.set_constant_resolver(constant_resolver)
|
|
450
|
+
|
|
451
|
+
# Build framework-specific services
|
|
452
|
+
framework_services = {}
|
|
453
|
+
router_registry = None
|
|
454
|
+
|
|
455
|
+
if framework:
|
|
456
|
+
framework_services = self.build_framework_services(
|
|
457
|
+
framework, parsed_files, project_root
|
|
458
|
+
)
|
|
459
|
+
router_registry = framework_services.get("router_registry")
|
|
460
|
+
|
|
461
|
+
# Wire constant resolver into router registry
|
|
462
|
+
if router_registry and constant_resolver:
|
|
463
|
+
router_registry.set_constant_resolver(constant_resolver)
|
|
464
|
+
|
|
465
|
+
# Store raw resolver for legacy plugins that need direct access
|
|
466
|
+
if isinstance(type_resolver, PythonTypeResolverAdapter):
|
|
467
|
+
framework_services["_raw_cross_file_resolver"] = type_resolver._resolver
|
|
468
|
+
|
|
469
|
+
return AnalysisContext(
|
|
470
|
+
type_resolver=type_resolver,
|
|
471
|
+
constant_resolver=constant_resolver,
|
|
472
|
+
path_resolver=path_resolver,
|
|
473
|
+
router_registry=router_registry,
|
|
474
|
+
project_root=project_root,
|
|
475
|
+
all_parsed_files=parsed_files,
|
|
476
|
+
language_services=framework_services,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
# =============================================================================
|
|
481
|
+
# Factory Function
|
|
482
|
+
# =============================================================================
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def get_python_language_services() -> PythonLanguageServices:
|
|
486
|
+
"""Get a PythonLanguageServices instance."""
|
|
487
|
+
return PythonLanguageServices()
|