seam-code 0.3.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.
- seam/__init__.py +3 -0
- seam/_data/schema.sql +225 -0
- seam/_web/assets/index-BL_tqprR.js +216 -0
- seam/_web/assets/index-GTKUhVyD.css +1 -0
- seam/_web/index.html +13 -0
- seam/analysis/__init__.py +14 -0
- seam/analysis/affected.py +254 -0
- seam/analysis/builtins.py +966 -0
- seam/analysis/byte_budget.py +217 -0
- seam/analysis/changes.py +709 -0
- seam/analysis/cluster_naming.py +260 -0
- seam/analysis/clustering.py +216 -0
- seam/analysis/confidence.py +699 -0
- seam/analysis/embeddings.py +195 -0
- seam/analysis/flows.py +708 -0
- seam/analysis/impact.py +444 -0
- seam/analysis/imports.py +994 -0
- seam/analysis/imports_ext.py +780 -0
- seam/analysis/imports_resolve.py +176 -0
- seam/analysis/processes.py +453 -0
- seam/analysis/relevance.py +155 -0
- seam/analysis/rwr.py +129 -0
- seam/analysis/staleness.py +328 -0
- seam/analysis/steer.py +282 -0
- seam/analysis/synthesis.py +253 -0
- seam/analysis/synthesis_channels.py +433 -0
- seam/analysis/testpaths.py +103 -0
- seam/analysis/traversal.py +470 -0
- seam/cli/__init__.py +0 -0
- seam/cli/install.py +232 -0
- seam/cli/main.py +2602 -0
- seam/cli/output.py +137 -0
- seam/cli/read.py +244 -0
- seam/cli/serve.py +145 -0
- seam/config.py +551 -0
- seam/indexer/__init__.py +0 -0
- seam/indexer/cluster_index.py +425 -0
- seam/indexer/db.py +496 -0
- seam/indexer/embedding_index.py +183 -0
- seam/indexer/field_access.py +536 -0
- seam/indexer/field_access_c_cpp.py +643 -0
- seam/indexer/field_access_ext.py +708 -0
- seam/indexer/field_access_ext2.py +408 -0
- seam/indexer/field_access_go_rust.py +737 -0
- seam/indexer/field_access_php_swift.py +888 -0
- seam/indexer/field_access_ts.py +626 -0
- seam/indexer/graph.py +321 -0
- seam/indexer/graph_c.py +562 -0
- seam/indexer/graph_c_cpp.py +39 -0
- seam/indexer/graph_common.py +644 -0
- seam/indexer/graph_cpp.py +615 -0
- seam/indexer/graph_csharp.py +651 -0
- seam/indexer/graph_go.py +723 -0
- seam/indexer/graph_go_rust.py +39 -0
- seam/indexer/graph_java.py +689 -0
- seam/indexer/graph_java_csharp.py +38 -0
- seam/indexer/graph_php.py +914 -0
- seam/indexer/graph_python.py +628 -0
- seam/indexer/graph_ruby.py +748 -0
- seam/indexer/graph_rust.py +653 -0
- seam/indexer/graph_scope_infer.py +902 -0
- seam/indexer/graph_scope_infer_ext.py +723 -0
- seam/indexer/graph_scope_infer_ext2.py +992 -0
- seam/indexer/graph_swift.py +1014 -0
- seam/indexer/graph_swift_infer.py +515 -0
- seam/indexer/graph_typescript.py +663 -0
- seam/indexer/migrations.py +816 -0
- seam/indexer/parser.py +204 -0
- seam/indexer/pipeline.py +197 -0
- seam/indexer/signatures.py +634 -0
- seam/indexer/signatures_ext.py +780 -0
- seam/indexer/sync.py +287 -0
- seam/indexer/synthesis_index.py +291 -0
- seam/indexer/tokenize.py +79 -0
- seam/installer/__init__.py +67 -0
- seam/installer/claude.py +97 -0
- seam/installer/codex.py +94 -0
- seam/installer/core.py +127 -0
- seam/installer/cursor.py +61 -0
- seam/installer/guide.py +110 -0
- seam/installer/jsonfile.py +85 -0
- seam/installer/markdownfile.py +146 -0
- seam/installer/tomlfile.py +72 -0
- seam/query/__init__.py +0 -0
- seam/query/clusters.py +206 -0
- seam/query/comments.py +217 -0
- seam/query/context.py +293 -0
- seam/query/engine.py +940 -0
- seam/query/fts.py +328 -0
- seam/query/names.py +470 -0
- seam/query/pack.py +433 -0
- seam/query/semantic.py +339 -0
- seam/query/structure.py +727 -0
- seam/server/__init__.py +0 -0
- seam/server/graph_api.py +437 -0
- seam/server/handler_common.py +323 -0
- seam/server/impact_handler.py +615 -0
- seam/server/mcp.py +556 -0
- seam/server/tools.py +697 -0
- seam/server/trace_handler.py +184 -0
- seam/server/web.py +922 -0
- seam/watcher/__init__.py +0 -0
- seam/watcher/__main__.py +56 -0
- seam/watcher/daemon.py +237 -0
- seam_code-0.3.0.dist-info/METADATA +318 -0
- seam_code-0.3.0.dist-info/RECORD +109 -0
- seam_code-0.3.0.dist-info/WHEEL +4 -0
- seam_code-0.3.0.dist-info/entry_points.txt +2 -0
- seam_code-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
"""Field-access edge helper — PHP and Swift read/write classification.
|
|
2
|
+
|
|
3
|
+
LAYER: leaf — imports only stdlib + tree_sitter + graph_common + the existing
|
|
4
|
+
resolve_receiver_type helpers. Never imports from graph.py, db.py, or any
|
|
5
|
+
non-leaf seam module.
|
|
6
|
+
|
|
7
|
+
LAYERING:
|
|
8
|
+
graph_common (leaf — no seam deps)
|
|
9
|
+
graph_scope_infer_ext (leaf — Go/Rust receiver-type inference)
|
|
10
|
+
graph_scope_infer_ext2 (leaf — Java/C#/C++/Ruby/PHP receiver-type inference)
|
|
11
|
+
↑
|
|
12
|
+
field_access_php_swift (this file — field-access classification for PHP + Swift)
|
|
13
|
+
↑
|
|
14
|
+
field_access_ext2 (re-exports extract_field_accesses_php/swift +
|
|
15
|
+
collect_field_symbols_php/swift for backward compat)
|
|
16
|
+
graph_php (calls those functions via field_access_ext2 import)
|
|
17
|
+
graph_swift (calls those functions via field_access_ext2 import;
|
|
18
|
+
also calls emit_swift_field_access_edges and
|
|
19
|
+
emit_swift_field_symbols directly)
|
|
20
|
+
|
|
21
|
+
WHY a separate module from field_access_ext2.py:
|
|
22
|
+
field_access_ext2.py (Ruby + PHP + Swift) exceeds 1000 lines. Splitting PHP + Swift
|
|
23
|
+
into this module keeps both resulting files under the limit.
|
|
24
|
+
This also hosts the Swift field-access emission helpers called by graph_swift.py,
|
|
25
|
+
which are moved here to keep graph_swift.py under 1000 lines.
|
|
26
|
+
|
|
27
|
+
PHP member access uses member_access_expression: $this->field or $obj->field.
|
|
28
|
+
This is DISTINCT from member_call_expression ($obj->method()):
|
|
29
|
+
member_access_expression → field access (may be read or write)
|
|
30
|
+
member_call_expression → method call (NOT a field edge)
|
|
31
|
+
|
|
32
|
+
Swift stored-property access uses navigation_expression: self.prop or obj.prop.
|
|
33
|
+
Call position: call_expression whose first child is a navigation_expression.
|
|
34
|
+
→ the navigation_expression is the callee → NOT a field edge.
|
|
35
|
+
|
|
36
|
+
NEVER RAISES: all public functions have a backstop try/except and return [] on error.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
import logging
|
|
40
|
+
|
|
41
|
+
from tree_sitter import Node
|
|
42
|
+
|
|
43
|
+
from seam.indexer.graph_common import (
|
|
44
|
+
Edge,
|
|
45
|
+
Symbol,
|
|
46
|
+
_text,
|
|
47
|
+
)
|
|
48
|
+
from seam.indexer.graph_scope_infer_ext import resolve_receiver_type_ext
|
|
49
|
+
from seam.indexer.graph_scope_infer_ext2 import _PHP_SELF_NAMES
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
# Return type for the public API: (source_fn, target_field, mode, line)
|
|
54
|
+
# mode is "reads" or "writes"
|
|
55
|
+
FieldAccess = tuple[str, str, str, int]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
59
|
+
# PHP field-access classification (A3 Slice 5)
|
|
60
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
61
|
+
#
|
|
62
|
+
# PHP member access uses member_access_expression: $this->field or $obj->field.
|
|
63
|
+
# This is DISTINCT from member_call_expression ($obj->method()):
|
|
64
|
+
# member_access_expression → field access (may be read or write)
|
|
65
|
+
# member_call_expression → method call (NOT a field edge)
|
|
66
|
+
#
|
|
67
|
+
# PHP augmented assignment:
|
|
68
|
+
# augmented_assignment_expression LHS member_access_expression → write
|
|
69
|
+
#
|
|
70
|
+
# Receiver resolution:
|
|
71
|
+
# $this → enclosing class via _PHP_SELF_NAMES (EXTRACTED).
|
|
72
|
+
# Other vars: resolved via resolve_receiver_type_ext from var_types.
|
|
73
|
+
# Unresolvable: bare field name (AMBIGUOUS).
|
|
74
|
+
#
|
|
75
|
+
# Field symbols:
|
|
76
|
+
# property_declaration nodes in the class body (declaration_list).
|
|
77
|
+
# The property_element child contains variable_name ($field).
|
|
78
|
+
# Strip the '$' prefix for the qualified target: '$balance' → 'balance'.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def extract_field_accesses_php(
|
|
82
|
+
func_body: Node,
|
|
83
|
+
source_fn: str,
|
|
84
|
+
class_name: str | None,
|
|
85
|
+
var_types: dict[str, str],
|
|
86
|
+
) -> list[FieldAccess]:
|
|
87
|
+
"""Extract field accesses (reads and writes) from a PHP method/function body.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
func_body: The 'compound_statement' body Node of the method/function.
|
|
91
|
+
source_fn: Qualified name of the enclosing function, e.g. 'Account.deposit'.
|
|
92
|
+
class_name: Enclosing class name (None for top-level functions).
|
|
93
|
+
var_types: Scope map: param/local variable name → type name (with '$' prefix).
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
List of (source_fn, target_field, mode, line) tuples.
|
|
97
|
+
target_field = 'ClassName.field' when $this receiver; bare 'field' otherwise.
|
|
98
|
+
mode = 'reads' | 'writes'.
|
|
99
|
+
line = 1-based source line.
|
|
100
|
+
Returns [] on any error.
|
|
101
|
+
|
|
102
|
+
Never raises.
|
|
103
|
+
"""
|
|
104
|
+
result: list[FieldAccess] = []
|
|
105
|
+
try:
|
|
106
|
+
_php_walk_body(func_body, source_fn, class_name, var_types, result)
|
|
107
|
+
except Exception as exc: # noqa: BLE001
|
|
108
|
+
logger.debug(
|
|
109
|
+
"extract_field_accesses_php: failed for source=%r: %r", source_fn, exc
|
|
110
|
+
)
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _php_walk_body(
|
|
115
|
+
node: Node,
|
|
116
|
+
source_fn: str,
|
|
117
|
+
class_name: str | None,
|
|
118
|
+
var_types: dict[str, str],
|
|
119
|
+
result: list[FieldAccess],
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Recursively walk a PHP function/method body collecting field accesses."""
|
|
122
|
+
for child in node.children:
|
|
123
|
+
_php_walk_stmt(child, source_fn, class_name, var_types, result)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _php_walk_stmt(
|
|
127
|
+
node: Node,
|
|
128
|
+
source_fn: str,
|
|
129
|
+
class_name: str | None,
|
|
130
|
+
var_types: dict[str, str],
|
|
131
|
+
result: list[FieldAccess],
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Walk a single PHP statement node for field accesses.
|
|
134
|
+
|
|
135
|
+
Handles:
|
|
136
|
+
expression_statement containing assignment/augmented_assignment → extract
|
|
137
|
+
assignment_expression: LHS member_access_expression → write.
|
|
138
|
+
augmented_assignment_expression: LHS member_access_expression → write.
|
|
139
|
+
member_call_expression: NOT a field edge (method call). Recurse into arguments.
|
|
140
|
+
member_access_expression (standalone, not in call position) → read.
|
|
141
|
+
All other nodes: recurse collecting reads.
|
|
142
|
+
Skips nested class/function/method definitions.
|
|
143
|
+
"""
|
|
144
|
+
t = node.type
|
|
145
|
+
|
|
146
|
+
# Skip nested scope-creating constructs.
|
|
147
|
+
if t in ("class_declaration", "function_definition", "method_declaration",
|
|
148
|
+
"arrow_function", "anonymous_function_creation_expression"):
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
# assignment_expression: $this->field = v → write; RHS → reads.
|
|
152
|
+
if t == "assignment_expression":
|
|
153
|
+
_php_handle_assignment(node, source_fn, class_name, var_types, result)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
# augmented_assignment_expression: $this->field += v → write; RHS → reads.
|
|
157
|
+
if t == "augmented_assignment_expression":
|
|
158
|
+
_php_handle_augmented_assignment(node, source_fn, class_name, var_types, result)
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
# member_call_expression: $this->method() → NOT a field edge. Recurse into args.
|
|
162
|
+
# WHY PHP needs explicit member_call_expression handling (unlike Python/TS):
|
|
163
|
+
# PHP uses TWO DISTINCT node types — member_access_expression for $this->field
|
|
164
|
+
# and member_call_expression for $this->method(). There is no shared parent node
|
|
165
|
+
# to check "is this in call position?" on. Handling member_call_expression here
|
|
166
|
+
# prevents the generic recursion from reaching its member_access_expression object
|
|
167
|
+
# child (the receiver) and mistakenly emitting a field-read for it.
|
|
168
|
+
if t in ("member_call_expression", "nullsafe_member_call_expression"):
|
|
169
|
+
# Only recurse into the arguments — not the object/name fields.
|
|
170
|
+
args = node.child_by_field_name("arguments")
|
|
171
|
+
if args is not None:
|
|
172
|
+
for arg in args.children:
|
|
173
|
+
_php_walk_stmt(arg, source_fn, class_name, var_types, result)
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
# A bare member_access_expression (not in call position) → read.
|
|
177
|
+
if t == "member_access_expression":
|
|
178
|
+
_php_emit_read_if_not_in_call(node, source_fn, class_name, var_types, result)
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
# Recurse into all other node types.
|
|
182
|
+
for child in node.children:
|
|
183
|
+
_php_walk_stmt(child, source_fn, class_name, var_types, result)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _php_handle_assignment(
|
|
187
|
+
node: Node,
|
|
188
|
+
source_fn: str,
|
|
189
|
+
class_name: str | None,
|
|
190
|
+
var_types: dict[str, str],
|
|
191
|
+
result: list[FieldAccess],
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Handle PHP assignment_expression: LHS member_access_expression → write."""
|
|
194
|
+
left = node.child_by_field_name("left")
|
|
195
|
+
if left is not None and left.type == "member_access_expression":
|
|
196
|
+
acc = _php_classify_member_access(left, class_name, var_types)
|
|
197
|
+
if acc is not None:
|
|
198
|
+
target, _ = acc
|
|
199
|
+
result.append((source_fn, target, "writes", left.start_point[0] + 1))
|
|
200
|
+
|
|
201
|
+
right = node.child_by_field_name("right")
|
|
202
|
+
if right is not None:
|
|
203
|
+
_php_collect_reads_recursive(right, source_fn, class_name, var_types, result)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _php_handle_augmented_assignment(
|
|
207
|
+
node: Node,
|
|
208
|
+
source_fn: str,
|
|
209
|
+
class_name: str | None,
|
|
210
|
+
var_types: dict[str, str],
|
|
211
|
+
result: list[FieldAccess],
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Handle PHP augmented_assignment_expression: LHS member_access_expression → write.
|
|
214
|
+
|
|
215
|
+
PHP grammar: augmented_assignment_expression has 'left' and 'right' fields.
|
|
216
|
+
"""
|
|
217
|
+
left = node.child_by_field_name("left")
|
|
218
|
+
if left is not None and left.type == "member_access_expression":
|
|
219
|
+
acc = _php_classify_member_access(left, class_name, var_types)
|
|
220
|
+
if acc is not None:
|
|
221
|
+
target, _ = acc
|
|
222
|
+
result.append((source_fn, target, "writes", left.start_point[0] + 1))
|
|
223
|
+
|
|
224
|
+
right = node.child_by_field_name("right")
|
|
225
|
+
if right is not None:
|
|
226
|
+
_php_collect_reads_recursive(right, source_fn, class_name, var_types, result)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _php_emit_read_if_not_in_call(
|
|
230
|
+
node: Node,
|
|
231
|
+
source_fn: str,
|
|
232
|
+
class_name: str | None,
|
|
233
|
+
var_types: dict[str, str],
|
|
234
|
+
result: list[FieldAccess],
|
|
235
|
+
) -> None:
|
|
236
|
+
"""Emit a reads edge for a PHP member_access_expression NOT in call position.
|
|
237
|
+
|
|
238
|
+
A member_access_expression is in call position when its parent is a
|
|
239
|
+
member_call_expression/nullsafe_member_call_expression AND it is the
|
|
240
|
+
'object' part of that call. We handle this by not reaching here for such cases
|
|
241
|
+
(the _php_walk_stmt handles member_call_expression separately and recurses only
|
|
242
|
+
into arguments, not the object).
|
|
243
|
+
"""
|
|
244
|
+
parent = node.parent
|
|
245
|
+
if parent is not None and parent.type in (
|
|
246
|
+
"member_call_expression", "nullsafe_member_call_expression"
|
|
247
|
+
):
|
|
248
|
+
# If this member_access_expression is the 'object' field of a call → skip.
|
|
249
|
+
obj_field = parent.child_by_field_name("object")
|
|
250
|
+
if obj_field is not None and obj_field.start_point == node.start_point:
|
|
251
|
+
return # It's the receiver of a method call — skip
|
|
252
|
+
|
|
253
|
+
acc = _php_classify_member_access(node, class_name, var_types)
|
|
254
|
+
if acc is not None:
|
|
255
|
+
target, _ = acc
|
|
256
|
+
result.append((source_fn, target, "reads", node.start_point[0] + 1))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _php_collect_reads_recursive(
|
|
260
|
+
node: Node,
|
|
261
|
+
source_fn: str,
|
|
262
|
+
class_name: str | None,
|
|
263
|
+
var_types: dict[str, str],
|
|
264
|
+
result: list[FieldAccess],
|
|
265
|
+
) -> None:
|
|
266
|
+
"""Recursively collect reads from a PHP expression node."""
|
|
267
|
+
t = node.type
|
|
268
|
+
if t in ("class_declaration", "function_definition", "method_declaration",
|
|
269
|
+
"arrow_function", "anonymous_function_creation_expression"):
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
if t == "member_access_expression":
|
|
273
|
+
_php_emit_read_if_not_in_call(node, source_fn, class_name, var_types, result)
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
for child in node.children:
|
|
277
|
+
_php_collect_reads_recursive(child, source_fn, class_name, var_types, result)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _php_classify_member_access(
|
|
281
|
+
node: Node,
|
|
282
|
+
class_name: str | None,
|
|
283
|
+
var_types: dict[str, str],
|
|
284
|
+
) -> tuple[str, str | None] | None:
|
|
285
|
+
"""Resolve a PHP member_access_expression to (qualified_target, receiver_text).
|
|
286
|
+
|
|
287
|
+
PHP member_access_expression:
|
|
288
|
+
object: the receiver ($this, $obj, variable_name)
|
|
289
|
+
'->': the access operator
|
|
290
|
+
name: the field name ('name' node)
|
|
291
|
+
|
|
292
|
+
Returns None when the node lacks the expected shape.
|
|
293
|
+
Returns (target, receiver_text) where:
|
|
294
|
+
- target = 'ClassName.field' when $this / $self resolves (EXTRACTED confidence)
|
|
295
|
+
- target = 'Type.field' when receiver type is known via var_types (EXTRACTED)
|
|
296
|
+
- target = bare 'field' when unresolvable (AMBIGUOUS confidence)
|
|
297
|
+
receiver_text = raw receiver text (e.g. '$this', '$obj')
|
|
298
|
+
|
|
299
|
+
Conservatism contract: NEVER emit a wrong qualified target.
|
|
300
|
+
Never raises (returns None on any exception).
|
|
301
|
+
"""
|
|
302
|
+
try:
|
|
303
|
+
obj_node = node.child_by_field_name("object")
|
|
304
|
+
name_node = node.child_by_field_name("name")
|
|
305
|
+
if obj_node is None or name_node is None:
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
# name_node should be a 'name' node (plain identifier).
|
|
309
|
+
if name_node.type != "name":
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
field_name = _text(name_node)
|
|
313
|
+
if not field_name:
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
receiver_text = _text(obj_node)
|
|
317
|
+
|
|
318
|
+
resolved_type = resolve_receiver_type_ext(
|
|
319
|
+
receiver_text, class_name, var_types, _PHP_SELF_NAMES
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
if resolved_type is not None:
|
|
323
|
+
return f"{resolved_type}.{field_name}", receiver_text
|
|
324
|
+
else:
|
|
325
|
+
return field_name, receiver_text
|
|
326
|
+
|
|
327
|
+
except Exception as exc: # noqa: BLE001
|
|
328
|
+
logger.debug("_php_classify_member_access: failed: %r", exc)
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def collect_field_symbols_php(
|
|
333
|
+
class_node: Node,
|
|
334
|
+
class_name: str,
|
|
335
|
+
) -> list[tuple[str, int]]:
|
|
336
|
+
"""Collect (qualified_field_name, line) pairs from a PHP class_declaration.
|
|
337
|
+
|
|
338
|
+
Returns field symbols for ALL property_declaration nodes in the class body
|
|
339
|
+
(declaration_list). Each property_declaration may have one or more property_element
|
|
340
|
+
children. The field name has the '$' prefix stripped: '$balance' → 'Account.balance'.
|
|
341
|
+
|
|
342
|
+
Returns [] on any error. Never raises.
|
|
343
|
+
"""
|
|
344
|
+
try:
|
|
345
|
+
result: list[tuple[str, int]] = []
|
|
346
|
+
body = class_node.child_by_field_name("body")
|
|
347
|
+
if body is None:
|
|
348
|
+
# PHP class body is in a 'declaration_list' child
|
|
349
|
+
for child in class_node.children:
|
|
350
|
+
if child.type == "declaration_list":
|
|
351
|
+
body = child
|
|
352
|
+
break
|
|
353
|
+
if body is None:
|
|
354
|
+
return result
|
|
355
|
+
|
|
356
|
+
for child in body.named_children:
|
|
357
|
+
if child.type == "property_declaration":
|
|
358
|
+
try:
|
|
359
|
+
_php_collect_property_decl(child, class_name, result)
|
|
360
|
+
except Exception: # noqa: BLE001
|
|
361
|
+
pass
|
|
362
|
+
|
|
363
|
+
return result
|
|
364
|
+
|
|
365
|
+
except Exception as exc: # noqa: BLE001
|
|
366
|
+
logger.debug("collect_field_symbols_php: failed for class %r: %r", class_name, exc)
|
|
367
|
+
return []
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _php_collect_property_decl(
|
|
371
|
+
prop_decl: Node,
|
|
372
|
+
class_name: str,
|
|
373
|
+
result: list[tuple[str, int]],
|
|
374
|
+
) -> None:
|
|
375
|
+
"""Extract field symbols from a single PHP property_declaration node.
|
|
376
|
+
|
|
377
|
+
PHP property_declaration structure:
|
|
378
|
+
visibility_modifier? type? property_element+ ';'
|
|
379
|
+
Each property_element contains a variable_name node ($field_name).
|
|
380
|
+
Strip the '$' prefix for the field symbol name.
|
|
381
|
+
"""
|
|
382
|
+
for child in prop_decl.children:
|
|
383
|
+
if child.type == "property_element":
|
|
384
|
+
for pc in child.children:
|
|
385
|
+
if pc.type == "variable_name":
|
|
386
|
+
raw_name = _text(pc).strip()
|
|
387
|
+
field_name = raw_name.lstrip("$")
|
|
388
|
+
if field_name:
|
|
389
|
+
qualified = f"{class_name}.{field_name}"
|
|
390
|
+
result.append((qualified, prop_decl.start_point[0] + 1))
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
394
|
+
# Swift field-access classification (A3 Slice 5)
|
|
395
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
396
|
+
#
|
|
397
|
+
# Swift stored-property access uses navigation_expression: self.prop or obj.prop.
|
|
398
|
+
# Call position: call_expression whose first child is a navigation_expression.
|
|
399
|
+
# → the navigation_expression is the callee → NOT a field edge.
|
|
400
|
+
# Assignment: Swift uses a dedicated 'assignment' node wrapping
|
|
401
|
+
# directly_assignable_expression (LHS) and the value (RHS).
|
|
402
|
+
# Augmented assignment: assignment node with an operator child (+=, -=, etc.).
|
|
403
|
+
#
|
|
404
|
+
# Receiver resolution:
|
|
405
|
+
# self_expression → enclosing class (EXTRACTED).
|
|
406
|
+
# Other vars: resolved via var_types lookup.
|
|
407
|
+
# Unresolvable: bare field name (AMBIGUOUS).
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def extract_field_accesses_swift(
|
|
411
|
+
func_body: Node,
|
|
412
|
+
source_fn: str,
|
|
413
|
+
class_name: str | None,
|
|
414
|
+
var_types: dict[str, str],
|
|
415
|
+
) -> list[FieldAccess]:
|
|
416
|
+
"""Extract field accesses (reads and writes) from a Swift function body.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
func_body: The 'function_body' or 'statements' Node of the function.
|
|
420
|
+
source_fn: Qualified name of the enclosing function, e.g. 'Account.deposit'.
|
|
421
|
+
class_name: Enclosing class/struct/actor name (None for top-level functions).
|
|
422
|
+
var_types: Scope map: param/local name → type name (pre-populated with class
|
|
423
|
+
properties via _scan_class_properties + _record_param_types).
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
List of (source_fn, target_field, mode, line) tuples.
|
|
427
|
+
target_field = 'ClassName.field' when self receiver; bare 'field' otherwise.
|
|
428
|
+
mode = 'reads' | 'writes'.
|
|
429
|
+
line = 1-based source line.
|
|
430
|
+
Returns [] on any error.
|
|
431
|
+
|
|
432
|
+
Never raises.
|
|
433
|
+
"""
|
|
434
|
+
result: list[FieldAccess] = []
|
|
435
|
+
try:
|
|
436
|
+
_swift_walk_body(func_body, source_fn, class_name, var_types, result)
|
|
437
|
+
except Exception as exc: # noqa: BLE001
|
|
438
|
+
logger.debug(
|
|
439
|
+
"extract_field_accesses_swift: failed for source=%r: %r", source_fn, exc
|
|
440
|
+
)
|
|
441
|
+
return result
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _swift_walk_body(
|
|
445
|
+
node: Node,
|
|
446
|
+
source_fn: str,
|
|
447
|
+
class_name: str | None,
|
|
448
|
+
var_types: dict[str, str],
|
|
449
|
+
result: list[FieldAccess],
|
|
450
|
+
) -> None:
|
|
451
|
+
"""Recursively walk a Swift function body collecting field accesses."""
|
|
452
|
+
# function_body contains a 'statements' child; handle both shapes.
|
|
453
|
+
for child in node.children:
|
|
454
|
+
_swift_walk_stmt(child, source_fn, class_name, var_types, result)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _swift_walk_stmt(
|
|
458
|
+
node: Node,
|
|
459
|
+
source_fn: str,
|
|
460
|
+
class_name: str | None,
|
|
461
|
+
var_types: dict[str, str],
|
|
462
|
+
result: list[FieldAccess],
|
|
463
|
+
) -> None:
|
|
464
|
+
"""Walk a single Swift statement/expression node for field accesses.
|
|
465
|
+
|
|
466
|
+
Handles:
|
|
467
|
+
assignment: LHS is directly_assignable_expression → extract write/read.
|
|
468
|
+
call_expression: first child is navigation_expression → NOT a field edge.
|
|
469
|
+
navigation_expression (standalone, not in call position) → read.
|
|
470
|
+
All other nodes: recurse collecting reads.
|
|
471
|
+
Skips nested function_declaration and class_declaration (own scope).
|
|
472
|
+
"""
|
|
473
|
+
t = node.type
|
|
474
|
+
|
|
475
|
+
# Skip nested scope-creating constructs.
|
|
476
|
+
if t in ("function_declaration", "class_declaration"):
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
# assignment: LHS directly_assignable_expression → write; RHS → reads.
|
|
480
|
+
if t == "assignment":
|
|
481
|
+
_swift_handle_assignment(node, source_fn, class_name, var_types, result)
|
|
482
|
+
return
|
|
483
|
+
|
|
484
|
+
# call_expression: first child is the callee. If callee is navigation_expression,
|
|
485
|
+
# it's a method call — NOT a field edge. Recurse into arguments only.
|
|
486
|
+
if t == "call_expression":
|
|
487
|
+
_swift_handle_call_expression(node, source_fn, class_name, var_types, result)
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
# A bare navigation_expression (not in call position) → read.
|
|
491
|
+
if t == "navigation_expression":
|
|
492
|
+
_swift_emit_read_if_not_in_call(node, source_fn, class_name, var_types, result)
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
# Recurse into all other nodes (return, if, for, while, switch, etc.).
|
|
496
|
+
for child in node.children:
|
|
497
|
+
_swift_walk_stmt(child, source_fn, class_name, var_types, result)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _swift_handle_assignment(
|
|
501
|
+
node: Node,
|
|
502
|
+
source_fn: str,
|
|
503
|
+
class_name: str | None,
|
|
504
|
+
var_types: dict[str, str],
|
|
505
|
+
result: list[FieldAccess],
|
|
506
|
+
) -> None:
|
|
507
|
+
"""Handle Swift assignment node: LHS navigation → write; RHS → reads.
|
|
508
|
+
|
|
509
|
+
Swift assignment node structure:
|
|
510
|
+
directly_assignable_expression operator value
|
|
511
|
+
where:
|
|
512
|
+
directly_assignable_expression wraps a navigation_expression for property access.
|
|
513
|
+
operator is '=' (plain) or '+=' etc. (augmented) — both are classified as WRITE.
|
|
514
|
+
"""
|
|
515
|
+
lhs = None
|
|
516
|
+
rhs = None
|
|
517
|
+
children = list(node.children)
|
|
518
|
+
for i, child in enumerate(children):
|
|
519
|
+
if child.type == "directly_assignable_expression":
|
|
520
|
+
lhs = child
|
|
521
|
+
elif lhs is not None and child.type not in (
|
|
522
|
+
"=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>="
|
|
523
|
+
):
|
|
524
|
+
# The value is after the operator — any non-operator child after LHS.
|
|
525
|
+
if child.type not in ("directly_assignable_expression",):
|
|
526
|
+
rhs = child
|
|
527
|
+
break
|
|
528
|
+
|
|
529
|
+
# LHS: look for navigation_expression inside directly_assignable_expression.
|
|
530
|
+
if lhs is not None:
|
|
531
|
+
nav = _swift_find_navigation_in_assignable(lhs)
|
|
532
|
+
if nav is not None:
|
|
533
|
+
acc = _swift_classify_navigation(nav, class_name, var_types)
|
|
534
|
+
if acc is not None:
|
|
535
|
+
target, _ = acc
|
|
536
|
+
result.append((source_fn, target, "writes", nav.start_point[0] + 1))
|
|
537
|
+
|
|
538
|
+
# RHS: collect reads.
|
|
539
|
+
if rhs is not None:
|
|
540
|
+
_swift_collect_reads_recursive(rhs, source_fn, class_name, var_types, result)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _swift_find_navigation_in_assignable(
|
|
544
|
+
node: Node,
|
|
545
|
+
) -> Node | None:
|
|
546
|
+
"""Find the navigation_expression inside a directly_assignable_expression.
|
|
547
|
+
|
|
548
|
+
Returns None if no navigation_expression is found (e.g. plain variable).
|
|
549
|
+
"""
|
|
550
|
+
if node.type == "navigation_expression":
|
|
551
|
+
return node
|
|
552
|
+
for child in node.children:
|
|
553
|
+
if child.type == "navigation_expression":
|
|
554
|
+
return child
|
|
555
|
+
return None
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _swift_handle_call_expression(
|
|
559
|
+
node: Node,
|
|
560
|
+
source_fn: str,
|
|
561
|
+
class_name: str | None,
|
|
562
|
+
var_types: dict[str, str],
|
|
563
|
+
result: list[FieldAccess],
|
|
564
|
+
) -> None:
|
|
565
|
+
"""Handle call_expression: skip navigation_expression callee; recurse into arguments.
|
|
566
|
+
|
|
567
|
+
A call_expression whose first child is a navigation_expression is a method call
|
|
568
|
+
(e.g. self.doWork()). We must NOT emit a field edge for the callee navigation.
|
|
569
|
+
However, arguments may contain field reads — recurse into the call_suffix.
|
|
570
|
+
"""
|
|
571
|
+
if not node.children:
|
|
572
|
+
return
|
|
573
|
+
|
|
574
|
+
for child in node.children:
|
|
575
|
+
if child.type == "call_suffix":
|
|
576
|
+
for sub in child.children:
|
|
577
|
+
_swift_walk_stmt(sub, source_fn, class_name, var_types, result)
|
|
578
|
+
elif child.type == "value_arguments":
|
|
579
|
+
for sub in child.children:
|
|
580
|
+
_swift_walk_stmt(sub, source_fn, class_name, var_types, result)
|
|
581
|
+
# Skip the callee (first child) — it's either a simple_identifier or
|
|
582
|
+
# navigation_expression (method call); either way, not a field read.
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _swift_emit_read_if_not_in_call(
|
|
586
|
+
node: Node,
|
|
587
|
+
source_fn: str,
|
|
588
|
+
class_name: str | None,
|
|
589
|
+
var_types: dict[str, str],
|
|
590
|
+
result: list[FieldAccess],
|
|
591
|
+
) -> None:
|
|
592
|
+
"""Emit a reads edge for a Swift navigation_expression NOT in call position.
|
|
593
|
+
|
|
594
|
+
A navigation_expression is in call position when its parent is a call_expression
|
|
595
|
+
AND it is the first child (callee). We skip those (method calls).
|
|
596
|
+
|
|
597
|
+
WHY first-child check instead of a named field: Swift's tree-sitter grammar does
|
|
598
|
+
not expose the callee of a call_expression as a named field (unlike Python 'function'
|
|
599
|
+
or TS 'function'). The callee is always the first child of the call_expression node.
|
|
600
|
+
We compare start_point (same stable identity rationale as Python/TS/Go) to handle
|
|
601
|
+
the fact that tree-sitter returns fresh Node objects on each child access.
|
|
602
|
+
"""
|
|
603
|
+
parent = node.parent
|
|
604
|
+
if parent is not None and parent.type == "call_expression":
|
|
605
|
+
if parent.children and parent.children[0].start_point == node.start_point:
|
|
606
|
+
return # Callee of a call expression — not a field read
|
|
607
|
+
|
|
608
|
+
acc = _swift_classify_navigation(node, class_name, var_types)
|
|
609
|
+
if acc is not None:
|
|
610
|
+
target, _ = acc
|
|
611
|
+
result.append((source_fn, target, "reads", node.start_point[0] + 1))
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _swift_collect_reads_recursive(
|
|
615
|
+
node: Node,
|
|
616
|
+
source_fn: str,
|
|
617
|
+
class_name: str | None,
|
|
618
|
+
var_types: dict[str, str],
|
|
619
|
+
result: list[FieldAccess],
|
|
620
|
+
) -> None:
|
|
621
|
+
"""Recursively collect reads from a Swift expression node."""
|
|
622
|
+
t = node.type
|
|
623
|
+
if t in ("function_declaration", "class_declaration"):
|
|
624
|
+
return
|
|
625
|
+
|
|
626
|
+
if t == "navigation_expression":
|
|
627
|
+
_swift_emit_read_if_not_in_call(node, source_fn, class_name, var_types, result)
|
|
628
|
+
# Do NOT recurse into navigation_expression children — the receiver part
|
|
629
|
+
# ('self' in 'self.balance') is not itself a field access target to re-emit.
|
|
630
|
+
return
|
|
631
|
+
|
|
632
|
+
if t == "call_expression":
|
|
633
|
+
# In RHS expressions, call arguments may contain field reads.
|
|
634
|
+
_swift_handle_call_expression(node, source_fn, class_name, var_types, result)
|
|
635
|
+
return
|
|
636
|
+
|
|
637
|
+
for child in node.children:
|
|
638
|
+
_swift_collect_reads_recursive(child, source_fn, class_name, var_types, result)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _swift_classify_navigation(
|
|
642
|
+
node: Node,
|
|
643
|
+
class_name: str | None,
|
|
644
|
+
var_types: dict[str, str],
|
|
645
|
+
) -> tuple[str, str | None] | None:
|
|
646
|
+
"""Resolve a Swift navigation_expression to (qualified_target, receiver_text).
|
|
647
|
+
|
|
648
|
+
Returns None when the node lacks the expected shape.
|
|
649
|
+
Returns (target, receiver_text) where:
|
|
650
|
+
- target = 'ClassName.field' when self_expression receiver (EXTRACTED)
|
|
651
|
+
- target = 'Type.field' when var_types resolves the receiver (EXTRACTED)
|
|
652
|
+
- target = bare 'field' when unresolvable (AMBIGUOUS)
|
|
653
|
+
receiver_text = raw receiver text (e.g. 'self', 'account')
|
|
654
|
+
|
|
655
|
+
Conservatism contract: NEVER emit a wrong qualified target.
|
|
656
|
+
Never raises (returns None on any exception).
|
|
657
|
+
"""
|
|
658
|
+
try:
|
|
659
|
+
children = list(node.children)
|
|
660
|
+
if not children:
|
|
661
|
+
return None
|
|
662
|
+
|
|
663
|
+
receiver_node = children[0]
|
|
664
|
+
nav_suffix = None
|
|
665
|
+
for child in children:
|
|
666
|
+
if child.type == "navigation_suffix":
|
|
667
|
+
nav_suffix = child
|
|
668
|
+
break
|
|
669
|
+
if nav_suffix is None:
|
|
670
|
+
return None
|
|
671
|
+
|
|
672
|
+
field_name = None
|
|
673
|
+
for child in nav_suffix.children:
|
|
674
|
+
if child.type == "simple_identifier":
|
|
675
|
+
field_name = _text(child)
|
|
676
|
+
break
|
|
677
|
+
if not field_name:
|
|
678
|
+
return None
|
|
679
|
+
|
|
680
|
+
receiver_text = _text(receiver_node)
|
|
681
|
+
|
|
682
|
+
# self_expression → enclosing class (EXTRACTED).
|
|
683
|
+
if receiver_node.type == "self_expression":
|
|
684
|
+
if class_name:
|
|
685
|
+
return f"{class_name}.{field_name}", receiver_text
|
|
686
|
+
else:
|
|
687
|
+
return field_name, receiver_text
|
|
688
|
+
|
|
689
|
+
# Try var_types lookup for other receivers.
|
|
690
|
+
resolved_type: str | None = None
|
|
691
|
+
if receiver_text and receiver_text in var_types:
|
|
692
|
+
resolved_type = var_types[receiver_text]
|
|
693
|
+
|
|
694
|
+
if resolved_type is not None:
|
|
695
|
+
return f"{resolved_type}.{field_name}", receiver_text
|
|
696
|
+
else:
|
|
697
|
+
return field_name, receiver_text
|
|
698
|
+
|
|
699
|
+
except Exception as exc: # noqa: BLE001
|
|
700
|
+
logger.debug("_swift_classify_navigation: failed: %r", exc)
|
|
701
|
+
return None
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def collect_field_symbols_swift(
|
|
705
|
+
class_node: Node,
|
|
706
|
+
class_name: str,
|
|
707
|
+
) -> list[tuple[str, int]]:
|
|
708
|
+
"""Collect (qualified_field_name, line) pairs from a Swift class/struct/actor node.
|
|
709
|
+
|
|
710
|
+
Returns field symbols for ALL stored property_declaration nodes in the class_body.
|
|
711
|
+
A stored property has no computed body (no '{' block directly in the declaration).
|
|
712
|
+
|
|
713
|
+
Returns [] on any error. Never raises.
|
|
714
|
+
"""
|
|
715
|
+
try:
|
|
716
|
+
result: list[tuple[str, int]] = []
|
|
717
|
+
|
|
718
|
+
body = None
|
|
719
|
+
for child in class_node.children:
|
|
720
|
+
if child.type == "class_body":
|
|
721
|
+
body = child
|
|
722
|
+
break
|
|
723
|
+
if body is None:
|
|
724
|
+
return result
|
|
725
|
+
|
|
726
|
+
for child in body.children:
|
|
727
|
+
if child.type == "property_declaration":
|
|
728
|
+
try:
|
|
729
|
+
_swift_collect_property_decl(child, class_name, result)
|
|
730
|
+
except Exception: # noqa: BLE001
|
|
731
|
+
pass
|
|
732
|
+
|
|
733
|
+
return result
|
|
734
|
+
|
|
735
|
+
except Exception as exc: # noqa: BLE001
|
|
736
|
+
logger.debug(
|
|
737
|
+
"collect_field_symbols_swift: failed for class %r: %r", class_name, exc
|
|
738
|
+
)
|
|
739
|
+
return []
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def _swift_collect_property_decl(
|
|
743
|
+
prop_decl: Node,
|
|
744
|
+
class_name: str,
|
|
745
|
+
result: list[tuple[str, int]],
|
|
746
|
+
) -> None:
|
|
747
|
+
"""Extract a field symbol from a Swift property_declaration node.
|
|
748
|
+
|
|
749
|
+
Skip computed properties (they have a computed_property, computed_value_body,
|
|
750
|
+
or code_block child). Only stored properties (with no computed body) are indexed.
|
|
751
|
+
"""
|
|
752
|
+
# Check for computed property — skip those.
|
|
753
|
+
for child in prop_decl.children:
|
|
754
|
+
if child.type in ("computed_property", "computed_value_body", "code_block"):
|
|
755
|
+
return
|
|
756
|
+
|
|
757
|
+
name_node = None
|
|
758
|
+
for child in prop_decl.children:
|
|
759
|
+
if child.type == "pattern":
|
|
760
|
+
for gc in child.children:
|
|
761
|
+
if gc.type == "simple_identifier":
|
|
762
|
+
name_node = gc
|
|
763
|
+
break
|
|
764
|
+
break
|
|
765
|
+
|
|
766
|
+
if name_node is None:
|
|
767
|
+
return
|
|
768
|
+
|
|
769
|
+
field_name = _text(name_node).strip()
|
|
770
|
+
if not field_name:
|
|
771
|
+
return
|
|
772
|
+
|
|
773
|
+
qualified = f"{class_name}.{field_name}"
|
|
774
|
+
result.append((qualified, prop_decl.start_point[0] + 1))
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
778
|
+
# Swift field-access emission helpers for graph_swift.py
|
|
779
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
780
|
+
#
|
|
781
|
+
# These helpers encapsulate the field-access edge + field-symbol emission wiring
|
|
782
|
+
# that was previously inline in graph_swift.py. Moving them here reduces
|
|
783
|
+
# graph_swift.py below the 1000-line limit.
|
|
784
|
+
#
|
|
785
|
+
# Both functions are PURE CONSTRUCTORS: they return new lists and never mutate
|
|
786
|
+
# their inputs. graph_swift.py extends its own edge/symbol lists with the results.
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def emit_swift_field_access_edges(
|
|
790
|
+
func_node: Node,
|
|
791
|
+
file_str: str,
|
|
792
|
+
class_name: str,
|
|
793
|
+
var_types: dict[str, str],
|
|
794
|
+
) -> list[Edge]:
|
|
795
|
+
"""Build reads/writes field-access edges for a Swift function_declaration.
|
|
796
|
+
|
|
797
|
+
Finds the function_body child and runs extract_field_accesses_swift on it.
|
|
798
|
+
The source_fn is derived from the function's simple_identifier child.
|
|
799
|
+
Returns a (possibly empty) list of Edge TypedDicts.
|
|
800
|
+
|
|
801
|
+
WHY this lives in field_access_php_swift instead of graph_swift:
|
|
802
|
+
graph_swift.py reached the 1000-line limit. These emission helpers share the same
|
|
803
|
+
layer as the Swift extractor (leaf), so moving them here keeps both files under
|
|
804
|
+
the limit without introducing a new dependency direction. graph_swift.py calls
|
|
805
|
+
this function directly via `from seam.indexer.field_access_ext2 import ...`.
|
|
806
|
+
|
|
807
|
+
All emitted edges carry confidence='INFERRED' — receiver-type inference for Swift
|
|
808
|
+
fields is extraction-time heuristic (self → class is certain; var_types lookup is
|
|
809
|
+
scope-annotation-based). INFERRED is the correct label for the whole edge regardless
|
|
810
|
+
of how confident the individual receiver resolution was.
|
|
811
|
+
|
|
812
|
+
Never raises.
|
|
813
|
+
"""
|
|
814
|
+
try:
|
|
815
|
+
func_name = None
|
|
816
|
+
for child in func_node.children:
|
|
817
|
+
if child.type == "simple_identifier":
|
|
818
|
+
func_name = _text(child)
|
|
819
|
+
break
|
|
820
|
+
if not func_name:
|
|
821
|
+
return []
|
|
822
|
+
|
|
823
|
+
source_fn = f"{class_name}.{func_name}"
|
|
824
|
+
|
|
825
|
+
func_body = None
|
|
826
|
+
for child in func_node.children:
|
|
827
|
+
if child.type == "function_body":
|
|
828
|
+
func_body = child
|
|
829
|
+
break
|
|
830
|
+
if func_body is None:
|
|
831
|
+
return []
|
|
832
|
+
|
|
833
|
+
edges: list[Edge] = []
|
|
834
|
+
for src, tgt, mode, line in extract_field_accesses_swift(
|
|
835
|
+
func_body, source_fn, class_name, var_types
|
|
836
|
+
):
|
|
837
|
+
edges.append(Edge(
|
|
838
|
+
source=src,
|
|
839
|
+
target=tgt,
|
|
840
|
+
kind=mode,
|
|
841
|
+
file=file_str,
|
|
842
|
+
line=line,
|
|
843
|
+
confidence="INFERRED",
|
|
844
|
+
receiver=None,
|
|
845
|
+
))
|
|
846
|
+
return edges
|
|
847
|
+
|
|
848
|
+
except Exception as exc: # noqa: BLE001
|
|
849
|
+
logger.debug(
|
|
850
|
+
"emit_swift_field_access_edges: failed for class=%r: %r", class_name, exc
|
|
851
|
+
)
|
|
852
|
+
return []
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def emit_swift_field_symbols(
|
|
856
|
+
class_node: Node,
|
|
857
|
+
class_name: str,
|
|
858
|
+
file_str: str,
|
|
859
|
+
) -> list[Symbol]:
|
|
860
|
+
"""Build field-kind Symbol TypedDicts for a Swift class/struct/actor node.
|
|
861
|
+
|
|
862
|
+
Calls collect_field_symbols_swift and wraps each result as a Symbol.
|
|
863
|
+
Returns a (possibly empty) list of Symbol TypedDicts.
|
|
864
|
+
|
|
865
|
+
Never raises.
|
|
866
|
+
"""
|
|
867
|
+
try:
|
|
868
|
+
symbols: list[Symbol] = []
|
|
869
|
+
for qual_name, field_line in collect_field_symbols_swift(class_node, class_name):
|
|
870
|
+
symbols.append(Symbol(
|
|
871
|
+
name=qual_name,
|
|
872
|
+
kind="field",
|
|
873
|
+
file=file_str,
|
|
874
|
+
start_line=field_line,
|
|
875
|
+
end_line=field_line,
|
|
876
|
+
docstring=None,
|
|
877
|
+
signature=None,
|
|
878
|
+
decorators=[],
|
|
879
|
+
is_exported=None,
|
|
880
|
+
visibility=None,
|
|
881
|
+
qualified_name=qual_name,
|
|
882
|
+
))
|
|
883
|
+
return symbols
|
|
884
|
+
except Exception as exc: # noqa: BLE001
|
|
885
|
+
logger.debug(
|
|
886
|
+
"emit_swift_field_symbols: failed for class=%r: %r", class_name, exc
|
|
887
|
+
)
|
|
888
|
+
return []
|