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,1014 @@
1
+ """Swift symbol, edge, and comment extraction from tree-sitter ASTs.
2
+
3
+ LAYER: imports from graph_common (leaf) only — never from graph.py.
4
+
5
+ LAYERING:
6
+ graph_common (leaf — no seam deps)
7
+
8
+ graph_swift (this file)
9
+
10
+ graph.py (imports this module's public extractors at top level)
11
+
12
+ WHY separate from graph.py: graph.py would exceed 1000 lines with Swift inside.
13
+ Keeping a per-family extractor module follows the Phase 9 precedent.
14
+
15
+ All extractor functions follow the same contract:
16
+ - Accept a tree-sitter Node + filepath.
17
+ - Return a list — never raise, never return None.
18
+ - Edges carry confidence='INFERRED' by default.
19
+
20
+ Verified grammar facts (tree-sitter-swift 0.7.3, tree-sitter 0.25.2):
21
+ - import_declaration → identifier → simple_identifier segments.
22
+ - class_declaration represents class/struct/actor/extension/enum,
23
+ distinguished by keyword child type.
24
+ - protocol_declaration → kind=interface; protocol_function_declaration inside.
25
+ - function_declaration: top-level → function; inside class_body → method.
26
+ - call_expression: bare simple_identifier callee → emit call edge.
27
+ navigation_expression callee (obj.m, self.m) → resolved to a qualified
28
+ 'Type.method' edge when the receiver type is known (P5 type inference),
29
+ else SKIPPED (unknown receiver — never emit a wrong/global-name edge).
30
+ - Comments: 'comment' node for both // and ///; 'multiline_comment' for /* */.
31
+ """
32
+
33
+ import logging
34
+ from pathlib import Path
35
+
36
+ from tree_sitter import Node
37
+
38
+ import seam.config as config
39
+
40
+ # A3 Slice 5: Swift field-access edges + field symbols.
41
+ # The emission helpers (emit_swift_field_access_edges / emit_swift_field_symbols) are
42
+ # in field_access_php_swift (moved there in the A3 refactor to keep graph_swift.py
43
+ # under the 1000-line limit). field_access_ext2 re-exports the core functions.
44
+ from seam.indexer.field_access_php_swift import (
45
+ emit_swift_field_access_edges,
46
+ emit_swift_field_symbols,
47
+ )
48
+
49
+ # All shared types, constants, and helpers from the leaf module.
50
+ from seam.indexer.graph_common import (
51
+ Edge,
52
+ Symbol,
53
+ _find_enclosing_function,
54
+ _make_symbol,
55
+ _text,
56
+ )
57
+
58
+ # Pure receiver-type inference + comment extraction helpers live in a leaf module to
59
+ # keep this file under the 1000-line limit (see graph_swift_infer for the rationale).
60
+ # _extract_comments_swift was moved to graph_swift_infer in the A3 refactor.
61
+ from seam.indexer.graph_swift_infer import (
62
+ _extract_comments_swift, # noqa: F401 — re-exported for graph.py
63
+ _record_param_types,
64
+ _record_var_binding,
65
+ _resolve_navigation_target,
66
+ _scan_class_properties,
67
+ collect_composition_types_swift,
68
+ collect_param_types_swift,
69
+ )
70
+
71
+ # signatures.py is a leaf (no seam deps) so importing it here does not create a cycle.
72
+ from seam.indexer.signatures import extract_node_fields
73
+
74
+ logger = logging.getLogger(__name__)
75
+
76
+ # ── Doc-comment adjacency helper ───────────────────────────────────────────────
77
+
78
+
79
+ def _clean_swift_block_doc(raw: str) -> str | None:
80
+ """Strip /** */ delimiters + per-line leading '*' from a Swift block doc-comment.
81
+
82
+ WHY a dedicated cleaner: tree-sitter-swift emits a whole /** ... */ block as ONE
83
+ 'multiline_comment' node (unlike /// which is many 'comment' nodes), so it needs
84
+ line-by-line de-decoration rather than the prefix-strip used for ///.
85
+ """
86
+ inner = raw
87
+ if inner.startswith("/**"):
88
+ inner = inner[3:]
89
+ if inner.endswith("*/"):
90
+ inner = inner[:-2]
91
+ cleaned = [
92
+ stripped for line in inner.splitlines() if (stripped := line.strip().lstrip("*").strip())
93
+ ]
94
+ text = "\n".join(cleaned).strip()
95
+ return text or None
96
+
97
+
98
+ def _swift_doc_comment(decl_node: Node) -> str | None:
99
+ """Capture Swift doc-comment: a /** */ block OR contiguous /// lines above a decl.
100
+
101
+ tree-sitter-swift uses TWO node types: 'comment' for // and /// (one node per line),
102
+ and 'multiline_comment' for /* */ and /** */ (one node per block). Only '///' lines
103
+ and '/**' blocks qualify as doc-comments (per PRD user-story 9); plain // and /* */
104
+ do not. The /** */ block is checked first since it is a single prev_sibling node.
105
+
106
+ Adjacency rule: comment end_point[0] + 1 == next_node start_point[0].
107
+ Swift comment nodes do NOT include trailing newline in text (same as Go),
108
+ so end_point[0] is the comment's last visible row. A blank-line gap therefore
109
+ means end_point[0] + 1 != next_start_point[0] and breaks attachment.
110
+ """
111
+ current = decl_node.prev_sibling
112
+
113
+ # /** ... */ block doc-comment — a single 'multiline_comment' node. Checked before
114
+ # the /// loop because it is one node, not a run of sibling 'comment' lines. A plain
115
+ # /* */ block (single star) is NOT a doc-comment, so require the '/**' prefix.
116
+ if current is not None and current.type == "multiline_comment":
117
+ raw = _text(current)
118
+ if raw.startswith("/**"):
119
+ next_node = current.next_sibling
120
+ if next_node is None or current.end_point[0] + 1 == next_node.start_point[0]:
121
+ return _clean_swift_block_doc(raw)
122
+ return None
123
+
124
+ lines: list[str] = []
125
+
126
+ while current is not None and current.type == "comment":
127
+ raw = _text(current)
128
+ # Only /// (outer doc-comment) qualifies — // does not.
129
+ if not raw.startswith("///"):
130
+ break
131
+ # Adjacency check: no blank line between comment and next declaration.
132
+ next_node = current.next_sibling
133
+ if next_node is not None:
134
+ end_row = current.end_point[0]
135
+ next_start_row = next_node.start_point[0]
136
+ if end_row + 1 != next_start_row:
137
+ break
138
+ # Strip '///' prefix and normalize whitespace.
139
+ body = raw[3:].strip()
140
+ lines.append(body)
141
+ current = current.prev_sibling
142
+
143
+ if not lines:
144
+ return None
145
+
146
+ # Lines were collected bottom-up; reverse to restore source order.
147
+ return "\n".join(reversed(lines))
148
+
149
+
150
+ # ── Symbol extraction helpers ─────────────────────────────────────────────────
151
+
152
+
153
+ def _swift_class_keyword(decl_node: Node) -> str | None:
154
+ """Return the keyword child type for a class_declaration node.
155
+
156
+ Returns 'class', 'struct', 'actor', 'extension', or 'enum' (or None if absent).
157
+ This distinguishes the five forms that all use class_declaration in the grammar.
158
+ """
159
+ for child in decl_node.children:
160
+ if child.type in ("class", "struct", "actor", "extension", "enum"):
161
+ return child.type
162
+ return None
163
+
164
+
165
+ def _swift_extension_type_name(decl_node: Node) -> str | None:
166
+ """Extract the extended type name from an 'extension TypeName { ... }' node.
167
+
168
+ The extended type name is the type_identifier inside the first user_type child
169
+ that immediately follows the 'extension' keyword (NOT inside class_body).
170
+
171
+ WHY not child_by_field_name('name'): extension nodes have no 'name' field —
172
+ the extended type is stored in a user_type child, not a named field.
173
+ """
174
+ found_ext_kw = False
175
+ for child in decl_node.children:
176
+ if child.type == "extension":
177
+ found_ext_kw = True
178
+ continue
179
+ # The user_type directly after 'extension' holds the extended name.
180
+ if found_ext_kw and child.type == "user_type":
181
+ for gc in child.children:
182
+ if gc.type == "type_identifier":
183
+ return _text(gc)
184
+ # Stop at class_body — the name was not found before the body.
185
+ if child.type == "class_body":
186
+ break
187
+ return None
188
+
189
+
190
+ def _swift_modifiers(node: Node) -> Node | None:
191
+ """Find the 'modifiers' child of a Swift declaration node, or None."""
192
+ for child in node.children:
193
+ if child.type == "modifiers":
194
+ return child
195
+ return None
196
+
197
+
198
+ def _swift_visibility_and_exported(node: Node) -> tuple[str, bool]:
199
+ """Extract (visibility, is_exported) from a Swift declaration node.
200
+
201
+ Scans the modifiers child for visibility_modifier keyword text:
202
+ public / open → ('public', True)
203
+ private / fileprivate → ('private', False)
204
+ internal (or absent) → ('internal', False)
205
+
206
+ WHY 'internal' for absent: Swift's default access level is 'internal'.
207
+ Only public/open actually crosses the module boundary (is_exported=True).
208
+ """
209
+ mods = _swift_modifiers(node)
210
+ if mods is None:
211
+ return ("internal", False)
212
+ for child in mods.children:
213
+ if child.type == "visibility_modifier":
214
+ vis_text = _text(child).strip()
215
+ if vis_text in ("public", "open"):
216
+ return ("public", True)
217
+ if vis_text in ("private", "fileprivate"):
218
+ return ("private", False)
219
+ # 'internal' explicitly written
220
+ return ("internal", False)
221
+ return ("internal", False)
222
+
223
+
224
+ def _swift_attributes(node: Node) -> list[str]:
225
+ """Extract Swift @attribute decorator texts from a declaration node.
226
+
227
+ Looks inside the modifiers child for 'attribute' nodes.
228
+ Each attribute is returned as its verbatim text (e.g. '@objc', '@available(iOS 13.0, *)').
229
+ """
230
+ result: list[str] = []
231
+ mods = _swift_modifiers(node)
232
+ if mods is None:
233
+ return result
234
+ try:
235
+ for child in mods.children:
236
+ if child.type == "attribute":
237
+ text = _text(child).strip()
238
+ if text:
239
+ result.append(text)
240
+ except Exception: # noqa: BLE001
241
+ pass
242
+ return result
243
+
244
+
245
+ def _swift_signature(node: Node) -> str | None:
246
+ """Build a one-line Swift signature from a declaration node.
247
+
248
+ Strategy: collect text from all children BEFORE the body (function_body,
249
+ class_body, enum_class_body, protocol_body), join, and normalize to one line.
250
+ Skip modifiers child (those are decorators / visibility, not sig header text)
251
+ except for the visibility_modifier which IS part of the sig (e.g. 'public').
252
+
253
+ Covers: function_declaration, class_declaration, protocol_declaration,
254
+ protocol_function_declaration.
255
+ """
256
+ try:
257
+ body_stop_types = frozenset(
258
+ {
259
+ "function_body",
260
+ "class_body",
261
+ "enum_class_body",
262
+ "protocol_body",
263
+ }
264
+ )
265
+ parts: list[str] = []
266
+ for child in node.children:
267
+ if child.type in body_stop_types:
268
+ break
269
+ if child.type == "modifiers":
270
+ # Include only visibility_modifier in signature, skip @attributes.
271
+ for mc in child.children:
272
+ if mc.type == "visibility_modifier":
273
+ vis = _text(mc).strip()
274
+ if vis:
275
+ parts.append(vis)
276
+ continue
277
+ text = _text(child).strip()
278
+ if text:
279
+ parts.append(text)
280
+ if not parts:
281
+ return None
282
+ raw = " ".join(parts)
283
+ # Collapse whitespace.
284
+ return " ".join(raw.split())
285
+ except Exception: # noqa: BLE001
286
+ pass
287
+ return None
288
+
289
+
290
+ def _truncate_sig(sig: str | None, max_len: int) -> str | None:
291
+ """Truncate signature to max_len chars, appending '...' if needed."""
292
+ if sig is None or len(sig) <= max_len:
293
+ return sig
294
+ return sig[: max_len - 3] + "..."
295
+
296
+
297
+ # ── Swift symbol extraction ────────────────────────────────────────────────────
298
+
299
+
300
+ def _extract_symbols_swift(root: Node, filepath: Path) -> list[Symbol]:
301
+ """Walk a Swift AST and extract function, method, class, struct, enum, protocol symbols.
302
+
303
+ Kind mapping (closed vocabulary):
304
+ class_declaration keyword 'class'/'struct'/'actor' → class
305
+ class_declaration keyword 'extension' → class (extended type name as symbol name)
306
+ class_declaration keyword 'enum' (body=enum_class_body) → type
307
+ protocol_declaration → interface
308
+ function_declaration top-level → function
309
+ function_declaration inside class_body → method (qualified as 'Type.method')
310
+ protocol_function_declaration → method (qualified as 'Proto.method')
311
+
312
+ Never raises: the outer try/except + logger.debug is the final backstop.
313
+ """
314
+ symbols: list[Symbol] = []
315
+ file_str = str(filepath)
316
+
317
+ try:
318
+ _walk_top_level(root, file_str, symbols)
319
+ except Exception as exc: # noqa: BLE001
320
+ logger.debug("_extract_symbols_swift: unhandled exception for %s: %r", filepath, exc)
321
+
322
+ return symbols
323
+
324
+
325
+ def _walk_top_level(root: Node, file_str: str, symbols: list[Symbol]) -> None:
326
+ """Walk top-level children of the source_file node."""
327
+ for child in root.children:
328
+ _visit_top(child, file_str, symbols)
329
+
330
+
331
+ def _visit_top(node: Node, file_str: str, symbols: list[Symbol]) -> None:
332
+ """Visit a top-level AST node and emit symbols."""
333
+ if node.type == "function_declaration":
334
+ _handle_function(node, file_str, symbols, class_name=None)
335
+
336
+ elif node.type == "class_declaration":
337
+ keyword = _swift_class_keyword(node)
338
+ if keyword in ("class", "struct", "actor"):
339
+ _handle_class_like(node, file_str, symbols, keyword)
340
+ elif keyword == "extension":
341
+ _handle_extension(node, file_str, symbols)
342
+ elif keyword == "enum":
343
+ _handle_enum(node, file_str, symbols)
344
+
345
+ elif node.type == "protocol_declaration":
346
+ _handle_protocol(node, file_str, symbols)
347
+
348
+
349
+ def _handle_function(
350
+ node: Node,
351
+ file_str: str,
352
+ symbols: list[Symbol],
353
+ class_name: str | None,
354
+ ) -> None:
355
+ """Emit a function or method symbol from a function_declaration node.
356
+
357
+ Top-level → kind='function'; inside a class body → kind='method' (qualified).
358
+ Never raises.
359
+ """
360
+ try:
361
+ # Simple name from simple_identifier child (function_declaration has no 'name' field)
362
+ name = None
363
+ for child in node.children:
364
+ if child.type == "simple_identifier":
365
+ name = _text(child)
366
+ break
367
+ if not name:
368
+ return
369
+
370
+ if class_name:
371
+ kind = "method"
372
+ qualified = f"{class_name}.{name}"
373
+ else:
374
+ kind = "function"
375
+ qualified = name
376
+
377
+ doc = _swift_doc_comment(node)
378
+ fields = extract_node_fields(
379
+ node,
380
+ "swift",
381
+ qualified_name=qualified,
382
+ max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
383
+ )
384
+ symbols.append(
385
+ _make_symbol(
386
+ qualified,
387
+ kind,
388
+ file_str,
389
+ node,
390
+ doc,
391
+ signature=fields["signature"],
392
+ decorators=fields["decorators"],
393
+ is_exported=fields["is_exported"],
394
+ visibility=fields["visibility"],
395
+ qualified_name=qualified,
396
+ )
397
+ )
398
+ except Exception as exc: # noqa: BLE001
399
+ logger.debug("_handle_function: failed for node at %r: %r", node.start_point, exc)
400
+
401
+
402
+ def _handle_class_like(
403
+ node: Node,
404
+ file_str: str,
405
+ symbols: list[Symbol],
406
+ keyword: str,
407
+ ) -> None:
408
+ """Emit a class/struct/actor symbol and recurse into its body for methods.
409
+
410
+ Always kind='class'. The name comes from the type_identifier child.
411
+ Recurses into class_body to find method function_declaration nodes.
412
+ Never raises.
413
+ """
414
+ try:
415
+ name_node = node.child_by_field_name("name")
416
+ if name_node is None:
417
+ # Fallback: find type_identifier directly among children
418
+ for child in node.children:
419
+ if child.type == "type_identifier":
420
+ name_node = child
421
+ break
422
+ if name_node is None:
423
+ return
424
+
425
+ class_name = _text(name_node)
426
+ doc = _swift_doc_comment(node)
427
+ fields = extract_node_fields(
428
+ node,
429
+ "swift",
430
+ qualified_name=class_name,
431
+ max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
432
+ )
433
+ symbols.append(
434
+ _make_symbol(
435
+ class_name,
436
+ "class",
437
+ file_str,
438
+ node,
439
+ doc,
440
+ signature=fields["signature"],
441
+ decorators=fields["decorators"],
442
+ is_exported=fields["is_exported"],
443
+ visibility=fields["visibility"],
444
+ qualified_name=class_name,
445
+ )
446
+ )
447
+
448
+ # A3 Slice 5: emit field symbols for Swift stored var/let properties.
449
+ if config.SEAM_FIELD_ACCESS_EDGES == "on":
450
+ symbols.extend(emit_swift_field_symbols(node, class_name, file_str))
451
+
452
+ # Recurse into class_body for methods
453
+ body = node.child_by_field_name("body")
454
+ if body is None:
455
+ for child in node.children:
456
+ if child.type == "class_body":
457
+ body = child
458
+ break
459
+ if body is not None:
460
+ for child in body.children:
461
+ if child.type == "function_declaration":
462
+ _handle_function(child, file_str, symbols, class_name=class_name)
463
+
464
+ except Exception as exc: # noqa: BLE001
465
+ logger.debug("_handle_class_like: failed at %r: %r", node.start_point, exc)
466
+
467
+
468
+ def _handle_extension(node: Node, file_str: str, symbols: list[Symbol]) -> None:
469
+ """Emit an extension symbol (kind='class') and its method symbols.
470
+
471
+ The extended type name is the type_identifier inside the user_type child
472
+ that follows the 'extension' keyword. Methods are qualified 'Type.method'.
473
+ Never raises.
474
+ """
475
+ try:
476
+ ext_name = _swift_extension_type_name(node)
477
+ if ext_name is None:
478
+ return
479
+
480
+ doc = _swift_doc_comment(node)
481
+ fields = extract_node_fields(
482
+ node,
483
+ "swift",
484
+ qualified_name=ext_name,
485
+ max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
486
+ )
487
+ symbols.append(
488
+ _make_symbol(
489
+ ext_name,
490
+ "class",
491
+ file_str,
492
+ node,
493
+ doc,
494
+ signature=fields["signature"],
495
+ decorators=fields["decorators"],
496
+ is_exported=fields["is_exported"],
497
+ visibility=fields["visibility"],
498
+ qualified_name=ext_name,
499
+ )
500
+ )
501
+
502
+ # Recurse into class_body for methods
503
+ for child in node.children:
504
+ if child.type == "class_body":
505
+ for gc in child.children:
506
+ if gc.type == "function_declaration":
507
+ _handle_function(gc, file_str, symbols, class_name=ext_name)
508
+ break
509
+
510
+ except Exception as exc: # noqa: BLE001
511
+ logger.debug("_handle_extension: failed at %r: %r", node.start_point, exc)
512
+
513
+
514
+ def _handle_enum(node: Node, file_str: str, symbols: list[Symbol]) -> None:
515
+ """Emit an enum symbol as kind='type'.
516
+
517
+ enum Status { ... } → kind='type'. enum cases are NOT emitted (no matching kind).
518
+ Never raises.
519
+ """
520
+ try:
521
+ name_node = node.child_by_field_name("name")
522
+ if name_node is None:
523
+ for child in node.children:
524
+ if child.type == "type_identifier":
525
+ name_node = child
526
+ break
527
+ if name_node is None:
528
+ return
529
+
530
+ enum_name = _text(name_node)
531
+ doc = _swift_doc_comment(node)
532
+ fields = extract_node_fields(
533
+ node,
534
+ "swift",
535
+ qualified_name=enum_name,
536
+ max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
537
+ )
538
+ symbols.append(
539
+ _make_symbol(
540
+ enum_name,
541
+ "type",
542
+ file_str,
543
+ node,
544
+ doc,
545
+ signature=fields["signature"],
546
+ decorators=fields["decorators"],
547
+ is_exported=fields["is_exported"],
548
+ visibility=fields["visibility"],
549
+ qualified_name=enum_name,
550
+ )
551
+ )
552
+ except Exception as exc: # noqa: BLE001
553
+ logger.debug("_handle_enum: failed at %r: %r", node.start_point, exc)
554
+
555
+
556
+ def _handle_protocol(node: Node, file_str: str, symbols: list[Symbol]) -> None:
557
+ """Emit a protocol symbol as kind='interface' and its method symbols.
558
+
559
+ protocol Describable { func describe() -> String } →
560
+ - 'Describable' (kind='interface')
561
+ - 'Describable.describe' (kind='method')
562
+
563
+ Protocol methods are 'protocol_function_declaration' nodes in the grammar,
564
+ not 'function_declaration'. Their name comes from a simple_identifier child.
565
+ Never raises.
566
+ """
567
+ try:
568
+ name_node = node.child_by_field_name("name")
569
+ if name_node is None:
570
+ for child in node.children:
571
+ if child.type == "type_identifier":
572
+ name_node = child
573
+ break
574
+ if name_node is None:
575
+ return
576
+
577
+ proto_name = _text(name_node)
578
+ doc = _swift_doc_comment(node)
579
+ fields = extract_node_fields(
580
+ node,
581
+ "swift",
582
+ qualified_name=proto_name,
583
+ max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
584
+ )
585
+ symbols.append(
586
+ _make_symbol(
587
+ proto_name,
588
+ "interface",
589
+ file_str,
590
+ node,
591
+ doc,
592
+ signature=fields["signature"],
593
+ decorators=fields["decorators"],
594
+ is_exported=fields["is_exported"],
595
+ visibility=fields["visibility"],
596
+ qualified_name=proto_name,
597
+ )
598
+ )
599
+
600
+ # Recurse into protocol_body for protocol_function_declaration methods
601
+ for child in node.children:
602
+ if child.type == "protocol_body":
603
+ for gc in child.children:
604
+ if gc.type == "protocol_function_declaration":
605
+ _handle_protocol_method(gc, file_str, symbols, proto_name)
606
+ break
607
+
608
+ except Exception as exc: # noqa: BLE001
609
+ logger.debug("_handle_protocol: failed at %r: %r", node.start_point, exc)
610
+
611
+
612
+ def _handle_protocol_method(
613
+ node: Node,
614
+ file_str: str,
615
+ symbols: list[Symbol],
616
+ proto_name: str,
617
+ ) -> None:
618
+ """Emit a protocol method symbol (protocol_function_declaration node).
619
+
620
+ Name comes from a simple_identifier child (no 'name' field in this node type).
621
+ Qualified as 'ProtoName.methodName'.
622
+ Never raises.
623
+ """
624
+ try:
625
+ name = None
626
+ for child in node.children:
627
+ if child.type == "simple_identifier":
628
+ name = _text(child)
629
+ break
630
+ if not name:
631
+ return
632
+
633
+ qualified = f"{proto_name}.{name}"
634
+ doc = _swift_doc_comment(node)
635
+ fields = extract_node_fields(
636
+ node,
637
+ "swift",
638
+ qualified_name=qualified,
639
+ max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
640
+ )
641
+ symbols.append(
642
+ _make_symbol(
643
+ qualified,
644
+ "method",
645
+ file_str,
646
+ node,
647
+ doc,
648
+ signature=fields["signature"],
649
+ decorators=fields["decorators"],
650
+ is_exported=fields["is_exported"],
651
+ visibility=fields["visibility"],
652
+ qualified_name=qualified,
653
+ )
654
+ )
655
+ except Exception as exc: # noqa: BLE001
656
+ logger.debug("_handle_protocol_method: failed at %r: %r", node.start_point, exc)
657
+
658
+
659
+ # ── Swift edge extraction ──────────────────────────────────────────────────────
660
+
661
+
662
+ def _extract_edges_swift(root: Node, filepath: Path) -> list[Edge]:
663
+ """Extract import, call, and composition (holds) edges from a Swift AST.
664
+
665
+ Import heuristic:
666
+ import Foundation → target = 'Foundation' (sole simple_identifier)
667
+ import UIKit.UIView → target = 'UIView' (LAST simple_identifier in identifier)
668
+
669
+ Call heuristic:
670
+ call_expression with a bare simple_identifier callee → kind='call'.
671
+ call_expression with a navigation_expression callee (obj.m, self.m):
672
+ P5 lightweight receiver-type inference (SEAM_SWIFT_TYPE_INFERENCE=on):
673
+ self.m() → '<EnclosingType>.m'
674
+ ClassName().m() → 'ClassName.m'
675
+ let x = Foo(); x.m() → 'Foo.m' (function-scope var→class dict)
676
+ Unknown receiver → SKIP (never emit a wrong global-name edge).
677
+ When inference is off → ALL navigation-expression calls are SKIPPED
678
+ (byte-identical to pre-P5 behavior).
679
+
680
+ Composition heuristic (Slice #80, SEAM_COMPOSITION_EDGES=on):
681
+ For each class/struct/actor declaration, emit Edge(kind='holds', source=TypeName,
682
+ target=HeldTypeName) for each plain user-type stored property (including
683
+ @ObservedObject/@StateObject/@EnvironmentObject-wrapped ones) and each plain
684
+ user-type init parameter. Deduped per (source, target) within the collector.
685
+
686
+ Never raises — outer try/except wraps the walk.
687
+ """
688
+ edges: list[Edge] = []
689
+ file_str = str(filepath)
690
+ file_stem = filepath.stem
691
+ infer = config.SEAM_SWIFT_TYPE_INFERENCE == "on"
692
+ composition_on = config.SEAM_COMPOSITION_EDGES == "on"
693
+ field_access_on = config.SEAM_FIELD_ACCESS_EDGES == "on"
694
+ param_edges_on = config.SEAM_PARAM_EDGES == "on"
695
+
696
+ try:
697
+ # Slice #80: emit holds edges for class/struct/actor declarations.
698
+ # We walk the top-level children here (not recursively via _walk_edges)
699
+ # because holds edges are class-scoped (one pass per class_declaration).
700
+ if composition_on:
701
+ _collect_swift_holds(root, file_str, edges)
702
+
703
+ _walk_edges(
704
+ root,
705
+ file_str,
706
+ file_stem,
707
+ edges,
708
+ infer,
709
+ field_access_on,
710
+ param_edges_on,
711
+ class_name=None,
712
+ var_types={},
713
+ class_var_types={},
714
+ )
715
+ except Exception as exc: # noqa: BLE001
716
+ logger.debug("_extract_edges_swift: unhandled exception for %s: %r", filepath, exc)
717
+
718
+ return edges
719
+
720
+
721
+ def _collect_swift_holds(root: Node, file_str: str, edges: list[Edge]) -> None:
722
+ """Walk top-level nodes and emit holds edges for class/struct/actor declarations.
723
+
724
+ For each class_declaration with keyword 'class', 'struct', or 'actor',
725
+ calls collect_composition_types_swift and emits one Edge(kind='holds') per
726
+ (held_type, line) pair. Extensions are skipped (they don't own stored properties
727
+ in the same compositional sense — they extend an existing type).
728
+
729
+ Never raises (backstop try/except). Called only when SEAM_COMPOSITION_EDGES=on.
730
+ """
731
+ try:
732
+ for child in root.children:
733
+ if child.type != "class_declaration":
734
+ continue
735
+ keyword = _swift_class_keyword(child)
736
+ # Only class/struct/actor own stored properties; extension does not.
737
+ if keyword not in ("class", "struct", "actor"):
738
+ continue
739
+ # Extract the type name (same as _handle_class_like uses).
740
+ name_node = child.child_by_field_name("name")
741
+ if name_node is None:
742
+ for gc in child.children:
743
+ if gc.type == "type_identifier":
744
+ name_node = gc
745
+ break
746
+ if name_node is None:
747
+ continue
748
+ class_name = _text(name_node)
749
+ if not class_name:
750
+ continue
751
+ # Emit one holds edge per collected (held_type, line) pair.
752
+ for held_type, held_line in collect_composition_types_swift(child):
753
+ edges.append(Edge(
754
+ source=class_name,
755
+ target=held_type,
756
+ kind="holds",
757
+ file=file_str,
758
+ line=held_line,
759
+ confidence="INFERRED",
760
+ receiver=None,
761
+ ))
762
+ except Exception as exc: # noqa: BLE001
763
+ logger.debug("_collect_swift_holds: failed: %r", exc)
764
+
765
+
766
+ def _swift_decl_type_name(node: Node) -> str | None:
767
+ """Return the type name carried by a class_declaration node (class/struct/actor/extension).
768
+
769
+ For extension nodes the name lives in a user_type child (no 'name' field), so this
770
+ falls back to _swift_extension_type_name; otherwise the type_identifier child is used.
771
+ Reused to set the enclosing-class context for self.method() resolution.
772
+ """
773
+ keyword = _swift_class_keyword(node)
774
+ if keyword == "extension":
775
+ return _swift_extension_type_name(node)
776
+ name_node = node.child_by_field_name("name")
777
+ if name_node is not None:
778
+ return _text(name_node)
779
+ for child in node.children:
780
+ if child.type == "type_identifier":
781
+ return _text(child)
782
+ return None
783
+
784
+
785
+ def _walk_edges(
786
+ node: Node,
787
+ file_str: str,
788
+ file_stem: str,
789
+ edges: list[Edge],
790
+ infer: bool,
791
+ field_access_on: bool,
792
+ param_edges_on: bool,
793
+ class_name: str | None,
794
+ var_types: dict[str, str],
795
+ class_var_types: dict[str, str],
796
+ ) -> None:
797
+ """Recursively walk the AST to collect import and call edges.
798
+
799
+ Threads three context values for type inference:
800
+ class_name — the nearest enclosing class/struct/actor/extension type name,
801
+ used to resolve self.method(). None at top level.
802
+ class_var_types — the enclosing class's stored-property `name → type` map,
803
+ pre-scanned on entering the class so a typed property is visible in EVERY
804
+ body regardless of declaration order. Fresh per class.
805
+ var_types — the current body's `name → type` map: re-seeded from
806
+ class_var_types (+ parameters, for functions) on entering a function_declaration,
807
+ and from class_var_types alone on entering a class (so class-body descendants —
808
+ initializers, computed accessors — see class properties without inheriting a
809
+ stale outer scope), then extended with local `let`/`var` bindings as walked.
810
+
811
+ Class-level stored `property_declaration`s are NOT recorded during the walk (they are
812
+ already in class_var_types via the pre-scan); recording them here would mutate the
813
+ shared class-scope dict in place and make resolution depend on source order.
814
+ field_access_on gates A3 Slice 5 reads/writes edge emission.
815
+ """
816
+ node_type = node.type
817
+
818
+ if node_type == "import_declaration":
819
+ _handle_import(node, file_str, file_stem, edges)
820
+ return # No need to recurse into import node
821
+
822
+ if node_type == "class_declaration":
823
+ # Update enclosing-class context for descendants (self.method resolution) and
824
+ # pre-scan its stored properties so bodies see DI'd typed properties. Seed the
825
+ # class-body scope too so initializers / nested classes don't inherit a stale
826
+ # outer var_types.
827
+ class_name = _swift_decl_type_name(node) or class_name
828
+ if infer:
829
+ class_var_types = _scan_class_properties(node)
830
+ var_types = dict(class_var_types)
831
+
832
+ elif node_type == "function_declaration":
833
+ if infer:
834
+ # New function scope: inherit class properties, then bind parameters.
835
+ # Local lets append below as the body is walked.
836
+ var_types = dict(class_var_types)
837
+ _record_param_types(node, var_types)
838
+
839
+ # A3 Slice 5: emit reads/writes field-access edges for self.prop accesses.
840
+ if field_access_on and class_name is not None:
841
+ edges.extend(emit_swift_field_access_edges(node, file_str, class_name, var_types))
842
+
843
+ # 'uses' edges: this function references plain user types as parameters.
844
+ # Source is the qualified function name (Class.method or bare top-level function),
845
+ # matching how call edges are sourced.
846
+ if param_edges_on:
847
+ _emit_swift_param_uses(node, file_str, class_name, edges)
848
+
849
+ elif infer and node_type == "property_declaration":
850
+ # Record ONLY function-scope locals. A class-level stored property (direct child
851
+ # of class_body) is already in class_var_types; skipping it here keeps the
852
+ # class-scope dict unmutated, so resolution is order-independent.
853
+ if node.parent is None or node.parent.type != "class_body":
854
+ _record_var_binding(node, var_types)
855
+
856
+ if node_type == "call_expression":
857
+ _handle_call(node, file_str, edges, infer, class_name, var_types)
858
+ # Still recurse — arguments can contain nested calls.
859
+
860
+ for child in node.children:
861
+ _walk_edges(
862
+ child, file_str, file_stem, edges, infer, field_access_on, param_edges_on,
863
+ class_name, var_types, class_var_types
864
+ )
865
+
866
+
867
+ def _emit_swift_param_uses(
868
+ node: Node,
869
+ file_str: str,
870
+ class_name: str | None,
871
+ edges: list[Edge],
872
+ ) -> None:
873
+ """Emit kind='uses' edges from a function_declaration to each plain user-typed param.
874
+
875
+ Source qualification matches the symbol/call-edge convention: 'Class.method' inside a
876
+ type body, bare function name at top level. Delegates plain-type extraction to
877
+ collect_param_types_swift (same conservatism as holds). Never raises.
878
+ """
879
+ try:
880
+ name_node = next((c for c in node.children if c.type == "simple_identifier"), None)
881
+ if name_node is None:
882
+ return
883
+ fname = _text(name_node)
884
+ if not fname:
885
+ return
886
+ source = f"{class_name}.{fname}" if class_name else fname
887
+ for ptype, pline in collect_param_types_swift(node):
888
+ edges.append(Edge(
889
+ source=source,
890
+ target=ptype,
891
+ kind="uses",
892
+ file=file_str,
893
+ line=pline,
894
+ confidence="INFERRED",
895
+ receiver=None,
896
+ ))
897
+ except Exception as exc: # noqa: BLE001
898
+ logger.debug("_emit_swift_param_uses: failed at %r: %r", node.start_point, exc)
899
+
900
+ def _handle_import(node: Node, file_str: str, file_stem: str, edges: list[Edge]) -> None:
901
+ """Extract import edge from a Swift import_declaration node.
902
+
903
+ import Foundation → target = 'Foundation'
904
+ import UIKit.UIView → target = 'UIView' (last simple_identifier segment)
905
+ """
906
+ line = node.start_point[0] + 1
907
+ # Find the 'identifier' child which contains simple_identifier segments.
908
+ for child in node.children:
909
+ if child.type == "identifier":
910
+ # Collect all simple_identifier children; take the LAST one.
911
+ segments = [_text(gc) for gc in child.children if gc.type == "simple_identifier"]
912
+ if segments:
913
+ target = segments[-1]
914
+ edges.append(
915
+ Edge(
916
+ source=file_stem,
917
+ target=target,
918
+ kind="import",
919
+ file=file_str,
920
+ line=line,
921
+ confidence="INFERRED",
922
+ receiver=None,
923
+ )
924
+ )
925
+ break
926
+
927
+
928
+ def _handle_call(
929
+ node: Node,
930
+ file_str: str,
931
+ edges: list[Edge],
932
+ infer: bool,
933
+ class_name: str | None,
934
+ var_types: dict[str, str],
935
+ ) -> None:
936
+ """Extract a call edge from a call_expression node.
937
+
938
+ Bare simple_identifier callees produce an unqualified edge (unchanged).
939
+ Navigation-expression callees (obj.m, self.m) are resolved to a qualified
940
+ 'Type.method' edge when type inference is on AND the receiver type is known;
941
+ otherwise they are skipped (no wrong global-name edge is ever emitted).
942
+
943
+ Tier B B2: raw receiver text is captured from the navigation_expression's
944
+ first child (the receiver side) and stored on the emitted edge.
945
+ """
946
+ if not node.children:
947
+ return
948
+ callee = node.children[0]
949
+
950
+ if callee.type == "simple_identifier":
951
+ # Bare call: unqualified target.
952
+ target = _text(callee)
953
+ if not target:
954
+ return
955
+ # Tier B B6: PascalCase bare call Foo() in Swift → instantiates edge.
956
+ # Swift uses call_expression for both function calls and constructor calls.
957
+ # A PascalCase callee (starts with uppercase) is a constructor call.
958
+ # Lowercase callees remain plain call edges.
959
+ if target[0].isupper():
960
+ source = _find_enclosing_function(node, "swift")
961
+ if source is not None:
962
+ edges.append(Edge(
963
+ source=source,
964
+ target=target,
965
+ kind="instantiates",
966
+ file=file_str,
967
+ line=node.start_point[0] + 1,
968
+ confidence="INFERRED",
969
+ receiver=None,
970
+ ))
971
+ return
972
+ _emit_call(node, file_str, target, receiver=None, edges=edges)
973
+ return
974
+
975
+ if infer and callee.type == "navigation_expression":
976
+ # Tier B B2: extract raw receiver text from the navigation_expression's
977
+ # first child (e.g. self_expression → 'self'; simple_identifier → var name).
978
+ nav_receiver: str | None = None
979
+ if callee.children:
980
+ recv_node = callee.children[0]
981
+ nav_receiver = _text(recv_node) if recv_node is not None else None
982
+
983
+ nav_target = _resolve_navigation_target(callee, class_name, var_types)
984
+ if nav_target is not None:
985
+ _emit_call(node, file_str, nav_target, receiver=nav_receiver, edges=edges)
986
+ # Unknown receiver (or inference off) → skip: never emit a wrong edge.
987
+
988
+
989
+ def _emit_call(
990
+ node: Node,
991
+ file_str: str,
992
+ target: str,
993
+ *,
994
+ receiver: str | None = None,
995
+ edges: list[Edge],
996
+ ) -> None:
997
+ """Append a call edge sourced from the enclosing function, if any.
998
+
999
+ Tier B B2: accepts an optional raw receiver text to store on the edge.
1000
+ """
1001
+ source = _find_enclosing_function(node, "swift")
1002
+ if source is not None:
1003
+ edges.append(
1004
+ Edge(
1005
+ source=source,
1006
+ target=target,
1007
+ kind="call",
1008
+ file=file_str,
1009
+ line=node.start_point[0] + 1,
1010
+ confidence="INFERRED",
1011
+ receiver=receiver,
1012
+ )
1013
+ )
1014
+