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.
Files changed (109) hide show
  1. seam/__init__.py +3 -0
  2. seam/_data/schema.sql +225 -0
  3. seam/_web/assets/index-BL_tqprR.js +216 -0
  4. seam/_web/assets/index-GTKUhVyD.css +1 -0
  5. seam/_web/index.html +13 -0
  6. seam/analysis/__init__.py +14 -0
  7. seam/analysis/affected.py +254 -0
  8. seam/analysis/builtins.py +966 -0
  9. seam/analysis/byte_budget.py +217 -0
  10. seam/analysis/changes.py +709 -0
  11. seam/analysis/cluster_naming.py +260 -0
  12. seam/analysis/clustering.py +216 -0
  13. seam/analysis/confidence.py +699 -0
  14. seam/analysis/embeddings.py +195 -0
  15. seam/analysis/flows.py +708 -0
  16. seam/analysis/impact.py +444 -0
  17. seam/analysis/imports.py +994 -0
  18. seam/analysis/imports_ext.py +780 -0
  19. seam/analysis/imports_resolve.py +176 -0
  20. seam/analysis/processes.py +453 -0
  21. seam/analysis/relevance.py +155 -0
  22. seam/analysis/rwr.py +129 -0
  23. seam/analysis/staleness.py +328 -0
  24. seam/analysis/steer.py +282 -0
  25. seam/analysis/synthesis.py +253 -0
  26. seam/analysis/synthesis_channels.py +433 -0
  27. seam/analysis/testpaths.py +103 -0
  28. seam/analysis/traversal.py +470 -0
  29. seam/cli/__init__.py +0 -0
  30. seam/cli/install.py +232 -0
  31. seam/cli/main.py +2602 -0
  32. seam/cli/output.py +137 -0
  33. seam/cli/read.py +244 -0
  34. seam/cli/serve.py +145 -0
  35. seam/config.py +551 -0
  36. seam/indexer/__init__.py +0 -0
  37. seam/indexer/cluster_index.py +425 -0
  38. seam/indexer/db.py +496 -0
  39. seam/indexer/embedding_index.py +183 -0
  40. seam/indexer/field_access.py +536 -0
  41. seam/indexer/field_access_c_cpp.py +643 -0
  42. seam/indexer/field_access_ext.py +708 -0
  43. seam/indexer/field_access_ext2.py +408 -0
  44. seam/indexer/field_access_go_rust.py +737 -0
  45. seam/indexer/field_access_php_swift.py +888 -0
  46. seam/indexer/field_access_ts.py +626 -0
  47. seam/indexer/graph.py +321 -0
  48. seam/indexer/graph_c.py +562 -0
  49. seam/indexer/graph_c_cpp.py +39 -0
  50. seam/indexer/graph_common.py +644 -0
  51. seam/indexer/graph_cpp.py +615 -0
  52. seam/indexer/graph_csharp.py +651 -0
  53. seam/indexer/graph_go.py +723 -0
  54. seam/indexer/graph_go_rust.py +39 -0
  55. seam/indexer/graph_java.py +689 -0
  56. seam/indexer/graph_java_csharp.py +38 -0
  57. seam/indexer/graph_php.py +914 -0
  58. seam/indexer/graph_python.py +628 -0
  59. seam/indexer/graph_ruby.py +748 -0
  60. seam/indexer/graph_rust.py +653 -0
  61. seam/indexer/graph_scope_infer.py +902 -0
  62. seam/indexer/graph_scope_infer_ext.py +723 -0
  63. seam/indexer/graph_scope_infer_ext2.py +992 -0
  64. seam/indexer/graph_swift.py +1014 -0
  65. seam/indexer/graph_swift_infer.py +515 -0
  66. seam/indexer/graph_typescript.py +663 -0
  67. seam/indexer/migrations.py +816 -0
  68. seam/indexer/parser.py +204 -0
  69. seam/indexer/pipeline.py +197 -0
  70. seam/indexer/signatures.py +634 -0
  71. seam/indexer/signatures_ext.py +780 -0
  72. seam/indexer/sync.py +287 -0
  73. seam/indexer/synthesis_index.py +291 -0
  74. seam/indexer/tokenize.py +79 -0
  75. seam/installer/__init__.py +67 -0
  76. seam/installer/claude.py +97 -0
  77. seam/installer/codex.py +94 -0
  78. seam/installer/core.py +127 -0
  79. seam/installer/cursor.py +61 -0
  80. seam/installer/guide.py +110 -0
  81. seam/installer/jsonfile.py +85 -0
  82. seam/installer/markdownfile.py +146 -0
  83. seam/installer/tomlfile.py +72 -0
  84. seam/query/__init__.py +0 -0
  85. seam/query/clusters.py +206 -0
  86. seam/query/comments.py +217 -0
  87. seam/query/context.py +293 -0
  88. seam/query/engine.py +940 -0
  89. seam/query/fts.py +328 -0
  90. seam/query/names.py +470 -0
  91. seam/query/pack.py +433 -0
  92. seam/query/semantic.py +339 -0
  93. seam/query/structure.py +727 -0
  94. seam/server/__init__.py +0 -0
  95. seam/server/graph_api.py +437 -0
  96. seam/server/handler_common.py +323 -0
  97. seam/server/impact_handler.py +615 -0
  98. seam/server/mcp.py +556 -0
  99. seam/server/tools.py +697 -0
  100. seam/server/trace_handler.py +184 -0
  101. seam/server/web.py +922 -0
  102. seam/watcher/__init__.py +0 -0
  103. seam/watcher/__main__.py +56 -0
  104. seam/watcher/daemon.py +237 -0
  105. seam_code-0.3.0.dist-info/METADATA +318 -0
  106. seam_code-0.3.0.dist-info/RECORD +109 -0
  107. seam_code-0.3.0.dist-info/WHEEL +4 -0
  108. seam_code-0.3.0.dist-info/entry_points.txt +2 -0
  109. 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 []