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,544 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Abstract service protocols for language-agnostic analysis.
|
|
3
|
+
|
|
4
|
+
These protocols define the contracts that language-specific implementations
|
|
5
|
+
must fulfill. Framework plugins depend on these abstractions, NOT on
|
|
6
|
+
language-specific implementations directly.
|
|
7
|
+
|
|
8
|
+
DESIGN PRINCIPLES:
|
|
9
|
+
1. Framework plugins should ONLY depend on these protocols
|
|
10
|
+
2. Language-specific implementations live in parsing/{language}/
|
|
11
|
+
3. This enables adding new languages without changing framework plugins
|
|
12
|
+
4. DRY: Common concepts are defined once, implemented per-language
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
# Framework plugin uses protocol, not implementation
|
|
16
|
+
class FastAPIPlugin(BaseFrameworkPlugin):
|
|
17
|
+
def extract_routes(self, file: ParsedFile, ctx: AnalysisContext):
|
|
18
|
+
# Uses abstract TypeResolver, not Python-specific CrossFileResolver
|
|
19
|
+
model_fields = ctx.type_resolver.get_model_fields("UserRequest")
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from abc import ABC, abstractmethod
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from .base import ParsedClass, ParsedFile
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# =============================================================================
|
|
34
|
+
# Type Resolution Protocol
|
|
35
|
+
# =============================================================================
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ResolvedType:
|
|
40
|
+
"""
|
|
41
|
+
A resolved type reference.
|
|
42
|
+
|
|
43
|
+
This is the language-agnostic representation of a resolved type.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
name: str
|
|
47
|
+
qualified_name: str
|
|
48
|
+
file_path: Path | None = None
|
|
49
|
+
|
|
50
|
+
# Type classification
|
|
51
|
+
is_class: bool = False
|
|
52
|
+
is_model: bool = False # Pydantic, dataclass, DTO, etc.
|
|
53
|
+
is_enum: bool = False
|
|
54
|
+
is_external: bool = False # From external package
|
|
55
|
+
external_package: str | None = None
|
|
56
|
+
|
|
57
|
+
# Inheritance info
|
|
58
|
+
base_classes: list[str] = field(default_factory=list)
|
|
59
|
+
all_ancestors: list[str] = field(default_factory=list) # Full chain
|
|
60
|
+
mro: list[str] = field(default_factory=list) # Method Resolution Order
|
|
61
|
+
|
|
62
|
+
# For models: field information
|
|
63
|
+
fields: list[ResolvedField] = field(default_factory=list)
|
|
64
|
+
|
|
65
|
+
# The original parsed class, if available
|
|
66
|
+
definition: ParsedClass | None = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class ResolvedField:
|
|
71
|
+
"""A resolved field within a model/class."""
|
|
72
|
+
|
|
73
|
+
name: str
|
|
74
|
+
type_annotation: str | None = None
|
|
75
|
+
default_value: str | None = None
|
|
76
|
+
is_required: bool = True
|
|
77
|
+
|
|
78
|
+
# For nested types
|
|
79
|
+
nested_type_name: str | None = None
|
|
80
|
+
nested_type_qualified: str | None = None
|
|
81
|
+
|
|
82
|
+
# Field metadata
|
|
83
|
+
alias: str | None = None
|
|
84
|
+
description: str | None = None
|
|
85
|
+
constraints: dict[str, Any] = field(default_factory=dict)
|
|
86
|
+
|
|
87
|
+
# Source
|
|
88
|
+
defined_in_class: str | None = None # Which class in MRO defines this
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@runtime_checkable
|
|
92
|
+
class TypeResolver(Protocol):
|
|
93
|
+
"""
|
|
94
|
+
Protocol for cross-file type resolution.
|
|
95
|
+
|
|
96
|
+
Language implementations provide their own resolvers that fulfill
|
|
97
|
+
this contract. Framework plugins depend on this protocol, not on
|
|
98
|
+
language-specific implementations.
|
|
99
|
+
|
|
100
|
+
Implementations:
|
|
101
|
+
- Python: CrossFileResolver (uses import graph, MRO)
|
|
102
|
+
- Java: Would use package/import resolution
|
|
103
|
+
- TypeScript: Would use module resolution
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def resolve_type(
|
|
107
|
+
self,
|
|
108
|
+
name: str,
|
|
109
|
+
in_file: Path | None = None,
|
|
110
|
+
) -> ResolvedType | None:
|
|
111
|
+
"""
|
|
112
|
+
Resolve a type reference to its definition.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
name: Type name (simple or qualified)
|
|
116
|
+
in_file: File context for import resolution
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
ResolvedType if found, None otherwise
|
|
120
|
+
"""
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
def is_model_type(
|
|
124
|
+
self,
|
|
125
|
+
name: str,
|
|
126
|
+
in_file: Path | None = None,
|
|
127
|
+
) -> bool:
|
|
128
|
+
"""
|
|
129
|
+
Check if a type is a data model (Pydantic, dataclass, DTO, etc.).
|
|
130
|
+
|
|
131
|
+
This is framework-agnostic: each language defines what constitutes
|
|
132
|
+
a "model" (Python: Pydantic/dataclass, Java: POJO/DTO, etc.)
|
|
133
|
+
"""
|
|
134
|
+
...
|
|
135
|
+
|
|
136
|
+
def get_model_fields(
|
|
137
|
+
self,
|
|
138
|
+
model_name: str,
|
|
139
|
+
in_file: Path | None = None,
|
|
140
|
+
include_inherited: bool = True,
|
|
141
|
+
) -> list[ResolvedField]:
|
|
142
|
+
"""
|
|
143
|
+
Get fields for a model type, optionally including inherited fields.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
model_name: Name of the model class
|
|
147
|
+
in_file: File context
|
|
148
|
+
include_inherited: Whether to include fields from base classes
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
List of resolved fields
|
|
152
|
+
"""
|
|
153
|
+
...
|
|
154
|
+
|
|
155
|
+
def is_subclass_of(
|
|
156
|
+
self,
|
|
157
|
+
class_name: str,
|
|
158
|
+
base_name: str,
|
|
159
|
+
in_file: Path | None = None,
|
|
160
|
+
) -> bool:
|
|
161
|
+
"""
|
|
162
|
+
Check if a class inherits from another.
|
|
163
|
+
|
|
164
|
+
Uses the full inheritance chain, not just direct bases.
|
|
165
|
+
"""
|
|
166
|
+
...
|
|
167
|
+
|
|
168
|
+
def get_all_models(self) -> dict[str, ResolvedType]:
|
|
169
|
+
"""Get all detected model types in the project."""
|
|
170
|
+
...
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# =============================================================================
|
|
174
|
+
# Constant/Config Resolution Protocol
|
|
175
|
+
# =============================================================================
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass
|
|
179
|
+
class ResolvedConstant:
|
|
180
|
+
"""A resolved constant or configuration value."""
|
|
181
|
+
|
|
182
|
+
name: str
|
|
183
|
+
value: str
|
|
184
|
+
|
|
185
|
+
# Source info
|
|
186
|
+
source_file: Path | None = None
|
|
187
|
+
source_line: int = 0
|
|
188
|
+
source_type: str = "" # "literal", "env_default", "config_attr", etc.
|
|
189
|
+
|
|
190
|
+
# Override tracking
|
|
191
|
+
is_default: bool = True # True if can be overridden at runtime
|
|
192
|
+
override_sources: list[str] = field(default_factory=list) # e.g., ["ENV:API_PREFIX"]
|
|
193
|
+
|
|
194
|
+
# Confidence
|
|
195
|
+
confidence: float = 1.0
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@runtime_checkable
|
|
199
|
+
class ConstantResolver(Protocol):
|
|
200
|
+
"""
|
|
201
|
+
Protocol for resolving constants and configuration values.
|
|
202
|
+
|
|
203
|
+
Language implementations handle their specific patterns:
|
|
204
|
+
- Python: module constants, Pydantic Settings, os.getenv()
|
|
205
|
+
- Java: application.properties, @Value annotations
|
|
206
|
+
- TypeScript: .env, config objects
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
def resolve(
|
|
210
|
+
self,
|
|
211
|
+
expression: str,
|
|
212
|
+
in_file: Path | None = None,
|
|
213
|
+
) -> ResolvedConstant | None:
|
|
214
|
+
"""
|
|
215
|
+
Resolve a constant expression to its value.
|
|
216
|
+
|
|
217
|
+
Handles:
|
|
218
|
+
- Simple names: API_PREFIX
|
|
219
|
+
- Attribute access: config.PREFIX, settings.api.prefix
|
|
220
|
+
- Env var defaults: os.getenv("X", "default")
|
|
221
|
+
"""
|
|
222
|
+
...
|
|
223
|
+
|
|
224
|
+
def resolve_value(
|
|
225
|
+
self,
|
|
226
|
+
expression: str,
|
|
227
|
+
in_file: Path | None = None,
|
|
228
|
+
) -> str | None:
|
|
229
|
+
"""Convenience method to just get the value string."""
|
|
230
|
+
...
|
|
231
|
+
|
|
232
|
+
def get_all_constants(self) -> dict[str, str]:
|
|
233
|
+
"""Get all resolved constants as name -> value mapping."""
|
|
234
|
+
...
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# =============================================================================
|
|
238
|
+
# Path Resolution Protocol
|
|
239
|
+
# =============================================================================
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@dataclass
|
|
243
|
+
class ResolvedPath:
|
|
244
|
+
"""A resolved route path."""
|
|
245
|
+
|
|
246
|
+
path: str # The resolved path string
|
|
247
|
+
|
|
248
|
+
# Resolution quality
|
|
249
|
+
confidence: float = 1.0
|
|
250
|
+
is_fully_resolved: bool = True
|
|
251
|
+
unresolved_vars: list[str] = field(default_factory=list)
|
|
252
|
+
|
|
253
|
+
# Notes about resolution
|
|
254
|
+
notes: list[str] = field(default_factory=list)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@runtime_checkable
|
|
258
|
+
class PathResolver(Protocol):
|
|
259
|
+
"""
|
|
260
|
+
Protocol for resolving computed route paths.
|
|
261
|
+
|
|
262
|
+
Handles dynamic path construction:
|
|
263
|
+
- Python: f-strings, string concatenation
|
|
264
|
+
- Java: String.format(), concatenation
|
|
265
|
+
- TypeScript: template literals
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
def resolve(
|
|
269
|
+
self,
|
|
270
|
+
path_expression: str,
|
|
271
|
+
file_path: Path,
|
|
272
|
+
additional_context: dict[str, str] | None = None,
|
|
273
|
+
) -> ResolvedPath:
|
|
274
|
+
"""
|
|
275
|
+
Resolve a path expression to its final value.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
path_expression: The path expression (may contain variables)
|
|
279
|
+
file_path: Source file for context
|
|
280
|
+
additional_context: Additional variable bindings
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
ResolvedPath with the resolved string and confidence
|
|
284
|
+
"""
|
|
285
|
+
...
|
|
286
|
+
|
|
287
|
+
def get_all_constants(self) -> dict[str, str]:
|
|
288
|
+
"""Get all path-related constants."""
|
|
289
|
+
...
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# =============================================================================
|
|
293
|
+
# Router Registry Protocol (Web Frameworks)
|
|
294
|
+
# =============================================================================
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@dataclass
|
|
298
|
+
class RouterInfo:
|
|
299
|
+
"""Information about a router/controller definition."""
|
|
300
|
+
|
|
301
|
+
name: str # Variable name
|
|
302
|
+
qualified_name: str
|
|
303
|
+
file_path: Path
|
|
304
|
+
|
|
305
|
+
router_type: str # "app", "router", "controller", "blueprint"
|
|
306
|
+
prefix: str = ""
|
|
307
|
+
tags: list[str] = field(default_factory=list)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@runtime_checkable
|
|
311
|
+
class RouterRegistry(Protocol):
|
|
312
|
+
"""
|
|
313
|
+
Protocol for tracking router definitions and hierarchies.
|
|
314
|
+
|
|
315
|
+
Web frameworks often have router nesting:
|
|
316
|
+
- FastAPI: app.include_router(router, prefix="/api")
|
|
317
|
+
- Express: app.use("/api", router)
|
|
318
|
+
- Spring: @RequestMapping("/api") on controller
|
|
319
|
+
|
|
320
|
+
This protocol abstracts the concept of router hierarchies.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
def resolve_path(
|
|
324
|
+
self,
|
|
325
|
+
router_var: str,
|
|
326
|
+
route_path: str,
|
|
327
|
+
file_path: Path,
|
|
328
|
+
) -> str:
|
|
329
|
+
"""
|
|
330
|
+
Resolve full path including router prefixes.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
router_var: The router variable name
|
|
334
|
+
route_path: The route's local path
|
|
335
|
+
file_path: Source file
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Full path with all prefixes applied
|
|
339
|
+
"""
|
|
340
|
+
...
|
|
341
|
+
|
|
342
|
+
def get_router(self, name: str, file_path: Path) -> RouterInfo | None:
|
|
343
|
+
"""Get router info by variable name."""
|
|
344
|
+
...
|
|
345
|
+
|
|
346
|
+
def get_all_routers(self) -> dict[str, RouterInfo]:
|
|
347
|
+
"""Get all registered routers."""
|
|
348
|
+
...
|
|
349
|
+
|
|
350
|
+
def get_router_dependencies(
|
|
351
|
+
self,
|
|
352
|
+
router_var: str,
|
|
353
|
+
file_path: Path,
|
|
354
|
+
) -> list[str]:
|
|
355
|
+
"""Return accumulated dependency function names for a router.
|
|
356
|
+
|
|
357
|
+
These are the dependency names declared via ``dependencies=[Depends(...)]``
|
|
358
|
+
on ``include_router()`` calls in the ancestry chain plus constructor-level
|
|
359
|
+
dependencies on the routers themselves.
|
|
360
|
+
"""
|
|
361
|
+
...
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# =============================================================================
|
|
365
|
+
# Analysis Context
|
|
366
|
+
# =============================================================================
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@dataclass
|
|
370
|
+
class AnalysisContext:
|
|
371
|
+
"""
|
|
372
|
+
Context object passed to framework plugins during analysis.
|
|
373
|
+
|
|
374
|
+
This replaces the multiple setter methods on plugins with a single,
|
|
375
|
+
immutable context object. All services are accessed through this.
|
|
376
|
+
|
|
377
|
+
DESIGN:
|
|
378
|
+
- Plugins receive context, don't store mutable state
|
|
379
|
+
- Services are language-specific implementations of protocols
|
|
380
|
+
- Framework plugins only depend on protocols, not implementations
|
|
381
|
+
|
|
382
|
+
Usage:
|
|
383
|
+
def extract_routes(self, file: ParsedFile, ctx: AnalysisContext):
|
|
384
|
+
# Resolve a model type
|
|
385
|
+
fields = ctx.type_resolver.get_model_fields("UserRequest", file.path)
|
|
386
|
+
|
|
387
|
+
# Resolve a computed path
|
|
388
|
+
path = ctx.path_resolver.resolve(f_string_expr, file.path)
|
|
389
|
+
|
|
390
|
+
# Get full path with router prefix
|
|
391
|
+
full_path = ctx.router_registry.resolve_path("router", "/users", file.path)
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
# Required services (always available)
|
|
395
|
+
type_resolver: TypeResolver
|
|
396
|
+
|
|
397
|
+
# Optional services (may be None depending on language/framework)
|
|
398
|
+
constant_resolver: ConstantResolver | None = None
|
|
399
|
+
path_resolver: PathResolver | None = None
|
|
400
|
+
router_registry: RouterRegistry | None = None
|
|
401
|
+
|
|
402
|
+
# Project info
|
|
403
|
+
project_root: Path | None = None
|
|
404
|
+
all_parsed_files: list[ParsedFile] = field(default_factory=list)
|
|
405
|
+
|
|
406
|
+
# Language-specific services (escape hatch for edge cases)
|
|
407
|
+
# Access via: ctx.language_services.get("some_python_thing")
|
|
408
|
+
language_services: dict[str, Any] = field(default_factory=dict)
|
|
409
|
+
|
|
410
|
+
def get_service(self, name: str) -> Any | None:
|
|
411
|
+
"""Get a language-specific service by name."""
|
|
412
|
+
return self.language_services.get(name)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# =============================================================================
|
|
416
|
+
# Language Services Factory
|
|
417
|
+
# =============================================================================
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class LanguageServices(ABC):
|
|
421
|
+
"""
|
|
422
|
+
Abstract factory for language-specific analysis services.
|
|
423
|
+
|
|
424
|
+
Each language implements this to provide its own:
|
|
425
|
+
- Type resolver (import/inheritance resolution)
|
|
426
|
+
- Constant resolver (config/env var resolution)
|
|
427
|
+
- Path resolver (string interpolation)
|
|
428
|
+
- Any framework-specific services
|
|
429
|
+
|
|
430
|
+
DESIGN:
|
|
431
|
+
- One implementation per language (PythonLanguageServices, JavaLanguageServices)
|
|
432
|
+
- Built from parsed files during analysis
|
|
433
|
+
- Returns protocol implementations, not concrete classes
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
@abstractmethod
|
|
437
|
+
def build_type_resolver(
|
|
438
|
+
self,
|
|
439
|
+
parsed_files: list[ParsedFile],
|
|
440
|
+
project_root: Path | None = None,
|
|
441
|
+
) -> TypeResolver:
|
|
442
|
+
"""
|
|
443
|
+
Build the type resolver for this language.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
parsed_files: All successfully parsed files
|
|
447
|
+
project_root: Project root directory
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
TypeResolver implementation for this language
|
|
451
|
+
"""
|
|
452
|
+
...
|
|
453
|
+
|
|
454
|
+
@abstractmethod
|
|
455
|
+
def build_constant_resolver(
|
|
456
|
+
self,
|
|
457
|
+
parsed_files: list[ParsedFile],
|
|
458
|
+
project_root: Path | None = None,
|
|
459
|
+
) -> ConstantResolver | None:
|
|
460
|
+
"""
|
|
461
|
+
Build the constant resolver for this language.
|
|
462
|
+
|
|
463
|
+
Returns None if the language doesn't have meaningful constant resolution.
|
|
464
|
+
"""
|
|
465
|
+
...
|
|
466
|
+
|
|
467
|
+
@abstractmethod
|
|
468
|
+
def build_path_resolver(
|
|
469
|
+
self,
|
|
470
|
+
parsed_files: list[ParsedFile],
|
|
471
|
+
project_root: Path | None = None,
|
|
472
|
+
) -> PathResolver | None:
|
|
473
|
+
"""
|
|
474
|
+
Build the path resolver for this language.
|
|
475
|
+
|
|
476
|
+
Returns None if the language doesn't need path resolution.
|
|
477
|
+
"""
|
|
478
|
+
...
|
|
479
|
+
|
|
480
|
+
def build_framework_services(
|
|
481
|
+
self,
|
|
482
|
+
framework: str,
|
|
483
|
+
parsed_files: list[ParsedFile],
|
|
484
|
+
project_root: Path | None = None,
|
|
485
|
+
) -> dict[str, Any]:
|
|
486
|
+
"""
|
|
487
|
+
Build framework-specific services.
|
|
488
|
+
|
|
489
|
+
Override to provide framework-specific services like RouterRegistry.
|
|
490
|
+
Default returns empty dict.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
framework: Framework name (e.g., "fastapi", "flask", "spring")
|
|
494
|
+
parsed_files: All parsed files
|
|
495
|
+
project_root: Project root
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
Dict of service_name -> service_instance
|
|
499
|
+
"""
|
|
500
|
+
return {}
|
|
501
|
+
|
|
502
|
+
def build_context(
|
|
503
|
+
self,
|
|
504
|
+
parsed_files: list[ParsedFile],
|
|
505
|
+
project_root: Path | None = None,
|
|
506
|
+
framework: str | None = None,
|
|
507
|
+
) -> AnalysisContext:
|
|
508
|
+
"""
|
|
509
|
+
Build complete analysis context for this language.
|
|
510
|
+
|
|
511
|
+
This is the main entry point. It builds all services and
|
|
512
|
+
packages them into an AnalysisContext.
|
|
513
|
+
"""
|
|
514
|
+
type_resolver = self.build_type_resolver(parsed_files, project_root)
|
|
515
|
+
constant_resolver = self.build_constant_resolver(parsed_files, project_root)
|
|
516
|
+
path_resolver = self.build_path_resolver(parsed_files, project_root)
|
|
517
|
+
|
|
518
|
+
# Build framework-specific services
|
|
519
|
+
framework_services = {}
|
|
520
|
+
if framework:
|
|
521
|
+
framework_services = self.build_framework_services(
|
|
522
|
+
framework, parsed_files, project_root
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# Wire up dependencies between services
|
|
526
|
+
if constant_resolver and path_resolver:
|
|
527
|
+
# Path resolver may need constant resolver
|
|
528
|
+
if hasattr(path_resolver, "set_constant_resolver"):
|
|
529
|
+
path_resolver.set_constant_resolver(constant_resolver)
|
|
530
|
+
|
|
531
|
+
router_registry = framework_services.get("router_registry")
|
|
532
|
+
if router_registry and constant_resolver:
|
|
533
|
+
if hasattr(router_registry, "set_constant_resolver"):
|
|
534
|
+
router_registry.set_constant_resolver(constant_resolver)
|
|
535
|
+
|
|
536
|
+
return AnalysisContext(
|
|
537
|
+
type_resolver=type_resolver,
|
|
538
|
+
constant_resolver=constant_resolver,
|
|
539
|
+
path_resolver=path_resolver,
|
|
540
|
+
router_registry=router_registry,
|
|
541
|
+
project_root=project_root,
|
|
542
|
+
all_parsed_files=parsed_files,
|
|
543
|
+
language_services=framework_services,
|
|
544
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Query API executor for extended analysis."""
|