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,515 @@
1
+ """Swift call-edge type inference and comment extraction helpers.
2
+
3
+ LAYER: imports from graph_common (leaf) and seam.analysis.builtins only — never
4
+ from graph_swift or graph.py.
5
+
6
+ LAYERING:
7
+ graph_common (leaf — no seam deps)
8
+
9
+ graph_swift_infer (this file — type inference + comment extraction)
10
+
11
+ graph_swift (imports these helpers for edge extraction + comment walking)
12
+
13
+ graph.py
14
+
15
+ WHY a separate module: graph_swift would exceed the 1000-line limit with the
16
+ dependency-injection inference added inline. This file holds the cohesive, leaf-pure
17
+ type-inference cluster (no Edge construction, no AST walking) so graph_swift keeps the
18
+ orchestration (walk + Edge emit). Comment extraction is also here to keep graph_swift
19
+ under the 1000-line limit (the _extract_comments_swift / _walk_comments functions moved
20
+ here in the A3 refactor, since they only depend on graph_common).
21
+
22
+ CONTRACT: every function is pure and never raises (the recording helpers wrap their
23
+ body in a backstop). The Swift module NEVER emits a wrong edge — resolution returns
24
+ None on any uncertainty so the caller drops the edge rather than guess.
25
+ """
26
+
27
+ import logging
28
+ from pathlib import Path
29
+
30
+ from tree_sitter import Node
31
+
32
+ from seam.analysis.builtins import is_builtin
33
+ from seam.indexer.graph_common import (
34
+ Comment,
35
+ _block_comment_lines,
36
+ _match_marker,
37
+ _text,
38
+ )
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ # ── Swift comment extraction ──────────────────────────────────────────────────
44
+ #
45
+ # Moved here from graph_swift.py in the A3 refactor so graph_swift.py stays
46
+ # under the 1000-line limit. Only depends on graph_common (leaf).
47
+
48
+
49
+ def _extract_comments_swift(root: Node, filepath: Path) -> list[Comment]:
50
+ """Walk a Swift AST and extract semantic comment markers.
51
+
52
+ Swift comment node types (verified against tree-sitter-swift 0.7.3):
53
+ 'comment' — // and /// lines (both kinds are the same node type)
54
+ 'multiline_comment' — /* */ blocks
55
+
56
+ For // and /// nodes: strip the '//' prefix (and any additional slashes),
57
+ then match the marker. For multiline_comment, scan every line with
58
+ _block_comment_lines.
59
+
60
+ Never raises — outer try/except wraps the walk.
61
+ """
62
+ comments: list[Comment] = []
63
+
64
+ try:
65
+ _walk_comments(root, comments)
66
+ except Exception as exc: # noqa: BLE001
67
+ logger.debug("_extract_comments_swift: unhandled exception for %s: %r", filepath, exc)
68
+
69
+ return comments
70
+
71
+
72
+ def _walk_comments(node: Node, comments: list[Comment]) -> None:
73
+ """Recursively collect semantic comment markers from the AST."""
74
+ if node.type == "comment":
75
+ raw = _text(node)
76
+ base_row = node.start_point[0] + 1
77
+ # Strip '//' prefix and any additional slashes (covers // and ///)
78
+ body = raw.lstrip("/").strip()
79
+ result = _match_marker(body)
80
+ if result is not None:
81
+ marker, text = result
82
+ comments.append(Comment(marker=marker, text=text, line=base_row))
83
+
84
+ elif node.type == "multiline_comment":
85
+ raw = _text(node)
86
+ base_row = node.start_point[0] + 1
87
+ for offset, body in _block_comment_lines(raw):
88
+ result = _match_marker(body)
89
+ if result is not None:
90
+ marker, text = result
91
+ comments.append(Comment(marker=marker, text=text, line=base_row + offset))
92
+
93
+ for child in node.children:
94
+ _walk_comments(child, comments)
95
+
96
+
97
+ def _swift_instantiated_class(value_node: Node) -> str | None:
98
+ """If value_node is a 'ClassName(...)' instantiation, return 'ClassName', else None.
99
+
100
+ A Swift constructor call is a call_expression whose first child is a bare
101
+ simple_identifier (the type name). Used both for `let x = Foo()` binding capture
102
+ and for inline `Foo().method()` resolution. Returns None for any other shape.
103
+ """
104
+ if value_node.type != "call_expression" or not value_node.children:
105
+ return None
106
+ head = value_node.children[0]
107
+ if head.type == "simple_identifier":
108
+ text = _text(head)
109
+ return text or None
110
+ return None
111
+
112
+
113
+ def _plain_user_type_name(holder: Node) -> str | None:
114
+ """Return the type name from a node ONLY if it wraps a plain `user_type`.
115
+
116
+ Conservative by design (the Swift module never emits a wrong edge): the type binds
117
+ only for a bare `: TypeName`. Optional (`: Foo?`), array (`: [Foo]`), dictionary, and
118
+ generic (`Array<Foo>`) annotations resolve to a different child node type — or carry a
119
+ type_arguments clause — so they return None rather than mis-bind a receiver to the
120
+ wrong type (e.g. an array's `.append` to its element type).
121
+
122
+ `holder` is a `type_annotation`, a `parameter`, or any node carrying a declared
123
+ type child. A `parameter` carries the type as a direct `user_type` child; a
124
+ `type_annotation` does too — but a parameter whose grammar wraps the type in a
125
+ nested `type_annotation` is handled by the recursive case below, so the function
126
+ works for both shapes without the caller needing to know which it is.
127
+ """
128
+ for child in holder.children:
129
+ if child.type == "user_type":
130
+ ids = [gc for gc in child.children if gc.type == "type_identifier"]
131
+ has_generic = any(gc.type == "type_arguments" for gc in child.children)
132
+ if len(ids) == 1 and not has_generic:
133
+ return _text(ids[0])
134
+ return None
135
+ # Defensive: some node shapes wrap the type one level deeper in a
136
+ # type_annotation (e.g. a parameter in a future grammar revision). Recurse
137
+ # so a direct-vs-wrapped grammar difference cannot silently drop the binding.
138
+ if child.type == "type_annotation":
139
+ return _plain_user_type_name(child)
140
+ # A non-plain type wrapper (optional/array/dictionary) — refuse to bind.
141
+ if child.type in ("optional_type", "array_type", "dictionary_type"):
142
+ return None
143
+ return None
144
+
145
+
146
+ def _property_var_name(node: Node) -> str | None:
147
+ """Return the bound variable name from a property_declaration's `pattern` child."""
148
+ for child in node.children:
149
+ if child.type == "pattern":
150
+ for gc in child.children:
151
+ if gc.type == "simple_identifier":
152
+ return _text(gc)
153
+ return None
154
+
155
+
156
+ def _declared_type_name(decl_node: Node) -> str | None:
157
+ """Return the bound type for a property_declaration, or None.
158
+
159
+ Two shapes bind (both conservative — see _plain_user_type_name):
160
+ let x: TypeName → 'TypeName' (typed declaration; the DI'd-property case)
161
+ let x = Foo() → 'Foo' (inline instantiation; the original P5 case)
162
+ A typed annotation wins over a value when both somehow appear. Anything else
163
+ (optional/array/generic/closure/computed property) → None.
164
+ """
165
+ type_name: str | None = None
166
+ value: Node | None = None
167
+ seen_eq = False
168
+ for child in decl_node.children:
169
+ if child.type == "type_annotation":
170
+ type_name = _plain_user_type_name(child)
171
+ elif child.type == "=":
172
+ seen_eq = True
173
+ elif seen_eq and value is None:
174
+ value = child
175
+ if type_name:
176
+ return type_name
177
+ if value is not None:
178
+ return _swift_instantiated_class(value)
179
+ return None
180
+
181
+
182
+ def _record_var_binding(node: Node, var_types: dict[str, str]) -> None:
183
+ """Record a property_declaration's `name → type` binding into a var_types map.
184
+
185
+ Captures BOTH `let x: Type` (typed declaration) and `let x = Type()` (inline
186
+ instantiation) — the typed form is what dependency-injected stored properties use,
187
+ and missing it was the dominant cause of dropped inter-class Swift edges.
188
+ Compound/tuple patterns and non-plain types are ignored. Never raises.
189
+ """
190
+ try:
191
+ var_name = _property_var_name(node)
192
+ if not var_name:
193
+ return
194
+ cls = _declared_type_name(node)
195
+ if cls:
196
+ var_types[var_name] = cls
197
+ except Exception as exc: # noqa: BLE001
198
+ logger.debug("_record_var_binding: failed at %r: %r", node.start_point, exc)
199
+
200
+
201
+ def _scan_class_properties(class_node: Node) -> dict[str, str]:
202
+ """Pre-scan a class/struct/actor/extension body for stored-property type bindings.
203
+
204
+ WHY a pre-scan instead of recording during the walk: Swift allows a property to be
205
+ declared AFTER the method that uses it, and every method needs the full property
206
+ type map regardless of source order. Returns name → type for the direct class_body
207
+ property declarations only (not nested types). Never raises.
208
+ """
209
+ out: dict[str, str] = {}
210
+ try:
211
+ for child in class_node.children:
212
+ if child.type == "class_body":
213
+ for gc in child.children:
214
+ if gc.type == "property_declaration":
215
+ _record_var_binding(gc, out)
216
+ break
217
+ except Exception as exc: # noqa: BLE001
218
+ logger.debug("_scan_class_properties: failed at %r: %r", class_node.start_point, exc)
219
+ return out
220
+
221
+
222
+ def _record_param_types(func_node: Node, var_types: dict[str, str]) -> None:
223
+ """Bind each parameter's in-body name → declared type into var_types.
224
+
225
+ func f(p: P) { p.use() } → binds p→P so `p.use()` resolves to 'P.use'. The in-body
226
+ name is the LAST simple_identifier before the type (handles `_ name:` and
227
+ `label name:` external-name forms). Same conservative plain-user_type rule as
228
+ properties. Never raises.
229
+ """
230
+ try:
231
+ for child in func_node.children:
232
+ if child.type != "parameter":
233
+ continue
234
+ names = [gc for gc in child.children if gc.type == "simple_identifier"]
235
+ ptype = _plain_user_type_name(child)
236
+ if names and ptype:
237
+ # Last identifier = the name actually used inside the body.
238
+ var_types[_text(names[-1])] = ptype
239
+ except Exception as exc: # noqa: BLE001
240
+ logger.debug("_record_param_types: failed at %r: %r", func_node.start_point, exc)
241
+
242
+
243
+ def _navigation_method_name(nav: Node) -> str | None:
244
+ """Return the trailing method name from a navigation_expression's navigation_suffix."""
245
+ for child in nav.children:
246
+ if child.type == "navigation_suffix":
247
+ for gc in child.children:
248
+ if gc.type == "simple_identifier":
249
+ return _text(gc)
250
+ return None
251
+
252
+
253
+ # ── Slice #80: Composition (holds) collector for Swift ───────────────────────
254
+ #
255
+ # collect_composition_types_swift returns deduped (held_type_name, line) pairs
256
+ # for a class/struct/actor declaration node. Two passes:
257
+ # 1. Stored properties (property_declaration with a plain type_annotation)
258
+ # — including @ObservedObject/@StateObject/@EnvironmentObject wrappers,
259
+ # which only affect the 'modifiers' child and do not change the type_annotation.
260
+ # 2. init_declaration parameters with a plain user_type annotation.
261
+ #
262
+ # CONSERVATISM CONTRACT (same as receiver-type inference):
263
+ # ONLY emit for a bare TypeName (plain user_type with no type_arguments).
264
+ # REFUSE: Foo? (optional_type), [Foo] (array_type), [K:V] (dictionary_type),
265
+ # Foo<T> (user_type with type_arguments), and Swift builtins.
266
+ #
267
+ # Reuses _plain_user_type_name which already handles all refusals.
268
+ # Dedup: a type appearing in BOTH a property and an init param → ONE entry.
269
+ # NEVER RAISES: backstop try/except returns [] on any error.
270
+
271
+
272
+ def collect_composition_types_swift(class_node: Node) -> list[tuple[str, int]]:
273
+ """Collect (held_type_name, line) pairs from a Swift class/struct/actor node.
274
+
275
+ Two passes over the class_body children:
276
+ 1. property_declaration: captures the type from the type_annotation child.
277
+ @ObservedObject, @StateObject, @EnvironmentObject modifiers are transparent —
278
+ the type_annotation is still a direct child regardless of the wrapper attribute.
279
+ 2. init_declaration: scans parameter children of the init, capturing each
280
+ parameter's plain user_type via _plain_user_type_name (same helper used for
281
+ receiver-type inference).
282
+
283
+ Returns [] on any error. Never raises.
284
+ """
285
+ try:
286
+ seen: set[str] = set()
287
+ result: list[tuple[str, int]] = []
288
+
289
+ # Find the class_body (first child of type 'class_body').
290
+ body: Node | None = None
291
+ for child in class_node.children:
292
+ if child.type == "class_body":
293
+ body = child
294
+ break
295
+ if body is None:
296
+ return result
297
+
298
+ # Pass 1: stored properties.
299
+ for child in body.children:
300
+ _swift_collect_property_holds(child, seen, result)
301
+
302
+ # Pass 2: init declaration parameters.
303
+ for child in body.children:
304
+ _swift_collect_init_holds(child, seen, result)
305
+
306
+ return result
307
+ except Exception as exc: # noqa: BLE001
308
+ logger.debug("collect_composition_types_swift: failed: %r", exc)
309
+ return []
310
+
311
+
312
+ def _swift_collect_property_holds(
313
+ node: Node,
314
+ seen: set[str],
315
+ result: list[tuple[str, int]],
316
+ ) -> None:
317
+ """Capture a stored-property declaration's held type if it is a plain user type.
318
+
319
+ property_declaration structure (relevant path):
320
+ property_declaration
321
+ [modifiers] ← optional; @ObservedObject/@StateObject/etc. live here
322
+ value_binding_pattern ← 'var' or 'let' keyword
323
+ pattern ← the variable name
324
+ type_annotation ← ': TypeName' ← THIS is what we care about
325
+
326
+ The modifiers (wrapper attributes) are transparent: they only affect the
327
+ modifiers child, not the type_annotation. _plain_user_type_name rejects
328
+ optional_type / array_type / dictionary_type / generics automatically.
329
+
330
+ Never raises.
331
+ """
332
+ try:
333
+ if node.type != "property_declaration":
334
+ return
335
+ # Find the type_annotation child.
336
+ for child in node.children:
337
+ if child.type == "type_annotation":
338
+ type_name = _plain_user_type_name(child)
339
+ if type_name and not is_builtin(type_name, "swift") and type_name not in seen:
340
+ seen.add(type_name)
341
+ result.append((type_name, node.start_point[0] + 1))
342
+ break
343
+ except Exception as exc: # noqa: BLE001
344
+ logger.debug("_swift_collect_property_holds: failed at %r: %r", node.start_point, exc)
345
+
346
+
347
+ def _swift_collect_init_holds(
348
+ node: Node,
349
+ seen: set[str],
350
+ result: list[tuple[str, int]],
351
+ ) -> None:
352
+ """Capture an init_declaration's parameter types that are plain user types.
353
+
354
+ init_declaration structure (relevant path):
355
+ init_declaration
356
+ 'init'
357
+ '('
358
+ parameter ← one per init param
359
+ [simple_identifier] ← external label (optional, e.g. 'with' or '_')
360
+ simple_identifier ← internal name (always present)
361
+ ':'
362
+ [user_type | optional_type | array_type | dictionary_type | ...]
363
+ ')'
364
+ function_body
365
+
366
+ _plain_user_type_name applied to the parameter node extracts the type if plain,
367
+ refusing optional/array/dictionary/generic shapes. Dedup: a type already in
368
+ 'seen' (added during the property pass) is skipped.
369
+
370
+ Never raises.
371
+ """
372
+ try:
373
+ if node.type != "init_declaration":
374
+ return
375
+ for child in node.children:
376
+ if child.type == "parameter":
377
+ _swift_collect_single_param_holds(child, seen, result)
378
+ except Exception as exc: # noqa: BLE001
379
+ logger.debug("_swift_collect_init_holds: failed at %r: %r", node.start_point, exc)
380
+
381
+
382
+ def _swift_collect_single_param_holds(
383
+ param: Node,
384
+ seen: set[str],
385
+ result: list[tuple[str, int]],
386
+ ) -> None:
387
+ """Record one init parameter's plain type into the result if accepted.
388
+
389
+ Applies _plain_user_type_name to the parameter node, which handles:
390
+ svc: Service → 'Service'
391
+ _ svc: Service → 'Service' (external '_' label)
392
+ with svc: Service → 'Service' (external label)
393
+ svc: Service? → None (optional — refused)
394
+ items: [Service] → None (array — refused)
395
+ map: [String: Svc] → None (dictionary — refused)
396
+ items: Array<Service> → None (generic — refused)
397
+
398
+ Dedup: skip if the type was already seen from a property declaration.
399
+ Never raises.
400
+ """
401
+ try:
402
+ type_name = _plain_user_type_name(param)
403
+ if type_name and not is_builtin(type_name, "swift") and type_name not in seen:
404
+ seen.add(type_name)
405
+ result.append((type_name, param.start_point[0] + 1))
406
+ except Exception as exc: # noqa: BLE001
407
+ logger.debug(
408
+ "_swift_collect_single_param_holds: failed at %r: %r", param.start_point, exc
409
+ )
410
+
411
+
412
+ def collect_param_types_swift(func_node: Node) -> list[tuple[str, int]]:
413
+ """Collect (param_type_name, line) pairs from a Swift function_declaration node.
414
+
415
+ Walks the function's `parameter` children and applies the SAME plain-type extraction
416
+ (_swift_collect_single_param_holds → _plain_user_type_name) used for init-param holds,
417
+ so a param typed with a plain user type (svc: Service) is captured while optional/
418
+ array/dictionary/generic shapes are refused and builtins are filtered. Deduped within
419
+ the signature.
420
+
421
+ WHY reuse the init-param helper: a method parameter and an init parameter are the same
422
+ AST shape (`parameter` node); the only difference is the enclosing declaration. Sharing
423
+ the helper guarantees `uses` and `holds` apply identical conservatism to param types.
424
+
425
+ Returns [] on any error. Never raises.
426
+ """
427
+ try:
428
+ seen: set[str] = set()
429
+ result: list[tuple[str, int]] = []
430
+ for child in func_node.children:
431
+ if child.type == "parameter":
432
+ _swift_collect_single_param_holds(child, seen, result)
433
+ return result
434
+ except Exception as exc: # noqa: BLE001
435
+ logger.debug("collect_param_types_swift: failed: %r", exc)
436
+ return []
437
+
438
+
439
+ def _resolve_navigation_target(
440
+ nav: Node,
441
+ class_name: str | None,
442
+ var_types: dict[str, str],
443
+ ) -> str | None:
444
+ """Resolve a navigation_expression callee to a qualified 'Type.method' target.
445
+
446
+ Handles these receiver shapes:
447
+ self.method → '<class_name>.method' (needs enclosing class)
448
+ ClassName().method → 'ClassName.method' (inline instantiation)
449
+ x.method (x: Type) → 'Type.method' (scope binding: prop/param/local)
450
+ self.prop.method → '<type of prop>.method' (one-level chain via class prop)
451
+ TypeName.method → 'TypeName.method' (B5: static/enum call — PascalCase
452
+ receiver NOT found in var_types)
453
+
454
+ The static-call path (B5, #6): when the receiver is a simple_identifier NOT in
455
+ var_types and its first character is uppercase (PascalCase), treat it as a type-qualified
456
+ static or enum call: `Logger.log()` → 'Logger.log'. This is gate-safe because:
457
+ (a) only plain simple_identifier receivers trigger it (no chains, no expressions),
458
+ (b) lowercase-first receivers that are NOT in scope still return None (the caller
459
+ drops the edge rather than guessing), preserving the conservatism contract.
460
+ (c) if the PascalCase name IS in var_types, the existing scope-lookup path fires
461
+ first (scope wins over static heuristic).
462
+
463
+ Returns None for any unknown receiver so the caller drops the edge rather than guess.
464
+ """
465
+ receiver = nav.children[0] if nav.children else None
466
+ if receiver is None:
467
+ return None
468
+
469
+ # The method name lives in the navigation_suffix → simple_identifier child.
470
+ method = _navigation_method_name(nav)
471
+ if not method:
472
+ return None
473
+
474
+ # self.method → enclosing class.
475
+ if receiver.type == "self_expression":
476
+ return f"{class_name}.{method}" if class_name else None
477
+
478
+ # x.method — try scope lookup first, then fall through to static-call heuristic.
479
+ if receiver.type == "simple_identifier":
480
+ recv_name = _text(receiver)
481
+ # `Self` (capital S) is Swift's metatype keyword: it names the ENCLOSING type,
482
+ # exactly like lowercase `self`. The tree-sitter grammar parses it as a plain
483
+ # simple_identifier (not self_expression), so without this it would fall into the
484
+ # PascalCase static-call heuristic below and emit a bogus target `Self.method`
485
+ # (a type literally named "Self") — which joins no symbol. Normalize to the class.
486
+ if recv_name == "Self":
487
+ return f"{class_name}.{method}" if class_name else None
488
+ # Scope lookup (class property / parameter / local — set during AST walk).
489
+ cls = var_types.get(recv_name)
490
+ if cls:
491
+ return f"{cls}.{method}"
492
+ # B5 (#6): PascalCase receiver not in scope → static / enum / type call.
493
+ # Only fires for upper-first identifiers — lowercase unknown → refuse (None).
494
+ if recv_name and recv_name[0].isupper():
495
+ return f"{recv_name}.{method}"
496
+ return None
497
+
498
+ # self.prop.method → resolve prop's type from the (inherited) scope map. Only the
499
+ # self.<prop> form is resolvable: a property's type is in var_types, whereas a
500
+ # foreign field access (x.prop.method) would need cross-class field typing we
501
+ # deliberately don't track — so it stays unresolved.
502
+ if receiver.type == "navigation_expression":
503
+ inner = receiver.children[0] if receiver.children else None
504
+ prop = _navigation_method_name(receiver)
505
+ if inner is not None and inner.type == "self_expression" and prop:
506
+ cls = var_types.get(prop)
507
+ return f"{cls}.{method}" if cls else None
508
+ return None
509
+
510
+ # ClassName().method — inline instantiation as the receiver.
511
+ inline_cls = _swift_instantiated_class(receiver)
512
+ if inline_cls:
513
+ return f"{inline_cls}.{method}"
514
+
515
+ return None