seam-code 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- seam/__init__.py +3 -0
- seam/_data/schema.sql +225 -0
- seam/_web/assets/index-BL_tqprR.js +216 -0
- seam/_web/assets/index-GTKUhVyD.css +1 -0
- seam/_web/index.html +13 -0
- seam/analysis/__init__.py +14 -0
- seam/analysis/affected.py +254 -0
- seam/analysis/builtins.py +966 -0
- seam/analysis/byte_budget.py +217 -0
- seam/analysis/changes.py +709 -0
- seam/analysis/cluster_naming.py +260 -0
- seam/analysis/clustering.py +216 -0
- seam/analysis/confidence.py +699 -0
- seam/analysis/embeddings.py +195 -0
- seam/analysis/flows.py +708 -0
- seam/analysis/impact.py +444 -0
- seam/analysis/imports.py +994 -0
- seam/analysis/imports_ext.py +780 -0
- seam/analysis/imports_resolve.py +176 -0
- seam/analysis/processes.py +453 -0
- seam/analysis/relevance.py +155 -0
- seam/analysis/rwr.py +129 -0
- seam/analysis/staleness.py +328 -0
- seam/analysis/steer.py +282 -0
- seam/analysis/synthesis.py +253 -0
- seam/analysis/synthesis_channels.py +433 -0
- seam/analysis/testpaths.py +103 -0
- seam/analysis/traversal.py +470 -0
- seam/cli/__init__.py +0 -0
- seam/cli/install.py +232 -0
- seam/cli/main.py +2602 -0
- seam/cli/output.py +137 -0
- seam/cli/read.py +244 -0
- seam/cli/serve.py +145 -0
- seam/config.py +551 -0
- seam/indexer/__init__.py +0 -0
- seam/indexer/cluster_index.py +425 -0
- seam/indexer/db.py +496 -0
- seam/indexer/embedding_index.py +183 -0
- seam/indexer/field_access.py +536 -0
- seam/indexer/field_access_c_cpp.py +643 -0
- seam/indexer/field_access_ext.py +708 -0
- seam/indexer/field_access_ext2.py +408 -0
- seam/indexer/field_access_go_rust.py +737 -0
- seam/indexer/field_access_php_swift.py +888 -0
- seam/indexer/field_access_ts.py +626 -0
- seam/indexer/graph.py +321 -0
- seam/indexer/graph_c.py +562 -0
- seam/indexer/graph_c_cpp.py +39 -0
- seam/indexer/graph_common.py +644 -0
- seam/indexer/graph_cpp.py +615 -0
- seam/indexer/graph_csharp.py +651 -0
- seam/indexer/graph_go.py +723 -0
- seam/indexer/graph_go_rust.py +39 -0
- seam/indexer/graph_java.py +689 -0
- seam/indexer/graph_java_csharp.py +38 -0
- seam/indexer/graph_php.py +914 -0
- seam/indexer/graph_python.py +628 -0
- seam/indexer/graph_ruby.py +748 -0
- seam/indexer/graph_rust.py +653 -0
- seam/indexer/graph_scope_infer.py +902 -0
- seam/indexer/graph_scope_infer_ext.py +723 -0
- seam/indexer/graph_scope_infer_ext2.py +992 -0
- seam/indexer/graph_swift.py +1014 -0
- seam/indexer/graph_swift_infer.py +515 -0
- seam/indexer/graph_typescript.py +663 -0
- seam/indexer/migrations.py +816 -0
- seam/indexer/parser.py +204 -0
- seam/indexer/pipeline.py +197 -0
- seam/indexer/signatures.py +634 -0
- seam/indexer/signatures_ext.py +780 -0
- seam/indexer/sync.py +287 -0
- seam/indexer/synthesis_index.py +291 -0
- seam/indexer/tokenize.py +79 -0
- seam/installer/__init__.py +67 -0
- seam/installer/claude.py +97 -0
- seam/installer/codex.py +94 -0
- seam/installer/core.py +127 -0
- seam/installer/cursor.py +61 -0
- seam/installer/guide.py +110 -0
- seam/installer/jsonfile.py +85 -0
- seam/installer/markdownfile.py +146 -0
- seam/installer/tomlfile.py +72 -0
- seam/query/__init__.py +0 -0
- seam/query/clusters.py +206 -0
- seam/query/comments.py +217 -0
- seam/query/context.py +293 -0
- seam/query/engine.py +940 -0
- seam/query/fts.py +328 -0
- seam/query/names.py +470 -0
- seam/query/pack.py +433 -0
- seam/query/semantic.py +339 -0
- seam/query/structure.py +727 -0
- seam/server/__init__.py +0 -0
- seam/server/graph_api.py +437 -0
- seam/server/handler_common.py +323 -0
- seam/server/impact_handler.py +615 -0
- seam/server/mcp.py +556 -0
- seam/server/tools.py +697 -0
- seam/server/trace_handler.py +184 -0
- seam/server/web.py +922 -0
- seam/watcher/__init__.py +0 -0
- seam/watcher/__main__.py +56 -0
- seam/watcher/daemon.py +237 -0
- seam_code-0.3.0.dist-info/METADATA +318 -0
- seam_code-0.3.0.dist-info/RECORD +109 -0
- seam_code-0.3.0.dist-info/WHEEL +4 -0
- seam_code-0.3.0.dist-info/entry_points.txt +2 -0
- seam_code-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,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
|