seam-code 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. seam/__init__.py +3 -0
  2. seam/_data/schema.sql +225 -0
  3. seam/_web/assets/index-BL_tqprR.js +216 -0
  4. seam/_web/assets/index-GTKUhVyD.css +1 -0
  5. seam/_web/index.html +13 -0
  6. seam/analysis/__init__.py +14 -0
  7. seam/analysis/affected.py +254 -0
  8. seam/analysis/builtins.py +966 -0
  9. seam/analysis/byte_budget.py +217 -0
  10. seam/analysis/changes.py +709 -0
  11. seam/analysis/cluster_naming.py +260 -0
  12. seam/analysis/clustering.py +216 -0
  13. seam/analysis/confidence.py +699 -0
  14. seam/analysis/embeddings.py +195 -0
  15. seam/analysis/flows.py +708 -0
  16. seam/analysis/impact.py +444 -0
  17. seam/analysis/imports.py +994 -0
  18. seam/analysis/imports_ext.py +780 -0
  19. seam/analysis/imports_resolve.py +176 -0
  20. seam/analysis/processes.py +453 -0
  21. seam/analysis/relevance.py +155 -0
  22. seam/analysis/rwr.py +129 -0
  23. seam/analysis/staleness.py +328 -0
  24. seam/analysis/steer.py +282 -0
  25. seam/analysis/synthesis.py +253 -0
  26. seam/analysis/synthesis_channels.py +433 -0
  27. seam/analysis/testpaths.py +103 -0
  28. seam/analysis/traversal.py +470 -0
  29. seam/cli/__init__.py +0 -0
  30. seam/cli/install.py +232 -0
  31. seam/cli/main.py +2602 -0
  32. seam/cli/output.py +137 -0
  33. seam/cli/read.py +244 -0
  34. seam/cli/serve.py +145 -0
  35. seam/config.py +551 -0
  36. seam/indexer/__init__.py +0 -0
  37. seam/indexer/cluster_index.py +425 -0
  38. seam/indexer/db.py +496 -0
  39. seam/indexer/embedding_index.py +183 -0
  40. seam/indexer/field_access.py +536 -0
  41. seam/indexer/field_access_c_cpp.py +643 -0
  42. seam/indexer/field_access_ext.py +708 -0
  43. seam/indexer/field_access_ext2.py +408 -0
  44. seam/indexer/field_access_go_rust.py +737 -0
  45. seam/indexer/field_access_php_swift.py +888 -0
  46. seam/indexer/field_access_ts.py +626 -0
  47. seam/indexer/graph.py +321 -0
  48. seam/indexer/graph_c.py +562 -0
  49. seam/indexer/graph_c_cpp.py +39 -0
  50. seam/indexer/graph_common.py +644 -0
  51. seam/indexer/graph_cpp.py +615 -0
  52. seam/indexer/graph_csharp.py +651 -0
  53. seam/indexer/graph_go.py +723 -0
  54. seam/indexer/graph_go_rust.py +39 -0
  55. seam/indexer/graph_java.py +689 -0
  56. seam/indexer/graph_java_csharp.py +38 -0
  57. seam/indexer/graph_php.py +914 -0
  58. seam/indexer/graph_python.py +628 -0
  59. seam/indexer/graph_ruby.py +748 -0
  60. seam/indexer/graph_rust.py +653 -0
  61. seam/indexer/graph_scope_infer.py +902 -0
  62. seam/indexer/graph_scope_infer_ext.py +723 -0
  63. seam/indexer/graph_scope_infer_ext2.py +992 -0
  64. seam/indexer/graph_swift.py +1014 -0
  65. seam/indexer/graph_swift_infer.py +515 -0
  66. seam/indexer/graph_typescript.py +663 -0
  67. seam/indexer/migrations.py +816 -0
  68. seam/indexer/parser.py +204 -0
  69. seam/indexer/pipeline.py +197 -0
  70. seam/indexer/signatures.py +634 -0
  71. seam/indexer/signatures_ext.py +780 -0
  72. seam/indexer/sync.py +287 -0
  73. seam/indexer/synthesis_index.py +291 -0
  74. seam/indexer/tokenize.py +79 -0
  75. seam/installer/__init__.py +67 -0
  76. seam/installer/claude.py +97 -0
  77. seam/installer/codex.py +94 -0
  78. seam/installer/core.py +127 -0
  79. seam/installer/cursor.py +61 -0
  80. seam/installer/guide.py +110 -0
  81. seam/installer/jsonfile.py +85 -0
  82. seam/installer/markdownfile.py +146 -0
  83. seam/installer/tomlfile.py +72 -0
  84. seam/query/__init__.py +0 -0
  85. seam/query/clusters.py +206 -0
  86. seam/query/comments.py +217 -0
  87. seam/query/context.py +293 -0
  88. seam/query/engine.py +940 -0
  89. seam/query/fts.py +328 -0
  90. seam/query/names.py +470 -0
  91. seam/query/pack.py +433 -0
  92. seam/query/semantic.py +339 -0
  93. seam/query/structure.py +727 -0
  94. seam/server/__init__.py +0 -0
  95. seam/server/graph_api.py +437 -0
  96. seam/server/handler_common.py +323 -0
  97. seam/server/impact_handler.py +615 -0
  98. seam/server/mcp.py +556 -0
  99. seam/server/tools.py +697 -0
  100. seam/server/trace_handler.py +184 -0
  101. seam/server/web.py +922 -0
  102. seam/watcher/__init__.py +0 -0
  103. seam/watcher/__main__.py +56 -0
  104. seam/watcher/daemon.py +237 -0
  105. seam_code-0.3.0.dist-info/METADATA +318 -0
  106. seam_code-0.3.0.dist-info/RECORD +109 -0
  107. seam_code-0.3.0.dist-info/WHEEL +4 -0
  108. seam_code-0.3.0.dist-info/entry_points.txt +2 -0
  109. seam_code-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,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