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,708 @@
1
+ """Field-access edge helper — Java and C# classification (façade for C/C++ too).
2
+
3
+ This file contains the Java and C# field-access implementations and re-exports the
4
+ C and C++ implementations from field_access_c_cpp so callers can continue using
5
+ `from seam.indexer.field_access_ext import <name>` without changes.
6
+
7
+ LAYER: leaf — imports only stdlib + tree_sitter + resolve_receiver_type helpers.
8
+ Never imports from graph.py, db.py, or any non-leaf seam module.
9
+
10
+ LAYERING:
11
+ graph_common (leaf — no seam deps)
12
+ graph_scope_infer_ext (leaf — Go/Rust receiver-type inference)
13
+ graph_scope_infer_ext2 (leaf — Java/C#/C++/Ruby/PHP receiver-type inference)
14
+
15
+ field_access_c_cpp (leaf — C and C++ field-access implementation)
16
+ field_access_ext (this file — Java/C# implementation + façade re-exports)
17
+
18
+ graph_java (calls extract_field_accesses_java + collect_field_symbols_java)
19
+ graph_csharp (calls extract_field_accesses_csharp + collect_field_symbols_csharp)
20
+ graph_c (calls extract_field_accesses_c + collect_field_symbols_c)
21
+ graph_cpp (calls extract_field_accesses_cpp + collect_field_symbols_cpp)
22
+
23
+ WHY split: the combined Java/C#/C/C++ implementation exceeded the 1000-line limit.
24
+ C and C++ are in field_access_c_cpp.py; all public names re-exported here unchanged.
25
+
26
+ Java:
27
+ field_access node: object + field → field_access edge
28
+ assignment_expression left=field_access → write; else read
29
+ update_expression (++/--) containing field_access → write
30
+ method_invocation → NOT a field edge (call position)
31
+
32
+ C#:
33
+ member_access_expression node: expression + name → field access edge
34
+ assignment_expression left=member_access_expression → write; else read
35
+ postfix_unary_expression / prefix_unary_expression (++/--) → write
36
+ invocation_expression function=member_access_expression → NOT a field edge
37
+
38
+ NEVER RAISES: all public functions have a backstop try/except and return [] on error.
39
+ """
40
+
41
+ import logging
42
+
43
+ from tree_sitter import Node
44
+
45
+ # Re-export C and C++ implementations so callers need no import changes.
46
+ # noqa: F401 — intentional re-exports for the façade pattern.
47
+ from seam.indexer.field_access_c_cpp import ( # noqa: F401
48
+ collect_field_symbols_c,
49
+ collect_field_symbols_cpp,
50
+ extract_field_accesses_c,
51
+ extract_field_accesses_cpp,
52
+ )
53
+ from seam.indexer.graph_common import _text
54
+ from seam.indexer.graph_scope_infer_ext import resolve_receiver_type_ext
55
+ from seam.indexer.graph_scope_infer_ext2 import (
56
+ _CS_SELF_NAMES,
57
+ _JAVA_SELF_NAMES,
58
+ )
59
+
60
+ logger = logging.getLogger(__name__)
61
+
62
+ # Return type for the public API: (source_fn, target_field, mode, line)
63
+ # mode is "reads" or "writes"
64
+ FieldAccess = tuple[str, str, str, int]
65
+
66
+
67
+ # ══════════════════════════════════════════════════════════════════════════════
68
+ # Java field-access classification (A3 Slice 4)
69
+ # ══════════════════════════════════════════════════════════════════════════════
70
+ #
71
+ # Java AST patterns for field accesses:
72
+ # field_access → <object>.<field> — the node for ALL member field access
73
+ # method_invocation object=... name=... → method call — NOT a field access
74
+ # assignment_expression left=field_access → WRITE
75
+ # assignment_expression (operator +=, -=, etc.) left=field_access → WRITE
76
+ # update_expression (++/--) operand=field_access → WRITE
77
+ #
78
+ # Receiver resolution:
79
+ # 'this' → enclosing class (EXTRACTED). Others via resolve_receiver_type_ext.
80
+
81
+
82
+ def extract_field_accesses_java(
83
+ func_body: Node,
84
+ source_fn: str,
85
+ class_name: str | None,
86
+ var_types: dict[str, str],
87
+ ) -> list[FieldAccess]:
88
+ """Extract field accesses (reads and writes) from a Java method/constructor body.
89
+
90
+ Args:
91
+ func_body: The 'body' block Node of the method/constructor.
92
+ source_fn: Qualified name of the enclosing function, e.g. 'Account.getBalance'.
93
+ class_name: Enclosing class name (None for static contexts).
94
+ var_types: Scope map: param/local name → type name.
95
+
96
+ Returns:
97
+ List of (source_fn, target_field, mode, line) tuples.
98
+ target_field = 'Type.field' when receiver resolves; bare 'field' when unknown.
99
+ mode = 'reads' | 'writes'.
100
+ line = 1-based source line of the field access.
101
+ Returns [] on any error.
102
+
103
+ Never raises.
104
+ """
105
+ result: list[FieldAccess] = []
106
+ try:
107
+ _java_walk_body(func_body, source_fn, class_name, var_types, result)
108
+ except Exception as exc: # noqa: BLE001
109
+ logger.debug(
110
+ "extract_field_accesses_java: failed for source=%r: %r", source_fn, exc
111
+ )
112
+ return result
113
+
114
+
115
+ def _java_walk_body(
116
+ node: Node,
117
+ source_fn: str,
118
+ class_name: str | None,
119
+ var_types: dict[str, str],
120
+ result: list[FieldAccess],
121
+ ) -> None:
122
+ """Recursively walk a Java method body collecting field accesses."""
123
+ for child in node.children:
124
+ _java_walk_stmt(child, source_fn, class_name, var_types, result)
125
+
126
+
127
+ def _java_walk_stmt(
128
+ node: Node,
129
+ source_fn: str,
130
+ class_name: str | None,
131
+ var_types: dict[str, str],
132
+ result: list[FieldAccess],
133
+ ) -> None:
134
+ """Walk a single Java statement node for field accesses.
135
+
136
+ Handles:
137
+ expression_statement → assignment_expression / update_expression / bare field_access
138
+ return_statement, if_statement, for/while loops → recurse
139
+ Skips nested class/method definitions.
140
+ """
141
+ t = node.type
142
+
143
+ # Skip nested scope-creating constructs.
144
+ if t in ("class_declaration", "method_declaration", "constructor_declaration",
145
+ "lambda_expression"):
146
+ return
147
+
148
+ # assignment_expression: left=field_access → write; right may contain reads.
149
+ if t == "assignment_expression":
150
+ _java_handle_assignment(node, source_fn, class_name, var_types, result)
151
+ return
152
+
153
+ # update_expression (++ / --): operand field_access → write.
154
+ if t == "update_expression":
155
+ _java_handle_update(node, source_fn, class_name, var_types, result)
156
+ return
157
+
158
+ # method_invocation: NOT a field edge (it's a call). Recurse into arguments.
159
+ if t == "method_invocation":
160
+ args = node.child_by_field_name("arguments")
161
+ if args is not None:
162
+ for arg in args.children:
163
+ _java_walk_stmt(arg, source_fn, class_name, var_types, result)
164
+ return
165
+
166
+ # A bare field_access that is not in call position → read.
167
+ if t == "field_access":
168
+ _java_emit_read_if_not_in_call(node, source_fn, class_name, var_types, result)
169
+ return
170
+
171
+ # Recurse into all other nodes.
172
+ for child in node.children:
173
+ _java_walk_stmt(child, source_fn, class_name, var_types, result)
174
+
175
+
176
+ def _java_handle_assignment(
177
+ node: Node,
178
+ source_fn: str,
179
+ class_name: str | None,
180
+ var_types: dict[str, str],
181
+ result: list[FieldAccess],
182
+ ) -> None:
183
+ """Handle Java assignment_expression: left field_access → write; right → reads."""
184
+ left = node.child_by_field_name("left")
185
+ if left is not None and left.type == "field_access":
186
+ acc = _java_classify_field_access(left, class_name, var_types)
187
+ if acc is not None:
188
+ target, _ = acc
189
+ result.append((source_fn, target, "writes", left.start_point[0] + 1))
190
+
191
+ right = node.child_by_field_name("right")
192
+ if right is not None:
193
+ _java_collect_reads_recursive(right, source_fn, class_name, var_types, result)
194
+
195
+
196
+ def _java_handle_update(
197
+ node: Node,
198
+ source_fn: str,
199
+ class_name: str | None,
200
+ var_types: dict[str, str],
201
+ result: list[FieldAccess],
202
+ ) -> None:
203
+ """Handle Java update_expression (++/--): operand field_access → write.
204
+
205
+ Java update_expression has 'expression' field OR the field_access is a direct child.
206
+ Both prefix (++this.f) and postfix (this.f++) forms produce update_expression.
207
+ """
208
+ # Try 'expression' field first (tree-sitter-java may use either form).
209
+ operand = node.child_by_field_name("expression")
210
+ if operand is None:
211
+ # Fallback: find first field_access child directly.
212
+ for child in node.children:
213
+ if child.type == "field_access":
214
+ operand = child
215
+ break
216
+ if operand is not None and operand.type == "field_access":
217
+ acc = _java_classify_field_access(operand, class_name, var_types)
218
+ if acc is not None:
219
+ target, _ = acc
220
+ result.append((source_fn, target, "writes", operand.start_point[0] + 1))
221
+
222
+
223
+ def _java_emit_read_if_not_in_call(
224
+ node: Node,
225
+ source_fn: str,
226
+ class_name: str | None,
227
+ var_types: dict[str, str],
228
+ result: list[FieldAccess],
229
+ ) -> None:
230
+ """Emit a reads edge for a Java field_access node that is NOT in call position.
231
+
232
+ In Java, a field_access becomes a method_invocation when it is the 'object' field
233
+ of the method_invocation AND the method is called on it. However, Java tree-sitter
234
+ does NOT emit a field_access for the callee — instead the 'object' field of
235
+ method_invocation is the receiver expression. So any bare field_access that we
236
+ encounter at the statement level is already NOT in call position.
237
+
238
+ WHY: Java method calls like this.foo() use method_invocation, not field_access +
239
+ call_expression. So there is no field_access-in-call-position case to worry about.
240
+ We still guard against being inside a method_invocation argument list.
241
+
242
+ WHY guard against being the 'object' of method_invocation: chained calls like
243
+ `this.obj.foo()` produce a field_access for `this.obj` as the 'object' of the
244
+ method_invocation. In that case `this.obj` IS genuinely being read (to get the
245
+ receiver), but we skip it conservatively — the call edge on `obj.foo` already
246
+ captures the dependency. Emitting both a 'reads' edge AND a 'call' edge for the
247
+ same chained expression would double-count the relationship.
248
+ """
249
+ parent = node.parent
250
+ # Guard: if parent is a method_invocation and this is the 'object' field,
251
+ # it means we are the receiver of a method call (e.g. this.obj.foo() where
252
+ # this.obj is a field_access). In that case, it's actually a field read
253
+ # (reading obj to call a method on it) — but we conservatively skip it
254
+ # to avoid confusion with call edges.
255
+ # NOTE: For simplicity, we only skip if parent is method_invocation AND
256
+ # this field_access is exactly the 'object' field (receiver, not an arg).
257
+ if parent is not None and parent.type == "method_invocation":
258
+ obj_field = parent.child_by_field_name("object")
259
+ if obj_field is not None and obj_field.start_point == node.start_point:
260
+ return # This is the method receiver — skip (the call edge handles it)
261
+
262
+ acc = _java_classify_field_access(node, class_name, 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 _java_collect_reads_recursive(
269
+ node: Node,
270
+ source_fn: str,
271
+ class_name: str | None,
272
+ var_types: dict[str, str],
273
+ result: list[FieldAccess],
274
+ ) -> None:
275
+ """Recursively collect reads from a Java expression node."""
276
+ t = node.type
277
+ if t in ("class_declaration", "method_declaration", "constructor_declaration",
278
+ "lambda_expression"):
279
+ return
280
+
281
+ if t == "field_access":
282
+ _java_emit_read_if_not_in_call(node, source_fn, class_name, var_types, result)
283
+ return
284
+
285
+ for child in node.children:
286
+ _java_collect_reads_recursive(child, source_fn, class_name, var_types, result)
287
+
288
+
289
+ def _java_classify_field_access(
290
+ node: Node,
291
+ class_name: str | None,
292
+ var_types: dict[str, str],
293
+ ) -> tuple[str, str | None] | None:
294
+ """Resolve a Java field_access node to (qualified_target, receiver_text).
295
+
296
+ Java field_access: object field contains receiver (e.g. 'this', identifier),
297
+ field field contains the identifier (field name).
298
+
299
+ Returns None when the node lacks the expected shape.
300
+ Returns (target, receiver_text) where:
301
+ - target = 'Type.field' when receiver resolves (EXTRACTED confidence)
302
+ - target = bare 'field' when unresolvable (AMBIGUOUS confidence)
303
+
304
+ Conservatism contract: NEVER emit a wrong qualified target.
305
+ Never raises (returns None on any exception).
306
+ """
307
+ try:
308
+ obj_node = node.child_by_field_name("object")
309
+ field_node = node.child_by_field_name("field")
310
+ if obj_node is None or field_node is None:
311
+ return None
312
+
313
+ field_name = _text(field_node)
314
+ if not field_name:
315
+ return None
316
+
317
+ receiver_text = _text(obj_node)
318
+
319
+ resolved_type = resolve_receiver_type_ext(
320
+ receiver_text, class_name, var_types, _JAVA_SELF_NAMES
321
+ )
322
+
323
+ if resolved_type is not None:
324
+ return f"{resolved_type}.{field_name}", receiver_text
325
+ else:
326
+ return field_name, receiver_text
327
+
328
+ except Exception as exc: # noqa: BLE001
329
+ logger.debug("_java_classify_field_access: failed: %r", exc)
330
+ return None
331
+
332
+
333
+ def collect_field_symbols_java(
334
+ class_node: Node,
335
+ class_name: str,
336
+ ) -> list[tuple[str, int]]:
337
+ """Collect (qualified_field_name, line) pairs from a Java class_declaration.
338
+
339
+ Returns field symbols for ALL field_declaration nodes in the class body.
340
+ Each field_declaration may declare multiple names (e.g. 'int x, y;').
341
+ Unlike the holds collector, we do NOT filter by user-type constraints —
342
+ ALL fields are indexed (including primitive types like int, String).
343
+
344
+ Returns [] on any error. Never raises.
345
+ """
346
+ try:
347
+ result: list[tuple[str, int]] = []
348
+ body = class_node.child_by_field_name("body")
349
+ if body is None:
350
+ return result
351
+
352
+ for child in body.named_children:
353
+ if child.type == "field_declaration":
354
+ try:
355
+ _java_collect_field_decl(child, class_name, result)
356
+ except Exception: # noqa: BLE001
357
+ pass
358
+
359
+ return result
360
+
361
+ except Exception as exc: # noqa: BLE001
362
+ logger.debug("collect_field_symbols_java: failed for class %r: %r", class_name, exc)
363
+ return []
364
+
365
+
366
+ def _java_collect_field_decl(
367
+ field_decl: Node,
368
+ class_name: str,
369
+ result: list[tuple[str, int]],
370
+ ) -> None:
371
+ """Extract field symbols from a single Java field_declaration node.
372
+
373
+ Java field_declaration:
374
+ modifiers? type variable_declarator+ ';'
375
+ Each variable_declarator has a 'name' field (identifier).
376
+
377
+ Multi-name declarations (int x, y;) produce one symbol per name.
378
+ """
379
+ for child in field_decl.named_children:
380
+ if child.type == "variable_declarator":
381
+ name_node = child.child_by_field_name("name")
382
+ if name_node is not None:
383
+ field_name = _text(name_node).strip()
384
+ if field_name:
385
+ qualified = f"{class_name}.{field_name}"
386
+ result.append((qualified, field_decl.start_point[0] + 1))
387
+
388
+
389
+ # ══════════════════════════════════════════════════════════════════════════════
390
+ # C# field-access classification (A3 Slice 4)
391
+ # ══════════════════════════════════════════════════════════════════════════════
392
+ #
393
+ # C# AST patterns for field accesses:
394
+ # member_access_expression → <expression>.<name> — the node for ALL member access
395
+ # invocation_expression function=member_access_expression → method call — NOT field
396
+ # assignment_expression left=member_access_expression → WRITE (any operator)
397
+ # postfix_unary_expression / prefix_unary_expression (++/--) → WRITE
398
+ #
399
+ # Receiver resolution:
400
+ # 'this' → enclosing class (EXTRACTED). Others via resolve_receiver_type_ext.
401
+
402
+
403
+ def extract_field_accesses_csharp(
404
+ func_body: Node,
405
+ source_fn: str,
406
+ class_name: str | None,
407
+ var_types: dict[str, str],
408
+ ) -> list[FieldAccess]:
409
+ """Extract field accesses (reads and writes) from a C# method/constructor body.
410
+
411
+ Args:
412
+ func_body: The 'body' block Node of the method/constructor.
413
+ source_fn: Qualified name of the enclosing function, e.g. 'Account.GetBalance'.
414
+ class_name: Enclosing class name (None for static contexts).
415
+ var_types: Scope map: param/local name → type name.
416
+
417
+ Returns:
418
+ List of (source_fn, target_field, mode, line) tuples.
419
+ Returns [] on any error.
420
+
421
+ Never raises.
422
+ """
423
+ result: list[FieldAccess] = []
424
+ try:
425
+ _cs_walk_body(func_body, source_fn, class_name, var_types, result)
426
+ except Exception as exc: # noqa: BLE001
427
+ logger.debug(
428
+ "extract_field_accesses_csharp: failed for source=%r: %r", source_fn, exc
429
+ )
430
+ return result
431
+
432
+
433
+ def _cs_walk_body(
434
+ node: Node,
435
+ source_fn: str,
436
+ class_name: str | None,
437
+ var_types: dict[str, str],
438
+ result: list[FieldAccess],
439
+ ) -> None:
440
+ """Recursively walk a C# method body collecting field accesses."""
441
+ for child in node.children:
442
+ _cs_walk_stmt(child, source_fn, class_name, var_types, result)
443
+
444
+
445
+ def _cs_walk_stmt(
446
+ node: Node,
447
+ source_fn: str,
448
+ class_name: str | None,
449
+ var_types: dict[str, str],
450
+ result: list[FieldAccess],
451
+ ) -> None:
452
+ """Walk a single C# statement node for field accesses."""
453
+ t = node.type
454
+
455
+ # Skip nested scope-creating constructs.
456
+ if t in ("class_declaration", "method_declaration", "constructor_declaration",
457
+ "lambda_expression", "anonymous_method_expression"):
458
+ return
459
+
460
+ # assignment_expression: left=member_access_expression → write.
461
+ if t == "assignment_expression":
462
+ _cs_handle_assignment(node, source_fn, class_name, var_types, result)
463
+ return
464
+
465
+ # postfix_unary_expression / prefix_unary_expression (++ / --): → write.
466
+ if t in ("postfix_unary_expression", "prefix_unary_expression"):
467
+ _cs_handle_unary_update(node, source_fn, class_name, var_types, result)
468
+ return
469
+
470
+ # invocation_expression: NOT a field edge. Recurse into arguments.
471
+ if t == "invocation_expression":
472
+ args = node.child_by_field_name("argument_list")
473
+ if args is not None:
474
+ for arg in args.children:
475
+ _cs_walk_stmt(arg, source_fn, class_name, var_types, result)
476
+ return
477
+
478
+ # A bare member_access_expression (not in call position) → read.
479
+ if t == "member_access_expression":
480
+ _cs_emit_read_if_not_in_call(node, source_fn, class_name, var_types, result)
481
+ return
482
+
483
+ # Recurse into all other nodes.
484
+ for child in node.children:
485
+ _cs_walk_stmt(child, source_fn, class_name, var_types, result)
486
+
487
+
488
+ def _cs_handle_assignment(
489
+ node: Node,
490
+ source_fn: str,
491
+ class_name: str | None,
492
+ var_types: dict[str, str],
493
+ result: list[FieldAccess],
494
+ ) -> None:
495
+ """Handle C# assignment_expression: left member_access_expression → write."""
496
+ left = node.child_by_field_name("left")
497
+ if left is not None and left.type == "member_access_expression":
498
+ acc = _cs_classify_member_access(left, class_name, var_types)
499
+ if acc is not None:
500
+ target, _ = acc
501
+ result.append((source_fn, target, "writes", left.start_point[0] + 1))
502
+
503
+ right = node.child_by_field_name("right")
504
+ if right is not None:
505
+ _cs_collect_reads_recursive(right, source_fn, class_name, var_types, result)
506
+
507
+
508
+ def _cs_handle_unary_update(
509
+ node: Node,
510
+ source_fn: str,
511
+ class_name: str | None,
512
+ var_types: dict[str, str],
513
+ result: list[FieldAccess],
514
+ ) -> None:
515
+ """Handle C# postfix/prefix ++ / -- on a member_access_expression → write.
516
+
517
+ postfix_unary_expression: the member_access_expression is the first named child.
518
+ prefix_unary_expression: the member_access_expression follows the operator.
519
+ """
520
+ for child in node.children:
521
+ if child.type == "member_access_expression":
522
+ acc = _cs_classify_member_access(child, class_name, var_types)
523
+ if acc is not None:
524
+ target, _ = acc
525
+ result.append((source_fn, target, "writes", child.start_point[0] + 1))
526
+ break # Only one operand in unary ++/--
527
+
528
+
529
+ def _cs_emit_read_if_not_in_call(
530
+ node: Node,
531
+ source_fn: str,
532
+ class_name: str | None,
533
+ var_types: dict[str, str],
534
+ result: list[FieldAccess],
535
+ ) -> None:
536
+ """Emit a reads edge for a C# member_access_expression NOT in call position.
537
+
538
+ A member_access_expression is in call position when its parent is an
539
+ invocation_expression AND it is the 'function' field of that call.
540
+ """
541
+ parent = node.parent
542
+ if parent is not None and parent.type == "invocation_expression":
543
+ func_field = parent.child_by_field_name("function")
544
+ if func_field is not None and func_field.start_point == node.start_point:
545
+ return # Call position — do not emit field read
546
+
547
+ acc = _cs_classify_member_access(node, class_name, var_types)
548
+ if acc is not None:
549
+ target, _ = acc
550
+ result.append((source_fn, target, "reads", node.start_point[0] + 1))
551
+
552
+
553
+ def _cs_collect_reads_recursive(
554
+ node: Node,
555
+ source_fn: str,
556
+ class_name: str | None,
557
+ var_types: dict[str, str],
558
+ result: list[FieldAccess],
559
+ ) -> None:
560
+ """Recursively collect reads from a C# expression node."""
561
+ t = node.type
562
+ if t in ("class_declaration", "method_declaration", "constructor_declaration",
563
+ "lambda_expression", "anonymous_method_expression"):
564
+ return
565
+
566
+ if t == "member_access_expression":
567
+ _cs_emit_read_if_not_in_call(node, source_fn, class_name, var_types, result)
568
+ return
569
+
570
+ for child in node.children:
571
+ _cs_collect_reads_recursive(child, source_fn, class_name, var_types, result)
572
+
573
+
574
+ def _cs_classify_member_access(
575
+ node: Node,
576
+ class_name: str | None,
577
+ var_types: dict[str, str],
578
+ ) -> tuple[str, str | None] | None:
579
+ """Resolve a C# member_access_expression node to (qualified_target, receiver_text).
580
+
581
+ C# member_access_expression:
582
+ expression: the receiver expression (e.g. 'this', 'obj')
583
+ name: identifier (the field/property name)
584
+
585
+ Conservatism contract: NEVER emit a wrong qualified target.
586
+ Never raises (returns None on any exception).
587
+ """
588
+ try:
589
+ expr_node = node.child_by_field_name("expression")
590
+ name_node = node.child_by_field_name("name")
591
+ if expr_node is None or name_node is None:
592
+ return None
593
+
594
+ # name should be an identifier node.
595
+ if name_node.type != "identifier":
596
+ return None
597
+
598
+ field_name = _text(name_node)
599
+ if not field_name:
600
+ return None
601
+
602
+ receiver_text = _text(expr_node)
603
+
604
+ resolved_type = resolve_receiver_type_ext(
605
+ receiver_text, class_name, var_types, _CS_SELF_NAMES
606
+ )
607
+
608
+ if resolved_type is not None:
609
+ return f"{resolved_type}.{field_name}", receiver_text
610
+ else:
611
+ return field_name, receiver_text
612
+
613
+ except Exception as exc: # noqa: BLE001
614
+ logger.debug("_cs_classify_member_access: failed: %r", exc)
615
+ return None
616
+
617
+
618
+ def collect_field_symbols_csharp(
619
+ class_node: Node,
620
+ class_name: str,
621
+ ) -> list[tuple[str, int]]:
622
+ """Collect (qualified_field_name, line) pairs from a C# class/struct/record declaration.
623
+
624
+ Returns field symbols for:
625
+ 1. field_declaration nodes in the class body.
626
+ 2. property_declaration nodes (auto-properties) in the class body.
627
+
628
+ C# field_declaration structure:
629
+ modifier* variable_declaration
630
+ variable_declaration → identifier(type) + variable_declarator*(name=identifier)
631
+
632
+ C# property_declaration:
633
+ modifier* type name=identifier { get; set; }
634
+
635
+ Returns [] on any error. Never raises.
636
+ """
637
+ try:
638
+ result: list[tuple[str, int]] = []
639
+ body = class_node.child_by_field_name("body")
640
+ if body is None:
641
+ return result
642
+
643
+ for child in body.named_children:
644
+ try:
645
+ if child.type == "field_declaration":
646
+ _cs_collect_field_decl(child, class_name, result)
647
+ elif child.type == "property_declaration":
648
+ _cs_collect_property_decl(child, class_name, result)
649
+ except Exception: # noqa: BLE001
650
+ pass
651
+
652
+ return result
653
+
654
+ except Exception as exc: # noqa: BLE001
655
+ logger.debug("collect_field_symbols_csharp: failed for class %r: %r", class_name, exc)
656
+ return []
657
+
658
+
659
+ def _cs_collect_field_decl(
660
+ field_decl: Node,
661
+ class_name: str,
662
+ result: list[tuple[str, int]],
663
+ ) -> None:
664
+ """Extract field symbols from a single C# field_declaration node.
665
+
666
+ C# field_declaration has a variable_declaration child:
667
+ variable_declaration → type identifier + variable_declarator*(name=identifier)
668
+ """
669
+ var_decl = None
670
+ for child in field_decl.children:
671
+ if child.type == "variable_declaration":
672
+ var_decl = child
673
+ break
674
+ if var_decl is None:
675
+ return
676
+
677
+ # Each variable_declarator in the variable_declaration is a field name.
678
+ for child in var_decl.named_children:
679
+ if child.type == "variable_declarator":
680
+ name_node = child.child_by_field_name("name")
681
+ if name_node is not None:
682
+ field_name = _text(name_node).strip()
683
+ if field_name:
684
+ qualified = f"{class_name}.{field_name}"
685
+ result.append((qualified, field_decl.start_point[0] + 1))
686
+
687
+
688
+ def _cs_collect_property_decl(
689
+ prop_decl: Node,
690
+ class_name: str,
691
+ result: list[tuple[str, int]],
692
+ ) -> None:
693
+ """Extract a field symbol from a C# property_declaration node.
694
+
695
+ C# property_declaration:
696
+ modifier* type name=identifier { ... }
697
+ The 'name' field is the property name (identifier).
698
+ We index auto-properties as field symbols (they are effectively stored state).
699
+ """
700
+ name_node = prop_decl.child_by_field_name("name")
701
+ if name_node is None:
702
+ return
703
+ field_name = _text(name_node).strip()
704
+ if not field_name:
705
+ return
706
+ qualified = f"{class_name}.{field_name}"
707
+ result.append((qualified, prop_decl.start_point[0] + 1))
708
+