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,737 @@
1
+ """Field-access edge helper — Go and Rust read/write classification.
2
+
3
+ LAYER: leaf — imports only stdlib + tree_sitter + the existing resolve_receiver_type
4
+ helpers from graph_scope_infer_ext. Never imports from graph.py, db.py, or any
5
+ non-leaf seam module.
6
+
7
+ LAYERING:
8
+ graph_common (leaf — no seam deps)
9
+ graph_scope_infer_ext (leaf — Go/Rust receiver-type inference)
10
+
11
+ field_access_go_rust (this file — field-access classification for Go + Rust)
12
+
13
+ field_access (re-exports extract_field_accesses_go/rust +
14
+ collect_field_symbols_go/rust for backward compat)
15
+ graph_go (calls those functions via field_access import)
16
+ graph_rust (calls those functions via field_access import)
17
+
18
+ WHY a separate module from field_access.py:
19
+ field_access.py would otherwise exceed the 1000-line limit when Python + TS/JS +
20
+ Go + Rust are all in one file. This follows the graph_scope_infer precedent.
21
+
22
+ Go AST patterns for field accesses:
23
+ selector_expression → <operand>.<field_identifier> — the node for ALL member access
24
+ call_expression function=selector_expression → method call — NOT a field access
25
+ inc_statement → selector_expression ++ → WRITE
26
+ dec_statement → selector_expression -- → WRITE
27
+ assignment_statement → expression_list op expression_list:
28
+ LHS expression_list contains selector_expression → WRITE (any operator: = += -= ...)
29
+ RHS expression_list contains selector_expression → READ
30
+
31
+ Rust AST patterns for field accesses:
32
+ field_expression → <value>.<field_identifier> — the node for ALL member access
33
+ call_expression function=field_expression → method call — NOT a field access
34
+ assignment_expression → LHS field_expression → WRITE; RHS → reads
35
+ compound_assignment_expr → first child field_expression → WRITE
36
+
37
+ NEVER RAISES: all public functions have a backstop try/except and return [] on error.
38
+ """
39
+
40
+ import logging
41
+
42
+ from tree_sitter import Node
43
+
44
+ from seam.indexer.graph_common import _text
45
+ from seam.indexer.graph_scope_infer_ext import (
46
+ _RUST_SELF_NAMES,
47
+ resolve_receiver_type_ext,
48
+ )
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+ # Return type for the public API: (source_fn, target_field, mode, line)
53
+ # mode is "reads" or "writes"
54
+ FieldAccess = tuple[str, str, str, int]
55
+
56
+
57
+ # ══════════════════════════════════════════════════════════════════════════════
58
+ # Go field-access classification (A3 Slice 3)
59
+ # ══════════════════════════════════════════════════════════════════════════════
60
+ #
61
+ # Go AST patterns for field accesses:
62
+ # selector_expression → <operand>.<field_identifier> — the node for ALL member access
63
+ # call_expression function=selector_expression → method call — NOT a field access
64
+ # inc_statement → selector_expression ++ → WRITE
65
+ # dec_statement → selector_expression -- → WRITE
66
+ # assignment_statement → expression_list op expression_list:
67
+ # LHS expression_list contains selector_expression → WRITE (any operator: = += -= ...)
68
+ # RHS expression_list contains selector_expression → READ
69
+ #
70
+ # Receiver resolution:
71
+ # Go has no universal self keyword. The receiver variable is arbitrary (r, a, s, etc.).
72
+ # We use resolve_receiver_type_ext with empty frozenset() as self_names, so ALL type
73
+ # resolution comes from param/local scope bindings (record_go_param_types pre-populates
74
+ # var_types for the receiver variable like 'r' → 'Account').
75
+ # Unresolvable receivers produce bare field names (AMBIGUOUS at read time).
76
+
77
+
78
+ def extract_field_accesses_go(
79
+ func_body: Node,
80
+ source_fn: str,
81
+ impl_type: str | None,
82
+ var_types: dict[str, str],
83
+ ) -> list[FieldAccess]:
84
+ """Extract field accesses (reads and writes) from a Go function/method body block.
85
+
86
+ Args:
87
+ func_body: The 'block' Node of the function/method body.
88
+ source_fn: Qualified name of the enclosing function, e.g. 'Account.Get'.
89
+ impl_type: Enclosing struct name for methods (e.g. 'Account'), or None for
90
+ top-level functions.
91
+ var_types: Scope map: param/local name → type name (populated before this call
92
+ by record_go_param_types + record_go_local_types).
93
+
94
+ Returns:
95
+ List of (source_fn, target_field, mode, line) tuples.
96
+ target_field = 'Type.Field' when receiver resolves; bare 'Field' when unknown.
97
+ mode = 'reads' | 'writes'.
98
+ line = 1-based source line.
99
+ Returns [] on any error.
100
+
101
+ Never raises.
102
+ """
103
+ result: list[FieldAccess] = []
104
+ try:
105
+ _go_walk_block(func_body, source_fn, impl_type, var_types, result)
106
+ except Exception as exc: # noqa: BLE001
107
+ logger.debug(
108
+ "extract_field_accesses_go: failed for source=%r: %r", source_fn, exc
109
+ )
110
+ return result
111
+
112
+
113
+ def _go_walk_block(
114
+ node: Node,
115
+ source_fn: str,
116
+ impl_type: str | None,
117
+ var_types: dict[str, str],
118
+ result: list[FieldAccess],
119
+ ) -> None:
120
+ """Recursively walk a Go block/statement collecting field accesses.
121
+
122
+ Does NOT recurse into nested function literals (they have their own scope).
123
+ """
124
+ for child in node.children:
125
+ _go_walk_stmt(child, source_fn, impl_type, var_types, result)
126
+
127
+
128
+ def _go_walk_stmt(
129
+ node: Node,
130
+ source_fn: str,
131
+ impl_type: str | None,
132
+ var_types: dict[str, str],
133
+ result: list[FieldAccess],
134
+ ) -> None:
135
+ """Walk a single Go statement node for field accesses.
136
+
137
+ Handles:
138
+ assignment_statement: LHS selector_expression → write; RHS → reads.
139
+ inc_statement / dec_statement: selector_expression → write.
140
+ All other statements: recurse, collecting reads from selector_expressions.
141
+ Skips nested function_literal nodes (their own scope).
142
+ """
143
+ t = node.type
144
+
145
+ # Skip nested function literals — they have their own scope.
146
+ if t == "func_literal":
147
+ return
148
+
149
+ if t == "assignment_statement":
150
+ _go_handle_assignment(node, source_fn, impl_type, var_types, result)
151
+ return
152
+
153
+ if t in ("inc_statement", "dec_statement"):
154
+ _go_handle_inc_dec(node, source_fn, impl_type, var_types, result)
155
+ return
156
+
157
+ # A bare selector_expression (not in call position and not in assignment LHS)
158
+ # is a read. The parent/context check is handled in _go_emit_read_if_not_in_call.
159
+ if t == "selector_expression":
160
+ _go_emit_read_if_not_in_call(node, source_fn, impl_type, var_types, result)
161
+ return
162
+
163
+ # Recurse into all other node types.
164
+ for child in node.children:
165
+ _go_walk_stmt(child, source_fn, impl_type, var_types, result)
166
+
167
+
168
+ def _go_handle_assignment(
169
+ node: Node,
170
+ source_fn: str,
171
+ impl_type: str | None,
172
+ var_types: dict[str, str],
173
+ result: list[FieldAccess],
174
+ ) -> None:
175
+ """Handle Go assignment_statement: LHS selector_expression → write; RHS → reads.
176
+
177
+ Go assignment_statement structure:
178
+ expression_list op expression_list
179
+ where op is '=', '+=', '-=', '*=', '/=', '%=', etc.
180
+
181
+ The first child is the LHS expression_list (write targets).
182
+ The last child is the RHS expression_list (read values).
183
+
184
+ WHY iterate expression_list children: multi-assignment (a.X, a.Y = 1, 2) has
185
+ multiple selector_expressions in the LHS expression_list.
186
+ """
187
+ children = node.children
188
+ if not children:
189
+ return
190
+
191
+ # Find the operator token position to split LHS / RHS.
192
+ # Go grammar: assignment_statement has the operator as a named token
193
+ # (type '=', '+=', '-=', etc.) between two expression_list nodes.
194
+ lhs_node = None
195
+ rhs_node = None
196
+ # The first named child tends to be the LHS expression_list.
197
+ for i, child in enumerate(children):
198
+ if child.type == "expression_list":
199
+ if lhs_node is None:
200
+ lhs_node = child
201
+ else:
202
+ rhs_node = child
203
+ break
204
+
205
+ # LHS: each selector_expression in the expression_list is a write target.
206
+ if lhs_node is not None:
207
+ for child in lhs_node.children:
208
+ if child.type == "selector_expression":
209
+ acc = _go_classify_selector(child, impl_type, var_types)
210
+ if acc is not None:
211
+ target, _ = acc
212
+ result.append((source_fn, target, "writes", child.start_point[0] + 1))
213
+
214
+ # RHS: collect reads from expression_list.
215
+ if rhs_node is not None:
216
+ _go_collect_reads_recursive(rhs_node, source_fn, impl_type, var_types, result)
217
+
218
+
219
+ def _go_handle_inc_dec(
220
+ node: Node,
221
+ source_fn: str,
222
+ impl_type: str | None,
223
+ var_types: dict[str, str],
224
+ result: list[FieldAccess],
225
+ ) -> None:
226
+ """Handle Go inc_statement (r.Field++) and dec_statement (r.Field--).
227
+
228
+ Both ++ and -- are writes (they mutate the field value).
229
+ Grammar: inc_statement = selector_expression '++' (or '--')
230
+ The selector_expression is the first child.
231
+ """
232
+ for child in node.children:
233
+ if child.type == "selector_expression":
234
+ acc = _go_classify_selector(child, impl_type, var_types)
235
+ if acc is not None:
236
+ target, _ = acc
237
+ result.append((source_fn, target, "writes", child.start_point[0] + 1))
238
+ break # Only one operand in inc/dec
239
+
240
+
241
+ def _go_emit_read_if_not_in_call(
242
+ node: Node,
243
+ source_fn: str,
244
+ impl_type: str | None,
245
+ var_types: dict[str, str],
246
+ result: list[FieldAccess],
247
+ ) -> None:
248
+ """Emit a reads edge for a Go selector_expression NOT in call position.
249
+
250
+ A selector_expression is in call position when its parent is call_expression
251
+ AND it IS the 'function' field of that call. We skip those (method calls).
252
+
253
+ WHY start_point comparison: same reasoning as Python/TS — tree-sitter creates
254
+ new Node objects on each field access; start_point is the stable identity.
255
+ """
256
+ parent = node.parent
257
+ if parent is not None and parent.type == "call_expression":
258
+ func_field = parent.child_by_field_name("function")
259
+ if func_field is not None and func_field.start_point == node.start_point:
260
+ return # Call position — do not emit field read
261
+
262
+ acc = _go_classify_selector(node, impl_type, var_types)
263
+ if acc is not None:
264
+ target, _ = acc
265
+ result.append((source_fn, target, "reads", node.start_point[0] + 1))
266
+
267
+
268
+ def _go_collect_reads_recursive(
269
+ node: Node,
270
+ source_fn: str,
271
+ impl_type: str | None,
272
+ var_types: dict[str, str],
273
+ result: list[FieldAccess],
274
+ ) -> None:
275
+ """Recursively collect reads from a Go expression node.
276
+
277
+ Walks the expression tree and emits reads edges for selector_expression nodes
278
+ that are NOT in call position. Skips nested func_literal nodes.
279
+ """
280
+ t = node.type
281
+ if t == "func_literal":
282
+ return
283
+
284
+ if t == "selector_expression":
285
+ _go_emit_read_if_not_in_call(node, source_fn, impl_type, var_types, result)
286
+ # Do NOT recurse into selector's children — the operand part ('r' in 'r.X')
287
+ # is not itself a field access target.
288
+ return
289
+
290
+ for child in node.children:
291
+ _go_collect_reads_recursive(child, source_fn, impl_type, var_types, result)
292
+
293
+
294
+ def _go_classify_selector(
295
+ node: Node,
296
+ impl_type: str | None,
297
+ var_types: dict[str, str],
298
+ ) -> tuple[str, str | None] | None:
299
+ """Resolve a Go selector_expression node to (qualified_target, receiver_text).
300
+
301
+ Go selector_expression:
302
+ operand: the receiver expression (identifier or more complex)
303
+ field: field_identifier — the accessed field/method name
304
+
305
+ Returns None when the node lacks the expected shape.
306
+ Returns (target, receiver_text) where:
307
+ - target = 'Type.Field' when receiver resolves (EXTRACTED confidence)
308
+ - target = bare 'Field' when unresolvable (AMBIGUOUS confidence)
309
+
310
+ Conservatism contract: NEVER emit a wrong qualified target.
311
+ Never raises (returns None on any exception).
312
+ """
313
+ try:
314
+ operand = node.child_by_field_name("operand")
315
+ field = node.child_by_field_name("field")
316
+ if operand is None or field is None:
317
+ return None
318
+
319
+ # Only field_identifier fields are real struct field accesses.
320
+ # A type_identifier in selector position means pkg.SomeType or pkg.SomeFunc —
321
+ # a package-level reference, not a struct field access. We skip those to avoid
322
+ # emitting phantom field edges for cross-package qualified identifiers like
323
+ # `fmt.Println` (selector_expression with type_identifier for 'Println').
324
+ if field.type != "field_identifier":
325
+ return None
326
+
327
+ field_name = _text(field)
328
+ if not field_name:
329
+ return None
330
+
331
+ receiver_text = _text(operand)
332
+
333
+ # Go has no universal self — pass empty frozenset() for self_names.
334
+ # The receiver type comes purely from var_types (record_go_param_types +
335
+ # the receiver-binding injected by _go_emit_field_access_edges), not from
336
+ # any self-alias convention. Without the receiver binding in var_types,
337
+ # `r.Balance` in a method `func (r *Account) M()` would produce a bare
338
+ # 'Balance' edge instead of the qualified 'Account.Balance'.
339
+ resolved_type = resolve_receiver_type_ext(
340
+ receiver_text, impl_type, var_types, frozenset()
341
+ )
342
+
343
+ if resolved_type is not None:
344
+ return f"{resolved_type}.{field_name}", receiver_text
345
+ else:
346
+ return field_name, receiver_text
347
+
348
+ except Exception as exc: # noqa: BLE001
349
+ logger.debug("_go_classify_selector: failed: %r", exc)
350
+ return None
351
+
352
+
353
+ def collect_field_symbols_go(
354
+ struct_node: Node,
355
+ struct_name: str,
356
+ ) -> list[tuple[str, int]]:
357
+ """Collect (qualified_field_name, line) pairs from a Go struct_type node.
358
+
359
+ Returns field symbols for ALL field declarations in the struct body.
360
+ Unlike the holds collector (collect_composition_types_go), we do NOT
361
+ filter by PascalCase or user-type constraints — ALL fields are indexed
362
+ because 'who writes balance' must work for primitive-typed fields too.
363
+
364
+ Args:
365
+ struct_node: The struct_type node (the 'type' field of a type_spec).
366
+ struct_name: Name of the enclosing struct (e.g. 'Account').
367
+
368
+ Returns [] on any error. Never raises.
369
+ """
370
+ try:
371
+ result: list[tuple[str, int]] = []
372
+
373
+ # struct_type contains a field_declaration_list child.
374
+ for child in struct_node.children:
375
+ if child.type != "field_declaration_list":
376
+ continue
377
+ for field_decl in child.named_children:
378
+ if field_decl.type != "field_declaration":
379
+ continue
380
+ try:
381
+ _go_collect_field_decl(field_decl, struct_name, result)
382
+ except Exception: # noqa: BLE001
383
+ pass
384
+ break # Only one field_declaration_list per struct
385
+
386
+ return result
387
+
388
+ except Exception as exc: # noqa: BLE001
389
+ logger.debug("collect_field_symbols_go: failed for struct %r: %r", struct_name, exc)
390
+ return []
391
+
392
+
393
+ def _go_collect_field_decl(
394
+ field_decl: Node,
395
+ struct_name: str,
396
+ result: list[tuple[str, int]],
397
+ ) -> None:
398
+ """Extract a field symbol from a single Go field_declaration node.
399
+
400
+ Go field_declaration has:
401
+ - One or more 'name' field_identifiers (the field names)
402
+ - A 'type' field (the declared type — any type, not filtered)
403
+
404
+ Multi-name declarations ('X, Y int') produce one symbol per name.
405
+
406
+ WHY no type filter: unlike holds (which only cares about composition with
407
+ user-defined types), field symbols index ALL fields regardless of type.
408
+ A field 'balance: int' is just as important as 'client: Client'.
409
+ """
410
+ # Collect all field_identifier children as field names.
411
+ # The 'name' field in Go field_declaration is either a single field_identifier
412
+ # or we find all field_identifier children for multi-name decls.
413
+ for child in field_decl.children:
414
+ if child.type == "field_identifier":
415
+ field_name = _text(child).strip()
416
+ if field_name:
417
+ qualified = f"{struct_name}.{field_name}"
418
+ result.append((qualified, field_decl.start_point[0] + 1))
419
+
420
+
421
+ # ══════════════════════════════════════════════════════════════════════════════
422
+ # Rust field-access classification (A3 Slice 3)
423
+ # ══════════════════════════════════════════════════════════════════════════════
424
+ #
425
+ # Rust AST patterns for field accesses:
426
+ # field_expression → <value>.<field_identifier> — the node for ALL member access
427
+ # call_expression function=field_expression → method call — NOT a field access
428
+ # assignment_expression → LHS field_expression → WRITE; RHS → reads
429
+ # compound_assignment_expr → first child field_expression → WRITE
430
+ #
431
+ # Receiver resolution:
432
+ # Rust uses self/Self as conventional receiver aliases (_RUST_SELF_NAMES).
433
+ # Other variables are resolved from var_types (seeded from struct_fields + params).
434
+
435
+
436
+ def extract_field_accesses_rust(
437
+ func_body: Node,
438
+ source_fn: str,
439
+ impl_type: str | None,
440
+ var_types: dict[str, str],
441
+ ) -> list[FieldAccess]:
442
+ """Extract field accesses (reads and writes) from a Rust function body block.
443
+
444
+ Args:
445
+ func_body: The 'block' Node of the function body.
446
+ source_fn: Qualified name of the enclosing function, e.g. 'Account.deposit'.
447
+ impl_type: Enclosing struct/impl type name (e.g. 'Account'), or None for
448
+ top-level functions.
449
+ var_types: Scope map: param/local name → type name (seeded from struct_fields
450
+ + record_rust_param_types + record_rust_local_types).
451
+
452
+ Returns:
453
+ List of (source_fn, target_field, mode, line) tuples.
454
+ Returns [] on any error.
455
+
456
+ Never raises.
457
+ """
458
+ result: list[FieldAccess] = []
459
+ try:
460
+ _rust_walk_block(func_body, source_fn, impl_type, var_types, result)
461
+ except Exception as exc: # noqa: BLE001
462
+ logger.debug(
463
+ "extract_field_accesses_rust: failed for source=%r: %r", source_fn, exc
464
+ )
465
+ return result
466
+
467
+
468
+ def _rust_walk_block(
469
+ node: Node,
470
+ source_fn: str,
471
+ impl_type: str | None,
472
+ var_types: dict[str, str],
473
+ result: list[FieldAccess],
474
+ ) -> None:
475
+ """Recursively walk a Rust block collecting field accesses.
476
+
477
+ Does NOT recurse into nested closure_expression nodes (their own scope).
478
+ """
479
+ for child in node.children:
480
+ _rust_walk_stmt(child, source_fn, impl_type, var_types, result)
481
+
482
+
483
+ def _rust_walk_stmt(
484
+ node: Node,
485
+ source_fn: str,
486
+ impl_type: str | None,
487
+ var_types: dict[str, str],
488
+ result: list[FieldAccess],
489
+ ) -> None:
490
+ """Walk a single Rust statement or expression for field accesses.
491
+
492
+ Handles:
493
+ expression_statement wrapping assignment_expression / compound_assignment_expr.
494
+ Direct assignment_expression or compound_assignment_expr.
495
+ All other statements: recurse collecting reads from field_expression nodes.
496
+ Skips closure_expression nodes (their own scope).
497
+ """
498
+ t = node.type
499
+
500
+ # Skip closures — they have their own scope.
501
+ if t == "closure_expression":
502
+ return
503
+
504
+ if t == "assignment_expression":
505
+ _rust_handle_assignment(node, source_fn, impl_type, var_types, result)
506
+ return
507
+
508
+ if t == "compound_assignment_expr":
509
+ _rust_handle_compound_assignment(node, source_fn, impl_type, var_types, result)
510
+ return
511
+
512
+ # A bare field_expression (not in call position) is a read.
513
+ if t == "field_expression":
514
+ _rust_emit_read_if_not_in_call(node, source_fn, impl_type, var_types, result)
515
+ return
516
+
517
+ # Recurse into all other nodes.
518
+ for child in node.children:
519
+ _rust_walk_stmt(child, source_fn, impl_type, var_types, result)
520
+
521
+
522
+ def _rust_handle_assignment(
523
+ node: Node,
524
+ source_fn: str,
525
+ impl_type: str | None,
526
+ var_types: dict[str, str],
527
+ result: list[FieldAccess],
528
+ ) -> None:
529
+ """Handle Rust assignment_expression: left field_expression → write; right → reads.
530
+
531
+ Rust grammar:
532
+ assignment_expression = left '=' right
533
+ where left may be a field_expression (self.field = ...).
534
+ """
535
+ left = node.child_by_field_name("left")
536
+ if left is not None and left.type == "field_expression":
537
+ acc = _rust_classify_field_expr(left, impl_type, var_types)
538
+ if acc is not None:
539
+ target, _ = acc
540
+ result.append((source_fn, target, "writes", left.start_point[0] + 1))
541
+
542
+ right = node.child_by_field_name("right")
543
+ if right is not None:
544
+ _rust_collect_reads_recursive(right, source_fn, impl_type, var_types, result)
545
+
546
+
547
+ def _rust_handle_compound_assignment(
548
+ node: Node,
549
+ source_fn: str,
550
+ impl_type: str | None,
551
+ var_types: dict[str, str],
552
+ result: list[FieldAccess],
553
+ ) -> None:
554
+ """Handle Rust compound_assignment_expr (+=, -=, etc.): left → write; right → reads.
555
+
556
+ Rust grammar:
557
+ compound_assignment_expr = left op right
558
+ where 'left' is the field child (tree-sitter uses 'left' field name).
559
+ """
560
+ left = node.child_by_field_name("left")
561
+ if left is not None and left.type == "field_expression":
562
+ acc = _rust_classify_field_expr(left, impl_type, var_types)
563
+ if acc is not None:
564
+ target, _ = acc
565
+ result.append((source_fn, target, "writes", left.start_point[0] + 1))
566
+
567
+ right = node.child_by_field_name("right")
568
+ if right is not None:
569
+ _rust_collect_reads_recursive(right, source_fn, impl_type, var_types, result)
570
+
571
+
572
+ def _rust_emit_read_if_not_in_call(
573
+ node: Node,
574
+ source_fn: str,
575
+ impl_type: str | None,
576
+ var_types: dict[str, str],
577
+ result: list[FieldAccess],
578
+ ) -> None:
579
+ """Emit a reads edge for a Rust field_expression NOT in call position.
580
+
581
+ A field_expression is in call position when its parent is call_expression AND
582
+ it IS the 'function' field of that call. We skip those (method calls).
583
+ """
584
+ parent = node.parent
585
+ if parent is not None and parent.type == "call_expression":
586
+ func_field = parent.child_by_field_name("function")
587
+ if func_field is not None and func_field.start_point == node.start_point:
588
+ return # Call position — do not emit field read
589
+
590
+ acc = _rust_classify_field_expr(node, impl_type, var_types)
591
+ if acc is not None:
592
+ target, _ = acc
593
+ result.append((source_fn, target, "reads", node.start_point[0] + 1))
594
+
595
+
596
+ def _rust_collect_reads_recursive(
597
+ node: Node,
598
+ source_fn: str,
599
+ impl_type: str | None,
600
+ var_types: dict[str, str],
601
+ result: list[FieldAccess],
602
+ ) -> None:
603
+ """Recursively collect reads from a Rust expression node.
604
+
605
+ Skips closure_expression nodes (their own scope).
606
+ """
607
+ t = node.type
608
+ if t == "closure_expression":
609
+ return
610
+
611
+ if t == "field_expression":
612
+ _rust_emit_read_if_not_in_call(node, source_fn, impl_type, var_types, result)
613
+ # Do NOT recurse — the value part ('self' in 'self.field') is not a separate
614
+ # field access target we want to re-emit.
615
+ return
616
+
617
+ for child in node.children:
618
+ _rust_collect_reads_recursive(child, source_fn, impl_type, var_types, result)
619
+
620
+
621
+ def _rust_classify_field_expr(
622
+ node: Node,
623
+ impl_type: str | None,
624
+ var_types: dict[str, str],
625
+ ) -> tuple[str, str | None] | None:
626
+ """Resolve a Rust field_expression node to (qualified_target, receiver_text).
627
+
628
+ Rust field_expression:
629
+ value: the receiver (e.g. 'self', 'other')
630
+ field: field_identifier — the accessed field name
631
+
632
+ Returns None when the node lacks the expected shape.
633
+ Returns (target, receiver_text) where:
634
+ - target = 'Type.field' when receiver resolves (EXTRACTED confidence)
635
+ - target = bare 'field' when unresolvable (AMBIGUOUS confidence)
636
+
637
+ Conservatism contract: NEVER emit a wrong qualified target.
638
+ Never raises (returns None on any exception).
639
+ """
640
+ try:
641
+ value_node = node.child_by_field_name("value")
642
+ field_node = node.child_by_field_name("field")
643
+ if value_node is None or field_node is None:
644
+ return None
645
+
646
+ if field_node.type != "field_identifier":
647
+ return None
648
+
649
+ field_name = _text(field_node)
650
+ if not field_name:
651
+ return None
652
+
653
+ receiver_text = _text(value_node)
654
+
655
+ resolved_type = resolve_receiver_type_ext(
656
+ receiver_text, impl_type, var_types, _RUST_SELF_NAMES
657
+ )
658
+
659
+ if resolved_type is not None:
660
+ return f"{resolved_type}.{field_name}", receiver_text
661
+ else:
662
+ return field_name, receiver_text
663
+
664
+ except Exception as exc: # noqa: BLE001
665
+ logger.debug("_rust_classify_field_expr: failed: %r", exc)
666
+ return None
667
+
668
+
669
+ def collect_field_symbols_rust(
670
+ struct_node: Node,
671
+ struct_name: str,
672
+ ) -> list[tuple[str, int]]:
673
+ """Collect (qualified_field_name, line) pairs from a Rust struct_item node.
674
+
675
+ Returns field symbols for ALL field declarations in the struct body.
676
+ Unlike the holds collector, we do NOT filter by user-type constraints —
677
+ ALL fields are indexed regardless of their type ('balance: i64', 'name: String').
678
+
679
+ Args:
680
+ struct_node: The struct_item node.
681
+ struct_name: Name of the struct (e.g. 'Account').
682
+
683
+ Returns [] on any error. Never raises.
684
+ """
685
+ try:
686
+ result: list[tuple[str, int]] = []
687
+
688
+ # struct_item contains a field_declaration_list child.
689
+ for child in struct_node.children:
690
+ if child.type != "field_declaration_list":
691
+ continue
692
+ for field_decl in child.named_children:
693
+ if field_decl.type != "field_declaration":
694
+ continue
695
+ try:
696
+ _rust_collect_field_decl(field_decl, struct_name, result)
697
+ except Exception: # noqa: BLE001
698
+ pass
699
+ break # Only one field_declaration_list per struct
700
+
701
+ return result
702
+
703
+ except Exception as exc: # noqa: BLE001
704
+ logger.debug("collect_field_symbols_rust: failed for struct %r: %r", struct_name, exc)
705
+ return []
706
+
707
+
708
+ def _rust_collect_field_decl(
709
+ field_decl: Node,
710
+ struct_name: str,
711
+ result: list[tuple[str, int]],
712
+ ) -> None:
713
+ """Extract a field symbol from a single Rust field_declaration node.
714
+
715
+ Rust field_declaration:
716
+ name: field_identifier (the field name)
717
+ type: <type node> (any type)
718
+
719
+ WHY no type filter: same reasoning as Go — ALL fields are indexed so that
720
+ queries like 'who writes balance' work for primitive-typed fields too.
721
+ """
722
+ name_node = field_decl.child_by_field_name("name")
723
+ if name_node is None:
724
+ # Fallback: find first field_identifier child
725
+ for child in field_decl.children:
726
+ if child.type == "field_identifier":
727
+ name_node = child
728
+ break
729
+ if name_node is None:
730
+ return
731
+
732
+ field_name = _text(name_node).strip()
733
+ if not field_name:
734
+ return
735
+
736
+ qualified = f"{struct_name}.{field_name}"
737
+ result.append((qualified, field_decl.start_point[0] + 1))