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,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
|
+
|