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,221 @@
|
|
|
1
|
+
"""CST expression analysis utilities.
|
|
2
|
+
|
|
3
|
+
Standalone functions for analyzing LibCST expression nodes — detecting
|
|
4
|
+
string interpolation, concatenation, container types, collecting referenced
|
|
5
|
+
variable names, etc. These are pure functions with no visitor state dependency.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import libcst as cst
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def collect_name_nodes(node: cst.BaseExpression) -> list[str]:
|
|
14
|
+
"""
|
|
15
|
+
Recursively collect all variable Name nodes from an expression.
|
|
16
|
+
|
|
17
|
+
Descends through Call arguments, BinaryOperations, Attributes,
|
|
18
|
+
Subscripts, f-string interpolations, comparisons, unary ops,
|
|
19
|
+
ternaries, and container literals to find every referenced variable.
|
|
20
|
+
|
|
21
|
+
Excludes keywords like True/False/None and builtin function names
|
|
22
|
+
when they appear as the func of a Call (those are the transformation,
|
|
23
|
+
not the source).
|
|
24
|
+
"""
|
|
25
|
+
results: list[str] = []
|
|
26
|
+
seen: set[str] = set()
|
|
27
|
+
|
|
28
|
+
def _add(name: str) -> None:
|
|
29
|
+
if name and name not in seen and name not in ("True", "False", "None"):
|
|
30
|
+
seen.add(name)
|
|
31
|
+
results.append(name)
|
|
32
|
+
|
|
33
|
+
def _walk(expr: cst.BaseExpression) -> None:
|
|
34
|
+
if isinstance(expr, cst.Name):
|
|
35
|
+
_add(expr.value)
|
|
36
|
+
|
|
37
|
+
elif isinstance(expr, cst.Attribute):
|
|
38
|
+
root = get_root_name(expr)
|
|
39
|
+
if root:
|
|
40
|
+
_add(root)
|
|
41
|
+
|
|
42
|
+
elif isinstance(expr, cst.Call):
|
|
43
|
+
# For x.method(a, b): receiver is a source variable, args too
|
|
44
|
+
if isinstance(expr.func, cst.Attribute):
|
|
45
|
+
root = get_root_name(expr.func.value)
|
|
46
|
+
if root:
|
|
47
|
+
_add(root)
|
|
48
|
+
# Walk call arguments
|
|
49
|
+
for arg in expr.args:
|
|
50
|
+
_walk(arg.value)
|
|
51
|
+
|
|
52
|
+
elif isinstance(expr, cst.BinaryOperation):
|
|
53
|
+
_walk(expr.left)
|
|
54
|
+
_walk(expr.right)
|
|
55
|
+
|
|
56
|
+
elif isinstance(expr, cst.UnaryOperation):
|
|
57
|
+
_walk(expr.expression)
|
|
58
|
+
|
|
59
|
+
elif isinstance(expr, cst.BooleanOperation):
|
|
60
|
+
_walk(expr.left)
|
|
61
|
+
_walk(expr.right)
|
|
62
|
+
|
|
63
|
+
elif isinstance(expr, cst.Comparison):
|
|
64
|
+
_walk(expr.left)
|
|
65
|
+
for target in expr.comparisons:
|
|
66
|
+
_walk(target.comparator)
|
|
67
|
+
|
|
68
|
+
elif isinstance(expr, cst.IfExp):
|
|
69
|
+
_walk(expr.body)
|
|
70
|
+
_walk(expr.test)
|
|
71
|
+
_walk(expr.orelse)
|
|
72
|
+
|
|
73
|
+
elif isinstance(expr, cst.Subscript):
|
|
74
|
+
_walk(expr.value)
|
|
75
|
+
|
|
76
|
+
elif isinstance(expr, (cst.FormattedString, cst.ConcatenatedString)):
|
|
77
|
+
walk_fstring_parts(expr, _walk)
|
|
78
|
+
|
|
79
|
+
elif isinstance(expr, cst.Tuple):
|
|
80
|
+
for el in expr.elements:
|
|
81
|
+
_walk(el.value)
|
|
82
|
+
|
|
83
|
+
elif isinstance(expr, (cst.List, cst.Set)):
|
|
84
|
+
for el in expr.elements:
|
|
85
|
+
if isinstance(el, (cst.Element, cst.StarredElement)):
|
|
86
|
+
_walk(el.value)
|
|
87
|
+
|
|
88
|
+
elif isinstance(expr, cst.Dict):
|
|
89
|
+
for el in expr.elements:
|
|
90
|
+
if isinstance(el, cst.DictElement):
|
|
91
|
+
_walk(el.key)
|
|
92
|
+
_walk(el.value)
|
|
93
|
+
elif isinstance(el, cst.StarredDictElement):
|
|
94
|
+
_walk(el.value)
|
|
95
|
+
|
|
96
|
+
elif isinstance(expr, (cst.FormattedStringExpression, cst.Await)):
|
|
97
|
+
_walk(expr.expression)
|
|
98
|
+
|
|
99
|
+
_walk(node)
|
|
100
|
+
return results
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_root_name(node: cst.BaseExpression) -> str | None:
|
|
104
|
+
"""
|
|
105
|
+
Walk through nested Attribute / Subscript to find the root Name.
|
|
106
|
+
For ``a.b.c`` returns ``"a"``, for ``items[0].x`` returns ``"items"``.
|
|
107
|
+
"""
|
|
108
|
+
while True:
|
|
109
|
+
if isinstance(node, cst.Name):
|
|
110
|
+
return node.value
|
|
111
|
+
if isinstance(node, (cst.Attribute, cst.Subscript)):
|
|
112
|
+
node = node.value
|
|
113
|
+
elif isinstance(node, cst.Call):
|
|
114
|
+
node = node.func
|
|
115
|
+
else:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def walk_fstring_parts(
|
|
120
|
+
node: cst.BaseExpression,
|
|
121
|
+
walker: callable,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Walk parts of a FormattedString / ConcatenatedString."""
|
|
124
|
+
if isinstance(node, cst.FormattedString):
|
|
125
|
+
for part in node.parts:
|
|
126
|
+
if isinstance(part, cst.FormattedStringExpression):
|
|
127
|
+
walker(part.expression)
|
|
128
|
+
elif isinstance(node, cst.ConcatenatedString):
|
|
129
|
+
walk_fstring_parts(node.left, walker)
|
|
130
|
+
walk_fstring_parts(node.right, walker)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def detect_fstring(value: cst.BaseExpression) -> bool:
|
|
134
|
+
"""Return True if the expression is or contains an f-string."""
|
|
135
|
+
if isinstance(value, cst.FormattedString):
|
|
136
|
+
return any(isinstance(p, cst.FormattedStringExpression) for p in value.parts)
|
|
137
|
+
if isinstance(value, cst.ConcatenatedString):
|
|
138
|
+
return detect_fstring(value.left) or detect_fstring(value.right)
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def detect_concatenation(value: cst.BaseExpression) -> bool:
|
|
143
|
+
"""Return True if the expression is a string concatenation with +."""
|
|
144
|
+
if isinstance(value, cst.BinaryOperation) and isinstance(value.operator, cst.Add):
|
|
145
|
+
left_might_be_str = isinstance(
|
|
146
|
+
value.left, (cst.SimpleString, cst.FormattedString, cst.ConcatenatedString)
|
|
147
|
+
) or detect_concatenation(value.left)
|
|
148
|
+
right_might_be_str = isinstance(
|
|
149
|
+
value.right, (cst.SimpleString, cst.FormattedString, cst.ConcatenatedString)
|
|
150
|
+
) or detect_concatenation(value.right)
|
|
151
|
+
if left_might_be_str or right_might_be_str:
|
|
152
|
+
return True
|
|
153
|
+
# Also flag any + with a Name operand: "SELECT " + uid
|
|
154
|
+
if isinstance(value.left, cst.Name) or isinstance(value.right, cst.Name):
|
|
155
|
+
return True
|
|
156
|
+
return bool(isinstance(value, cst.ConcatenatedString))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def detect_format_call(value: cst.BaseExpression) -> bool:
|
|
160
|
+
"""Return True if the expression uses .format() or % string formatting."""
|
|
161
|
+
if isinstance(value, cst.Call) and isinstance(value.func, cst.Attribute):
|
|
162
|
+
if value.func.attr.value == "format":
|
|
163
|
+
return True
|
|
164
|
+
if isinstance(value, cst.BinaryOperation) and isinstance(value.operator, cst.Modulo):
|
|
165
|
+
if isinstance(value.left, (cst.SimpleString, cst.FormattedString, cst.ConcatenatedString)):
|
|
166
|
+
return True
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def detect_container_type(value: cst.BaseExpression) -> str | None:
|
|
171
|
+
"""Detect if value is a container literal and return its type."""
|
|
172
|
+
if isinstance(value, cst.List):
|
|
173
|
+
return "list"
|
|
174
|
+
if isinstance(value, cst.Tuple):
|
|
175
|
+
return "tuple"
|
|
176
|
+
if isinstance(value, cst.Dict):
|
|
177
|
+
return "dict"
|
|
178
|
+
if isinstance(value, cst.Set):
|
|
179
|
+
return "set"
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def detect_method_call(value: cst.BaseExpression) -> bool:
|
|
184
|
+
"""Return True if the expression is a method call on a variable."""
|
|
185
|
+
if not isinstance(value, cst.Call):
|
|
186
|
+
return False
|
|
187
|
+
func = value.func
|
|
188
|
+
if not isinstance(func, cst.Attribute):
|
|
189
|
+
return False
|
|
190
|
+
root = get_root_name(func.value)
|
|
191
|
+
return root is not None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def extract_value_metadata(
|
|
195
|
+
value: cst.BaseExpression | None,
|
|
196
|
+
) -> tuple[list[str], bool, bool]:
|
|
197
|
+
"""
|
|
198
|
+
Analyze a value expression and return:
|
|
199
|
+
(source_variables, is_method_call, is_string_interpolation).
|
|
200
|
+
"""
|
|
201
|
+
if value is None:
|
|
202
|
+
return [], False, False
|
|
203
|
+
source_vars = collect_name_nodes(value)
|
|
204
|
+
is_method = detect_method_call(value)
|
|
205
|
+
is_fstr = detect_fstring(value)
|
|
206
|
+
return source_vars, is_method, is_fstr
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def is_classvar_annotation(annotation: cst.BaseExpression) -> bool:
|
|
210
|
+
"""Return True if the annotation is ``ClassVar`` or ``ClassVar[...]``."""
|
|
211
|
+
if isinstance(annotation, cst.Name) and annotation.value == "ClassVar":
|
|
212
|
+
return True
|
|
213
|
+
if isinstance(annotation, cst.Subscript):
|
|
214
|
+
base = annotation.value
|
|
215
|
+
if isinstance(base, cst.Name) and base.value == "ClassVar":
|
|
216
|
+
return True
|
|
217
|
+
if isinstance(base, cst.Attribute):
|
|
218
|
+
attr = base.attr
|
|
219
|
+
if isinstance(attr, cst.Name) and attr.value == "ClassVar":
|
|
220
|
+
return True
|
|
221
|
+
return False
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Extraction data types for Python code analysis.
|
|
2
|
+
|
|
3
|
+
Pure data models representing structural elements extracted from Python CST:
|
|
4
|
+
- Function/method definitions
|
|
5
|
+
- Class definitions
|
|
6
|
+
- Import statements
|
|
7
|
+
- Function calls
|
|
8
|
+
- Assignments
|
|
9
|
+
- Decorators
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from ...core.types import CodeLocation
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ExtractedDecorator:
|
|
22
|
+
"""A decorator extracted from the CST."""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
full_name: str # e.g., "app.get" for @app.get(...)
|
|
26
|
+
arguments: dict[str, Any]
|
|
27
|
+
positional_args: list[Any]
|
|
28
|
+
location: CodeLocation
|
|
29
|
+
raw_source: str | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ExtractedParameter:
|
|
34
|
+
"""A function parameter extracted from the CST."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
annotation: str | None = None
|
|
38
|
+
default: str | None = None
|
|
39
|
+
is_variadic: bool = False # *args
|
|
40
|
+
is_keyword_variadic: bool = False # **kwargs
|
|
41
|
+
is_positional_only: bool = False
|
|
42
|
+
is_keyword_only: bool = False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class ExtractedFunction:
|
|
47
|
+
"""A function or method extracted from the CST."""
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
qualified_name: str
|
|
51
|
+
parameters: list[ExtractedParameter]
|
|
52
|
+
return_annotation: str | None
|
|
53
|
+
decorators: list[ExtractedDecorator]
|
|
54
|
+
|
|
55
|
+
is_async: bool = False
|
|
56
|
+
binding: str = "free" # "instance", "static", or "free"
|
|
57
|
+
|
|
58
|
+
docstring: str | None = None
|
|
59
|
+
body_source: str | None = None # For complex analysis
|
|
60
|
+
|
|
61
|
+
# Location info
|
|
62
|
+
line: int = 0
|
|
63
|
+
end_line: int = 0
|
|
64
|
+
column: int = 0
|
|
65
|
+
|
|
66
|
+
# Owner class if this is a method
|
|
67
|
+
owner_type: str | None = None
|
|
68
|
+
|
|
69
|
+
# Local variable names (for data flow)
|
|
70
|
+
local_variables: list[str] = field(default_factory=list)
|
|
71
|
+
|
|
72
|
+
# Control flow information
|
|
73
|
+
control_flow_info: dict[str, Any] = field(default_factory=dict)
|
|
74
|
+
has_yield: bool = False
|
|
75
|
+
has_return: bool = False
|
|
76
|
+
|
|
77
|
+
# Return statement analysis
|
|
78
|
+
return_statements: list[ExtractedReturn] = field(default_factory=list)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class ExtractedReturn:
|
|
83
|
+
"""A return statement extracted from the CST."""
|
|
84
|
+
|
|
85
|
+
line: int
|
|
86
|
+
|
|
87
|
+
# What is being returned
|
|
88
|
+
returns_none: bool = False # return or return None
|
|
89
|
+
returns_call: bool = False # return func()
|
|
90
|
+
returns_variable: bool = False # return x
|
|
91
|
+
returns_literal: bool = False # return "string" or return 42
|
|
92
|
+
returns_expression: bool = False # return x + y
|
|
93
|
+
|
|
94
|
+
# Details about what's returned
|
|
95
|
+
call_name: str | None = None # Name of the called function
|
|
96
|
+
variable_name: str | None = None # Name of the variable
|
|
97
|
+
literal_value: Any = None # Literal value
|
|
98
|
+
literal_type: str | None = None # Type of literal
|
|
99
|
+
expression_text: str | None = None # Expression source
|
|
100
|
+
|
|
101
|
+
# For comprehensions and lambdas
|
|
102
|
+
returns_comprehension: bool = False
|
|
103
|
+
returns_lambda: bool = False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class ExtractedControlFlowBlock:
|
|
108
|
+
"""A control flow structure (if/for/while/try/with)."""
|
|
109
|
+
|
|
110
|
+
block_type: str # "if", "for", "while", "try", "with", "comprehension"
|
|
111
|
+
start_line: int
|
|
112
|
+
end_line: int
|
|
113
|
+
|
|
114
|
+
# For if blocks
|
|
115
|
+
has_elif: bool = False
|
|
116
|
+
has_else: bool = False
|
|
117
|
+
elif_lines: list[int] = field(default_factory=list)
|
|
118
|
+
else_line: int | None = None
|
|
119
|
+
|
|
120
|
+
# For try blocks
|
|
121
|
+
except_blocks: list[tuple[int, int]] = field(default_factory=list) # (start, end) pairs
|
|
122
|
+
finally_block: tuple[int, int] | None = None
|
|
123
|
+
|
|
124
|
+
# For loops
|
|
125
|
+
has_break: bool = False
|
|
126
|
+
has_continue: bool = False
|
|
127
|
+
|
|
128
|
+
# For with blocks
|
|
129
|
+
context_expr: str | None = None
|
|
130
|
+
# All context-manager expressions (supports ``with a, b:``)
|
|
131
|
+
with_items: list[str] = field(default_factory=list)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass
|
|
135
|
+
class ExtractedClass:
|
|
136
|
+
"""A class extracted from the CST."""
|
|
137
|
+
|
|
138
|
+
name: str
|
|
139
|
+
qualified_name: str
|
|
140
|
+
bases: list[str]
|
|
141
|
+
decorators: list[ExtractedDecorator]
|
|
142
|
+
|
|
143
|
+
methods: list[ExtractedFunction] = field(default_factory=list)
|
|
144
|
+
class_variables: list[str] = field(default_factory=list)
|
|
145
|
+
instance_variables: list[str] = field(default_factory=list)
|
|
146
|
+
|
|
147
|
+
docstring: str | None = None
|
|
148
|
+
|
|
149
|
+
is_dataclass: bool = False
|
|
150
|
+
is_pydantic_model: bool = False
|
|
151
|
+
|
|
152
|
+
# Pydantic/dataclass fields
|
|
153
|
+
fields: list[ExtractedField] = field(default_factory=list)
|
|
154
|
+
|
|
155
|
+
# Location info
|
|
156
|
+
line: int = 0
|
|
157
|
+
end_line: int = 0
|
|
158
|
+
column: int = 0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass
|
|
162
|
+
class ExtractedField:
|
|
163
|
+
"""A class field (for Pydantic models, dataclasses)."""
|
|
164
|
+
|
|
165
|
+
name: str
|
|
166
|
+
annotation: str | None = None
|
|
167
|
+
default: str | None = None
|
|
168
|
+
field_info: dict[str, Any] = field(default_factory=dict)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass
|
|
172
|
+
class ExtractedImport:
|
|
173
|
+
"""An import statement extracted from the CST."""
|
|
174
|
+
|
|
175
|
+
module: str
|
|
176
|
+
names: list[tuple[str, str | None]] # (name, alias)
|
|
177
|
+
is_from_import: bool
|
|
178
|
+
is_relative: bool = False
|
|
179
|
+
relative_level: int = 0
|
|
180
|
+
line: int = 0
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass
|
|
184
|
+
class ExtractedCall:
|
|
185
|
+
"""A function call extracted from the CST."""
|
|
186
|
+
|
|
187
|
+
callee: str # Full call expression, e.g., "db.execute"
|
|
188
|
+
arguments: list[ExtractedArgument]
|
|
189
|
+
|
|
190
|
+
# Context
|
|
191
|
+
line: int = 0
|
|
192
|
+
column: int = 0
|
|
193
|
+
end_line: int = 0
|
|
194
|
+
in_function: str | None = None # Enclosing function name
|
|
195
|
+
|
|
196
|
+
# Receiver info for method calls
|
|
197
|
+
is_method_call: bool = False
|
|
198
|
+
receiver: str | None = None
|
|
199
|
+
|
|
200
|
+
# Control flow context
|
|
201
|
+
in_loop: bool = False
|
|
202
|
+
in_conditional: bool = False
|
|
203
|
+
in_try: bool = False
|
|
204
|
+
in_except: bool = False
|
|
205
|
+
in_finally: bool = False
|
|
206
|
+
in_with: bool = False
|
|
207
|
+
in_comprehension: bool = False
|
|
208
|
+
loop_depth: int = 0
|
|
209
|
+
conditional_depth: int = 0
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclass
|
|
213
|
+
class ExtractedArgument:
|
|
214
|
+
"""An argument in a function call."""
|
|
215
|
+
|
|
216
|
+
value_source: str # Source code of the value (required)
|
|
217
|
+
|
|
218
|
+
# Optional fields with defaults
|
|
219
|
+
position: int | None = None
|
|
220
|
+
keyword: str | None = None
|
|
221
|
+
|
|
222
|
+
is_literal: bool = False
|
|
223
|
+
literal_value: Any = None
|
|
224
|
+
literal_type: str | None = None
|
|
225
|
+
|
|
226
|
+
is_name: bool = False
|
|
227
|
+
name_value: str | None = None
|
|
228
|
+
|
|
229
|
+
is_starred: bool = False
|
|
230
|
+
is_double_starred: bool = False
|
|
231
|
+
|
|
232
|
+
# Argument construction details (language-agnostic)
|
|
233
|
+
is_string_interpolation: bool = False
|
|
234
|
+
is_concatenation: bool = False
|
|
235
|
+
is_format_call: bool = False
|
|
236
|
+
container_type: str | None = None # "list", "tuple", "dict", "set"
|
|
237
|
+
source_variables: list[str] = field(default_factory=list)
|
|
238
|
+
|
|
239
|
+
# For call-wrapped arguments: func(int(x)) -> called_function = "int"
|
|
240
|
+
is_call_result: bool = False
|
|
241
|
+
called_function: str | None = None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@dataclass
|
|
245
|
+
class ExtractedAssignment:
|
|
246
|
+
"""A variable assignment extracted from the CST."""
|
|
247
|
+
|
|
248
|
+
target: str
|
|
249
|
+
value_source: str
|
|
250
|
+
annotation: str | None = None
|
|
251
|
+
|
|
252
|
+
line: int = 0
|
|
253
|
+
in_function: str | None = None
|
|
254
|
+
|
|
255
|
+
# Value type hints
|
|
256
|
+
is_literal: bool = False
|
|
257
|
+
is_call: bool = False
|
|
258
|
+
called_function: str | None = None
|
|
259
|
+
is_name: bool = False
|
|
260
|
+
referenced_name: str | None = None
|
|
261
|
+
|
|
262
|
+
# Variables in the RHS expression that the target derives from.
|
|
263
|
+
# For "x = int(y)" → ["y"], for "q = f'{a} {b}'" → ["a", "b"],
|
|
264
|
+
# for "x = obj.method()" → ["obj"].
|
|
265
|
+
source_variables: list[str] = field(default_factory=list)
|
|
266
|
+
|
|
267
|
+
# True when value is a method call on a variable (y = x.strip())
|
|
268
|
+
is_method_call: bool = False
|
|
269
|
+
|
|
270
|
+
# True when value is a string interpolation (f-string / FormattedString)
|
|
271
|
+
is_string_interpolation: bool = False
|