apisec-code-bolt 0.1.0__py3-none-any.whl

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