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,653 @@
|
|
|
1
|
+
"""Rust symbol, edge, and comment extraction from tree-sitter ASTs.
|
|
2
|
+
|
|
3
|
+
LAYER: imports from graph_common (leaf), graph_scope_infer_ext (leaf), field_access (leaf) —
|
|
4
|
+
never from graph.py.
|
|
5
|
+
|
|
6
|
+
LAYERING:
|
|
7
|
+
graph_common (leaf — no seam deps)
|
|
8
|
+
field_access (leaf — field-access read/write classification + field symbols)
|
|
9
|
+
↑
|
|
10
|
+
graph_rust (this file)
|
|
11
|
+
↑
|
|
12
|
+
graph_go_rust (thin re-exporter; graph.py imports from there)
|
|
13
|
+
|
|
14
|
+
WHY split from graph_go_rust.py: graph_go_rust.py exceeded 1000 lines after Tier B additions.
|
|
15
|
+
Go and Rust are now each large enough to stand alone.
|
|
16
|
+
|
|
17
|
+
All extractor functions follow the same contract:
|
|
18
|
+
- Accept a tree-sitter Node + filepath.
|
|
19
|
+
- Return a list — never raise, never return None.
|
|
20
|
+
- Edges carry confidence='INFERRED' by default.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from tree_sitter import Node
|
|
27
|
+
|
|
28
|
+
import seam.config as config
|
|
29
|
+
from seam.indexer.field_access import (
|
|
30
|
+
collect_field_symbols_rust,
|
|
31
|
+
extract_field_accesses_rust,
|
|
32
|
+
)
|
|
33
|
+
from seam.indexer.graph_common import (
|
|
34
|
+
Comment,
|
|
35
|
+
Edge,
|
|
36
|
+
Symbol,
|
|
37
|
+
_block_comment_lines,
|
|
38
|
+
_find_enclosing_function,
|
|
39
|
+
_make_symbol,
|
|
40
|
+
_match_marker,
|
|
41
|
+
_node_name,
|
|
42
|
+
_rust_impl_type_name,
|
|
43
|
+
_text,
|
|
44
|
+
)
|
|
45
|
+
from seam.indexer.graph_scope_infer_ext import (
|
|
46
|
+
_RUST_SELF_NAMES,
|
|
47
|
+
collect_composition_types_rust,
|
|
48
|
+
collect_param_types_rust,
|
|
49
|
+
record_rust_local_types,
|
|
50
|
+
record_rust_param_types,
|
|
51
|
+
resolve_receiver_type_ext,
|
|
52
|
+
scan_class_fields_rust,
|
|
53
|
+
)
|
|
54
|
+
from seam.indexer.signatures import extract_node_fields
|
|
55
|
+
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
# ── Doc-comment helper ─────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _rust_doc_comment(decl_node: Node) -> str | None:
|
|
62
|
+
"""Capture a Rust doc-comment: contiguous '///' line_comment nodes above item.
|
|
63
|
+
|
|
64
|
+
Only outer doc-comments ('///') qualify — '//' (plain) and '//!' (inner/module-level)
|
|
65
|
+
are excluded from docstrings (though //! may still match as a semantic marker).
|
|
66
|
+
|
|
67
|
+
Adjacency quirk: Rust line_comment nodes include the trailing newline in their
|
|
68
|
+
text, so end_point[0] reports the NEXT physical row (one row past the comment's
|
|
69
|
+
visible last line). Go comments do NOT include the trailing newline.
|
|
70
|
+
|
|
71
|
+
Correct adjacency rule for Rust:
|
|
72
|
+
visible_end_row = end_point[0] - (1 if raw_text.endswith("\\n") else 0)
|
|
73
|
+
adjacent = next_node.start_point[0] == visible_end_row + 1
|
|
74
|
+
"""
|
|
75
|
+
lines: list[str] = []
|
|
76
|
+
current = decl_node.prev_sibling
|
|
77
|
+
|
|
78
|
+
while current is not None and current.type == "line_comment":
|
|
79
|
+
raw = _text(current)
|
|
80
|
+
if not raw.startswith("///"):
|
|
81
|
+
break
|
|
82
|
+
next_node = current.next_sibling
|
|
83
|
+
if next_node is not None:
|
|
84
|
+
visible_end_row = current.end_point[0] - (1 if raw.endswith("\n") else 0)
|
|
85
|
+
if next_node.start_point[0] != visible_end_row + 1:
|
|
86
|
+
break
|
|
87
|
+
body = raw[3:].strip()
|
|
88
|
+
lines.append(body)
|
|
89
|
+
current = current.prev_sibling
|
|
90
|
+
|
|
91
|
+
if not lines:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
return "\n".join(reversed(lines))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ── Rust extraction ────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _extract_symbols_rust(root: Node, filepath: Path) -> list[Symbol]:
|
|
101
|
+
"""Walk a Rust AST and extract fn, struct, enum, trait, impl-method, and field symbols.
|
|
102
|
+
|
|
103
|
+
Kind mapping (per spec, existing 5 kinds only for non-field symbols):
|
|
104
|
+
function_item (top-level or in mod) → function
|
|
105
|
+
function_item inside impl_item → method (qualified as 'Type.fn')
|
|
106
|
+
struct_item → class
|
|
107
|
+
enum_item → type
|
|
108
|
+
trait_item → interface
|
|
109
|
+
function_item inside trait_item body → method (qualified as 'Trait.fn')
|
|
110
|
+
mod_item → traversed (NOT emitted as symbol)
|
|
111
|
+
|
|
112
|
+
A3 Slice 3: when SEAM_FIELD_ACCESS_EDGES='on', also emits kind='field' symbols
|
|
113
|
+
for each struct_item field_declaration, using collect_field_symbols_rust.
|
|
114
|
+
"""
|
|
115
|
+
symbols: list[Symbol] = []
|
|
116
|
+
file_str = str(filepath)
|
|
117
|
+
field_access_on = config.SEAM_FIELD_ACCESS_EDGES == "on"
|
|
118
|
+
|
|
119
|
+
def _walk(node: Node, impl_type: str | None = None) -> None:
|
|
120
|
+
if node.type == "function_item":
|
|
121
|
+
name = _node_name(node)
|
|
122
|
+
if name:
|
|
123
|
+
doc = _rust_doc_comment(node)
|
|
124
|
+
if impl_type is not None:
|
|
125
|
+
qualified = f"{impl_type}.{name}"
|
|
126
|
+
fields = extract_node_fields(
|
|
127
|
+
node,
|
|
128
|
+
"rust",
|
|
129
|
+
qualified_name=qualified,
|
|
130
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
131
|
+
)
|
|
132
|
+
symbols.append(
|
|
133
|
+
_make_symbol(
|
|
134
|
+
qualified,
|
|
135
|
+
"method",
|
|
136
|
+
file_str,
|
|
137
|
+
node,
|
|
138
|
+
doc,
|
|
139
|
+
signature=fields["signature"],
|
|
140
|
+
decorators=fields["decorators"],
|
|
141
|
+
is_exported=fields["is_exported"],
|
|
142
|
+
visibility=fields["visibility"],
|
|
143
|
+
qualified_name=qualified,
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
fields = extract_node_fields(
|
|
148
|
+
node,
|
|
149
|
+
"rust",
|
|
150
|
+
qualified_name=name,
|
|
151
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
152
|
+
)
|
|
153
|
+
symbols.append(
|
|
154
|
+
_make_symbol(
|
|
155
|
+
name,
|
|
156
|
+
"function",
|
|
157
|
+
file_str,
|
|
158
|
+
node,
|
|
159
|
+
doc,
|
|
160
|
+
signature=fields["signature"],
|
|
161
|
+
decorators=fields["decorators"],
|
|
162
|
+
is_exported=fields["is_exported"],
|
|
163
|
+
visibility=fields["visibility"],
|
|
164
|
+
qualified_name=name,
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
elif node.type == "function_signature_item":
|
|
169
|
+
# Signature-only trait method (no body): fn foo(&self);
|
|
170
|
+
if impl_type is not None:
|
|
171
|
+
name = _node_name(node)
|
|
172
|
+
if name:
|
|
173
|
+
doc = _rust_doc_comment(node)
|
|
174
|
+
qualified = f"{impl_type}.{name}"
|
|
175
|
+
fields = extract_node_fields(
|
|
176
|
+
node,
|
|
177
|
+
"rust",
|
|
178
|
+
qualified_name=qualified,
|
|
179
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
180
|
+
)
|
|
181
|
+
symbols.append(
|
|
182
|
+
_make_symbol(
|
|
183
|
+
qualified,
|
|
184
|
+
"method",
|
|
185
|
+
file_str,
|
|
186
|
+
node,
|
|
187
|
+
doc,
|
|
188
|
+
signature=fields["signature"],
|
|
189
|
+
decorators=fields["decorators"],
|
|
190
|
+
is_exported=fields["is_exported"],
|
|
191
|
+
visibility=fields["visibility"],
|
|
192
|
+
qualified_name=qualified,
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
elif node.type == "impl_item":
|
|
197
|
+
type_name = _rust_impl_type_name(node)
|
|
198
|
+
body = node.child_by_field_name("body")
|
|
199
|
+
if body is not None:
|
|
200
|
+
for child in body.children:
|
|
201
|
+
_walk(child, impl_type=type_name)
|
|
202
|
+
|
|
203
|
+
elif node.type == "struct_item":
|
|
204
|
+
name_node = node.child_by_field_name("name")
|
|
205
|
+
if name_node:
|
|
206
|
+
struct_name = _text(name_node)
|
|
207
|
+
doc = _rust_doc_comment(node)
|
|
208
|
+
fields = extract_node_fields(
|
|
209
|
+
node,
|
|
210
|
+
"rust",
|
|
211
|
+
qualified_name=struct_name,
|
|
212
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
213
|
+
)
|
|
214
|
+
symbols.append(
|
|
215
|
+
_make_symbol(
|
|
216
|
+
struct_name,
|
|
217
|
+
"class",
|
|
218
|
+
file_str,
|
|
219
|
+
node,
|
|
220
|
+
doc,
|
|
221
|
+
signature=fields["signature"],
|
|
222
|
+
decorators=fields["decorators"],
|
|
223
|
+
is_exported=fields["is_exported"],
|
|
224
|
+
visibility=fields["visibility"],
|
|
225
|
+
qualified_name=struct_name,
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
# A3: emit field symbols for this struct when feature is on.
|
|
229
|
+
if field_access_on:
|
|
230
|
+
for qualified_field, field_line in collect_field_symbols_rust(
|
|
231
|
+
node, struct_name
|
|
232
|
+
):
|
|
233
|
+
symbols.append(Symbol(
|
|
234
|
+
name=qualified_field,
|
|
235
|
+
kind="field",
|
|
236
|
+
file=file_str,
|
|
237
|
+
start_line=field_line,
|
|
238
|
+
end_line=field_line,
|
|
239
|
+
docstring=None,
|
|
240
|
+
signature=None,
|
|
241
|
+
decorators=[],
|
|
242
|
+
is_exported=None,
|
|
243
|
+
visibility=None,
|
|
244
|
+
qualified_name=qualified_field,
|
|
245
|
+
))
|
|
246
|
+
|
|
247
|
+
elif node.type == "enum_item":
|
|
248
|
+
name_node = node.child_by_field_name("name")
|
|
249
|
+
if name_node:
|
|
250
|
+
enum_name = _text(name_node)
|
|
251
|
+
doc = _rust_doc_comment(node)
|
|
252
|
+
fields = extract_node_fields(
|
|
253
|
+
node,
|
|
254
|
+
"rust",
|
|
255
|
+
qualified_name=enum_name,
|
|
256
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
257
|
+
)
|
|
258
|
+
symbols.append(
|
|
259
|
+
_make_symbol(
|
|
260
|
+
enum_name,
|
|
261
|
+
"type",
|
|
262
|
+
file_str,
|
|
263
|
+
node,
|
|
264
|
+
doc,
|
|
265
|
+
signature=fields["signature"],
|
|
266
|
+
decorators=fields["decorators"],
|
|
267
|
+
is_exported=fields["is_exported"],
|
|
268
|
+
visibility=fields["visibility"],
|
|
269
|
+
qualified_name=enum_name,
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
elif node.type == "trait_item":
|
|
274
|
+
name_node = node.child_by_field_name("name")
|
|
275
|
+
if name_node:
|
|
276
|
+
trait_name = _text(name_node)
|
|
277
|
+
doc = _rust_doc_comment(node)
|
|
278
|
+
fields = extract_node_fields(
|
|
279
|
+
node,
|
|
280
|
+
"rust",
|
|
281
|
+
qualified_name=trait_name,
|
|
282
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
283
|
+
)
|
|
284
|
+
symbols.append(
|
|
285
|
+
_make_symbol(
|
|
286
|
+
trait_name,
|
|
287
|
+
"interface",
|
|
288
|
+
file_str,
|
|
289
|
+
node,
|
|
290
|
+
doc,
|
|
291
|
+
signature=fields["signature"],
|
|
292
|
+
decorators=fields["decorators"],
|
|
293
|
+
is_exported=fields["is_exported"],
|
|
294
|
+
visibility=fields["visibility"],
|
|
295
|
+
qualified_name=trait_name,
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
# Recurse into trait body to emit each function_item as a method.
|
|
299
|
+
body = node.child_by_field_name("body")
|
|
300
|
+
if body is not None:
|
|
301
|
+
for child in body.children:
|
|
302
|
+
_walk(child, impl_type=trait_name)
|
|
303
|
+
|
|
304
|
+
elif node.type == "mod_item":
|
|
305
|
+
# Traverse into mod bodies but do NOT emit the mod itself as a symbol.
|
|
306
|
+
body = node.child_by_field_name("body")
|
|
307
|
+
if body is not None:
|
|
308
|
+
for child in body.children:
|
|
309
|
+
_walk(child, impl_type=None)
|
|
310
|
+
|
|
311
|
+
else:
|
|
312
|
+
for child in node.children:
|
|
313
|
+
_walk(child, impl_type)
|
|
314
|
+
|
|
315
|
+
for child in root.children:
|
|
316
|
+
_walk(child, impl_type=None)
|
|
317
|
+
|
|
318
|
+
return symbols
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _extract_edges_rust(root: Node, filepath: Path) -> list[Edge]:
|
|
322
|
+
"""Extract import, call, and holds edges from a Rust AST.
|
|
323
|
+
|
|
324
|
+
Import heuristic:
|
|
325
|
+
use std::io::Write → target = 'Write'
|
|
326
|
+
use std::io::{Read, Write} → targets 'Read', 'Write'
|
|
327
|
+
use name → target = 'name'
|
|
328
|
+
use foo as bar → target = 'foo' (real name, not alias)
|
|
329
|
+
|
|
330
|
+
Call heuristic:
|
|
331
|
+
call_expression where 'function' is identifier → bare call → target = identifier.
|
|
332
|
+
call_expression where 'function' is field_expression → recv.method() → capture receiver.
|
|
333
|
+
|
|
334
|
+
Tier B B5: when SEAM_TYPE_INFERENCE is on, field_expression calls are resolved to
|
|
335
|
+
'Type.method' qualified targets by looking up the receiver in the per-function scope
|
|
336
|
+
(params + let_declarations). Also handles self.method() → 'Type.method'.
|
|
337
|
+
|
|
338
|
+
Slice #78: when SEAM_COMPOSITION_EDGES is on, struct_item nodes emit holds edges
|
|
339
|
+
for each plain user-type field. Happens alongside the Tier B field pre-scan.
|
|
340
|
+
|
|
341
|
+
A3 Slice 3: when SEAM_FIELD_ACCESS_EDGES is on, field_expression nodes that are
|
|
342
|
+
NOT in call position emit reads/writes edges for struct field accesses.
|
|
343
|
+
"""
|
|
344
|
+
edges: list[Edge] = []
|
|
345
|
+
file_str = str(filepath)
|
|
346
|
+
file_stem = filepath.stem
|
|
347
|
+
infer = config.SEAM_TYPE_INFERENCE == "on"
|
|
348
|
+
composition_on = config.SEAM_COMPOSITION_EDGES == "on"
|
|
349
|
+
field_access_on = config.SEAM_FIELD_ACCESS_EDGES == "on"
|
|
350
|
+
param_edges_on = config.SEAM_PARAM_EDGES == "on"
|
|
351
|
+
|
|
352
|
+
# struct_fields: struct name → field type map; pre-scanned so impl methods can see them.
|
|
353
|
+
struct_fields: dict[str, dict[str, str]] = {}
|
|
354
|
+
|
|
355
|
+
def _walk(
|
|
356
|
+
node: Node,
|
|
357
|
+
var_types: dict[str, str],
|
|
358
|
+
impl_type: str | None,
|
|
359
|
+
) -> None:
|
|
360
|
+
ntype = node.type
|
|
361
|
+
|
|
362
|
+
if ntype == "use_declaration":
|
|
363
|
+
_handle_rust_use(node, file_str, file_stem, edges)
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
if ntype == "struct_item":
|
|
367
|
+
name_node = node.child_by_field_name("name")
|
|
368
|
+
if name_node:
|
|
369
|
+
sname = _text(name_node).strip()
|
|
370
|
+
if sname:
|
|
371
|
+
# Pre-scan struct fields when type inference OR field access is on.
|
|
372
|
+
# WHY: field_access_on needs struct_fields for receiver resolution
|
|
373
|
+
# (self.field → Account.field requires knowing field types). Even when
|
|
374
|
+
# SEAM_TYPE_INFERENCE is off, we still need the pre-scan so that
|
|
375
|
+
# extract_field_accesses_rust can resolve self to the enclosing type.
|
|
376
|
+
if infer or field_access_on:
|
|
377
|
+
struct_fields[sname] = scan_class_fields_rust(node)
|
|
378
|
+
# Slice #78: emit holds edges for each plain user-type field.
|
|
379
|
+
# WHY here: struct_item is where field declarations live in Rust
|
|
380
|
+
# (unlike Python/TS where fields are in the class body that is also
|
|
381
|
+
# walked for method edges). struct_item is a top-level item — safe
|
|
382
|
+
# to emit here without worrying about nested scopes.
|
|
383
|
+
if composition_on:
|
|
384
|
+
for held_type, held_line in collect_composition_types_rust(node):
|
|
385
|
+
edges.append(
|
|
386
|
+
Edge(
|
|
387
|
+
source=sname,
|
|
388
|
+
target=held_type,
|
|
389
|
+
kind="holds",
|
|
390
|
+
file=file_str,
|
|
391
|
+
line=held_line,
|
|
392
|
+
confidence="INFERRED",
|
|
393
|
+
receiver=None,
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
if ntype == "impl_item":
|
|
398
|
+
# _rust_impl_type_name returns None when the type field is absent — no guard needed.
|
|
399
|
+
new_impl_type = _rust_impl_type_name(node)
|
|
400
|
+
body = node.child_by_field_name("body")
|
|
401
|
+
if body is not None:
|
|
402
|
+
for child in body.children:
|
|
403
|
+
_walk(child, {}, new_impl_type)
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
if ntype == "function_item":
|
|
407
|
+
# Build a fresh per-function scope seeded with struct fields (Layer 1).
|
|
408
|
+
# WHY seed from struct_fields: in Rust, struct fields and impl methods are
|
|
409
|
+
# in separate AST nodes (struct_item vs impl_item). The pre-scan at struct_item
|
|
410
|
+
# populates struct_fields[TypeName]. Here, when inside an impl block for that
|
|
411
|
+
# type, we seed var_types from it so `self.field` lookups can resolve the
|
|
412
|
+
# field's declared type — equivalent to the Python class-body pre-scan.
|
|
413
|
+
# WHY fresh dict: same reason as Go/Python — parameter bindings must not leak
|
|
414
|
+
# between methods sharing the same impl block.
|
|
415
|
+
new_types: dict[str, str] = {}
|
|
416
|
+
if infer:
|
|
417
|
+
if impl_type and impl_type in struct_fields:
|
|
418
|
+
new_types.update(struct_fields[impl_type])
|
|
419
|
+
record_rust_param_types(node, new_types)
|
|
420
|
+
# A3: also seed for field-access resolution even when infer is off.
|
|
421
|
+
# WHY: field-access resolution uses var_types for receiver lookup;
|
|
422
|
+
# without the struct field seed, self.field won't resolve to Type.field.
|
|
423
|
+
if field_access_on and not infer:
|
|
424
|
+
if impl_type and impl_type in struct_fields:
|
|
425
|
+
new_types.update(struct_fields[impl_type])
|
|
426
|
+
record_rust_param_types(node, new_types)
|
|
427
|
+
# 'uses' edges: function/method references plain user types as params.
|
|
428
|
+
if param_edges_on:
|
|
429
|
+
_uses_fn = _node_name(node)
|
|
430
|
+
if _uses_fn:
|
|
431
|
+
_uses_src = f"{impl_type}.{_uses_fn}" if impl_type else _uses_fn
|
|
432
|
+
for ptype, pline in collect_param_types_rust(node):
|
|
433
|
+
edges.append(Edge(
|
|
434
|
+
source=_uses_src, target=ptype, kind="uses",
|
|
435
|
+
file=file_str, line=pline, confidence="INFERRED", receiver=None,
|
|
436
|
+
))
|
|
437
|
+
body = node.child_by_field_name("body")
|
|
438
|
+
if body is not None:
|
|
439
|
+
for child in body.children:
|
|
440
|
+
_walk(child, new_types, impl_type)
|
|
441
|
+
# A3: emit field-access (reads/writes) edges for this function body.
|
|
442
|
+
# Done AFTER the body walk so that record_rust_local_types has accumulated
|
|
443
|
+
# local bindings into new_types (incremental during the walk above).
|
|
444
|
+
if field_access_on and body is not None:
|
|
445
|
+
fn_name = _node_name(node)
|
|
446
|
+
if fn_name:
|
|
447
|
+
source_fn = (
|
|
448
|
+
f"{impl_type}.{fn_name}" if impl_type else fn_name
|
|
449
|
+
)
|
|
450
|
+
for _src, target_field, mode, fa_line in extract_field_accesses_rust(
|
|
451
|
+
body, source_fn, impl_type, new_types
|
|
452
|
+
):
|
|
453
|
+
edges.append(Edge(
|
|
454
|
+
source=source_fn,
|
|
455
|
+
target=target_field,
|
|
456
|
+
kind=mode,
|
|
457
|
+
file=file_str,
|
|
458
|
+
line=fa_line,
|
|
459
|
+
confidence="INFERRED",
|
|
460
|
+
receiver=None,
|
|
461
|
+
))
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
if infer and ntype == "let_declaration":
|
|
465
|
+
record_rust_local_types(node, var_types)
|
|
466
|
+
|
|
467
|
+
# Tier B B6: struct_expression (Foo { ... }) → instantiates edge.
|
|
468
|
+
if ntype == "struct_expression":
|
|
469
|
+
type_id = node.child_by_field_name("name")
|
|
470
|
+
if type_id is not None:
|
|
471
|
+
type_name = _text(type_id)
|
|
472
|
+
if type_name:
|
|
473
|
+
source = _find_enclosing_function(node, "rust")
|
|
474
|
+
if source is not None:
|
|
475
|
+
edges.append(
|
|
476
|
+
Edge(
|
|
477
|
+
source=source,
|
|
478
|
+
target=type_name,
|
|
479
|
+
kind="instantiates",
|
|
480
|
+
file=file_str,
|
|
481
|
+
line=node.start_point[0] + 1,
|
|
482
|
+
confidence="INFERRED",
|
|
483
|
+
receiver=None,
|
|
484
|
+
)
|
|
485
|
+
)
|
|
486
|
+
for child in node.children:
|
|
487
|
+
_walk(child, var_types, impl_type)
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
if ntype == "call_expression":
|
|
491
|
+
func_child = node.child_by_field_name("function")
|
|
492
|
+
callee_name: str | None = None
|
|
493
|
+
recv_text: str | None = None
|
|
494
|
+
|
|
495
|
+
if func_child and func_child.type == "identifier":
|
|
496
|
+
callee_name = _text(func_child)
|
|
497
|
+
elif func_child and func_child.type == "field_expression":
|
|
498
|
+
value_node = func_child.child_by_field_name("value")
|
|
499
|
+
field_node = func_child.child_by_field_name("field")
|
|
500
|
+
if field_node is not None and field_node.type == "field_identifier":
|
|
501
|
+
callee_name = _text(field_node)
|
|
502
|
+
if value_node is not None:
|
|
503
|
+
recv_text = _text(value_node)
|
|
504
|
+
elif func_child and func_child.type == "scoped_identifier":
|
|
505
|
+
# Tier B B6: Type::new() → instantiates edge.
|
|
506
|
+
name_node = func_child.child_by_field_name("name")
|
|
507
|
+
if name_node is not None and _text(name_node) == "new":
|
|
508
|
+
path_node = func_child.child_by_field_name("path")
|
|
509
|
+
if path_node is not None:
|
|
510
|
+
type_name = _text(path_node)
|
|
511
|
+
if type_name:
|
|
512
|
+
source = _find_enclosing_function(node, "rust")
|
|
513
|
+
if source is not None:
|
|
514
|
+
edges.append(
|
|
515
|
+
Edge(
|
|
516
|
+
source=source,
|
|
517
|
+
target=type_name,
|
|
518
|
+
kind="instantiates",
|
|
519
|
+
file=file_str,
|
|
520
|
+
line=node.start_point[0] + 1,
|
|
521
|
+
confidence="INFERRED",
|
|
522
|
+
receiver=None,
|
|
523
|
+
)
|
|
524
|
+
)
|
|
525
|
+
for child in node.children:
|
|
526
|
+
_walk(child, var_types, impl_type)
|
|
527
|
+
return
|
|
528
|
+
|
|
529
|
+
if callee_name:
|
|
530
|
+
final_target = callee_name
|
|
531
|
+
if infer and recv_text is not None:
|
|
532
|
+
resolved_type = resolve_receiver_type_ext(
|
|
533
|
+
recv_text, impl_type, var_types, _RUST_SELF_NAMES
|
|
534
|
+
)
|
|
535
|
+
if resolved_type:
|
|
536
|
+
final_target = f"{resolved_type}.{callee_name}"
|
|
537
|
+
|
|
538
|
+
source = _find_enclosing_function(node, "rust")
|
|
539
|
+
if source is not None:
|
|
540
|
+
edges.append(
|
|
541
|
+
Edge(
|
|
542
|
+
source=source,
|
|
543
|
+
target=final_target,
|
|
544
|
+
kind="call",
|
|
545
|
+
file=file_str,
|
|
546
|
+
line=node.start_point[0] + 1,
|
|
547
|
+
confidence="INFERRED",
|
|
548
|
+
receiver=recv_text,
|
|
549
|
+
)
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
for child in node.children:
|
|
553
|
+
_walk(child, var_types, impl_type)
|
|
554
|
+
|
|
555
|
+
for child in root.children:
|
|
556
|
+
_walk(child, {}, None)
|
|
557
|
+
|
|
558
|
+
return edges
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _handle_rust_use(use_node: Node, file_str: str, file_stem: str, edges: list[Edge]) -> None:
|
|
562
|
+
"""Extract import edges from a Rust use_declaration node.
|
|
563
|
+
|
|
564
|
+
Emits one edge per imported name (the REAL exported name, never the alias):
|
|
565
|
+
use ident → 'ident'
|
|
566
|
+
use path::ident → 'ident' (rightmost segment of scoped_identifier)
|
|
567
|
+
use path::{A, B} → 'A', 'B' (each member of the use_list)
|
|
568
|
+
use path::* → skipped (glob — no single resolvable target)
|
|
569
|
+
use foo as bar → 'foo' (use_as_clause: real name, not alias 'bar')
|
|
570
|
+
"""
|
|
571
|
+
line = use_node.start_point[0] + 1
|
|
572
|
+
arg = use_node.child_by_field_name("argument")
|
|
573
|
+
if arg is None:
|
|
574
|
+
return
|
|
575
|
+
|
|
576
|
+
def _emit(target: str) -> None:
|
|
577
|
+
edges.append(
|
|
578
|
+
Edge(
|
|
579
|
+
source=file_stem,
|
|
580
|
+
target=target,
|
|
581
|
+
kind="import",
|
|
582
|
+
file=file_str,
|
|
583
|
+
line=line,
|
|
584
|
+
confidence="INFERRED",
|
|
585
|
+
receiver=None,
|
|
586
|
+
)
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
def _collect(node: Node) -> None:
|
|
590
|
+
if node.type == "identifier":
|
|
591
|
+
_emit(_text(node))
|
|
592
|
+
elif node.type == "scoped_identifier":
|
|
593
|
+
name_node = node.child_by_field_name("name")
|
|
594
|
+
if name_node and name_node.type == "identifier":
|
|
595
|
+
_emit(_text(name_node))
|
|
596
|
+
elif node.type == "use_as_clause":
|
|
597
|
+
path_node = node.child_by_field_name("path")
|
|
598
|
+
if path_node is not None:
|
|
599
|
+
if path_node.type == "identifier":
|
|
600
|
+
_emit(_text(path_node))
|
|
601
|
+
elif path_node.type == "scoped_identifier":
|
|
602
|
+
name_node = path_node.child_by_field_name("name")
|
|
603
|
+
if name_node and name_node.type == "identifier":
|
|
604
|
+
_emit(_text(name_node))
|
|
605
|
+
elif node.type in ("use_list", "use_wildcard"):
|
|
606
|
+
if node.type == "use_wildcard":
|
|
607
|
+
return
|
|
608
|
+
for child in node.named_children:
|
|
609
|
+
_collect(child)
|
|
610
|
+
elif node.type == "scoped_use_list":
|
|
611
|
+
lst = node.child_by_field_name("list")
|
|
612
|
+
if lst is not None:
|
|
613
|
+
for child in lst.named_children:
|
|
614
|
+
_collect(child)
|
|
615
|
+
|
|
616
|
+
_collect(arg)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _extract_comments_rust(root: Node, filepath: Path) -> list[Comment]:
|
|
620
|
+
"""Walk a Rust AST and extract semantic comment markers.
|
|
621
|
+
|
|
622
|
+
Rust comment node types:
|
|
623
|
+
line_comment — covers // (plain), /// (outer doc), //! (inner doc)
|
|
624
|
+
block_comment — /* */ blocks
|
|
625
|
+
"""
|
|
626
|
+
comments: list[Comment] = []
|
|
627
|
+
|
|
628
|
+
def _walk(node: Node) -> None:
|
|
629
|
+
if node.type == "line_comment":
|
|
630
|
+
raw = _text(node)
|
|
631
|
+
base_row = node.start_point[0] + 1
|
|
632
|
+
body = raw.lstrip("/").lstrip("!").strip()
|
|
633
|
+
result = _match_marker(body)
|
|
634
|
+
if result is not None:
|
|
635
|
+
marker, text = result
|
|
636
|
+
comments.append(Comment(marker=marker, text=text, line=base_row))
|
|
637
|
+
|
|
638
|
+
elif node.type == "block_comment":
|
|
639
|
+
raw = _text(node)
|
|
640
|
+
base_row = node.start_point[0] + 1
|
|
641
|
+
for offset, body in _block_comment_lines(raw):
|
|
642
|
+
result = _match_marker(body)
|
|
643
|
+
if result is not None:
|
|
644
|
+
marker, text = result
|
|
645
|
+
comments.append(Comment(marker=marker, text=text, line=base_row + offset))
|
|
646
|
+
|
|
647
|
+
for child in node.children:
|
|
648
|
+
_walk(child)
|
|
649
|
+
|
|
650
|
+
for child in root.children:
|
|
651
|
+
_walk(child)
|
|
652
|
+
|
|
653
|
+
return comments
|