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,643 @@
1
+ """Field-access edge helper — C and C++ read/write classification.
2
+
3
+ LAYER: leaf — imports only stdlib + tree_sitter + the existing resolve_receiver_type
4
+ helpers from graph_scope_infer_ext2. 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_ext2 (leaf — Java/C#/C++/Ruby/PHP receiver-type inference)
10
+
11
+ field_access_c_cpp (this file — field-access classification for C and C++)
12
+
13
+ field_access_ext (re-exports extract_field_accesses_c/cpp +
14
+ collect_field_symbols_c/cpp for backward compat)
15
+ graph_c (calls those functions via field_access_ext import)
16
+ graph_cpp (calls those functions via field_access_ext import)
17
+
18
+ WHY a separate module from field_access_ext.py:
19
+ field_access_ext.py (Java + C#) would otherwise exceed the 1000-line limit with
20
+ C and C++ added. This follows the graph_c_cpp.py split precedent.
21
+
22
+ C AST patterns for field accesses:
23
+ field_expression → <argument>.<field_identifier> or <argument>-><field_identifier>
24
+ Both dot (.) and arrow (->) accesses use field_expression.
25
+ call_expression function=field_expression → function pointer call — NOT field access
26
+ assignment_expression left=field_expression → WRITE
27
+ update_expression argument=field_expression → WRITE (++ / --)
28
+
29
+ C++ AST patterns for field accesses:
30
+ field_expression → same node type as C for BOTH dot and arrow access
31
+ this->field: argument='this', operator='->'
32
+ call_expression function=field_expression → method/function-pointer call — NOT field
33
+ assignment_expression left=field_expression → WRITE
34
+ update_expression argument=field_expression → WRITE (++ / --)
35
+
36
+ NEVER RAISES: all public functions have a backstop try/except and return [] on error.
37
+ """
38
+
39
+ import logging
40
+
41
+ from tree_sitter import Node
42
+
43
+ from seam.indexer.graph_common import _text
44
+ from seam.indexer.graph_scope_infer_ext import resolve_receiver_type_ext
45
+ from seam.indexer.graph_scope_infer_ext2 import _CPP_SELF_NAMES
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+ # Return type for the public API: (source_fn, target_field, mode, line)
50
+ # mode is "reads" or "writes"
51
+ FieldAccess = tuple[str, str, str, int]
52
+
53
+
54
+ # ══════════════════════════════════════════════════════════════════════════════
55
+ # C field-access classification (A3 Slice 4)
56
+ # ══════════════════════════════════════════════════════════════════════════════
57
+ #
58
+ # C AST patterns for field accesses:
59
+ # field_expression → <argument>.<field_identifier> or <argument>-><field_identifier>
60
+ # Both dot (.) and arrow (->) accesses use field_expression.
61
+ # call_expression function=field_expression → function pointer call — NOT field access
62
+ # assignment_expression left=field_expression → WRITE
63
+ # update_expression argument=field_expression → WRITE (++ / --)
64
+ #
65
+ # Receiver resolution for C:
66
+ # C has no class concept in free functions. We look up var_types for typed struct
67
+ # pointers. If var_types has the receiver → qualified StructName.field.
68
+ # Otherwise → bare field name (AMBIGUOUS confidence).
69
+ # There are no self-names in C (frozenset()).
70
+
71
+
72
+ def extract_field_accesses_c(
73
+ func_body: Node,
74
+ source_fn: str,
75
+ struct_name: str | None,
76
+ var_types: dict[str, str],
77
+ ) -> list[FieldAccess]:
78
+ """Extract field accesses (reads and writes) from a C function body.
79
+
80
+ Args:
81
+ func_body: The 'compound_statement' (body) Node of the function.
82
+ source_fn: Name of the enclosing function, e.g. 'get_balance'.
83
+ struct_name: None (C functions are not inside a struct; kept for API parity).
84
+ var_types: Scope map: param/local name → struct type name (for pointer params).
85
+
86
+ Returns:
87
+ List of (source_fn, target_field, mode, line) tuples.
88
+ Returns [] on any error.
89
+
90
+ Never raises.
91
+ """
92
+ result: list[FieldAccess] = []
93
+ try:
94
+ _c_walk_body(func_body, source_fn, struct_name, var_types, result)
95
+ except Exception as exc: # noqa: BLE001
96
+ logger.debug(
97
+ "extract_field_accesses_c: failed for source=%r: %r", source_fn, exc
98
+ )
99
+ return result
100
+
101
+
102
+ def _c_walk_body(
103
+ node: Node,
104
+ source_fn: str,
105
+ struct_name: str | None,
106
+ var_types: dict[str, str],
107
+ result: list[FieldAccess],
108
+ ) -> None:
109
+ """Recursively walk a C function body collecting field accesses."""
110
+ for child in node.children:
111
+ _c_walk_stmt(child, source_fn, struct_name, var_types, result)
112
+
113
+
114
+ def _c_walk_stmt(
115
+ node: Node,
116
+ source_fn: str,
117
+ struct_name: str | None,
118
+ var_types: dict[str, str],
119
+ result: list[FieldAccess],
120
+ ) -> None:
121
+ """Walk a single C statement node for field accesses."""
122
+ t = node.type
123
+
124
+ # assignment_expression: left=field_expression → write.
125
+ if t == "assignment_expression":
126
+ _c_handle_assignment(node, source_fn, struct_name, var_types, result)
127
+ return
128
+
129
+ # update_expression (++/--): argument=field_expression → write.
130
+ if t == "update_expression":
131
+ _c_handle_update(node, source_fn, struct_name, var_types, result)
132
+ return
133
+
134
+ # call_expression: function child may be field_expression (function pointer call).
135
+ # Do NOT emit a field edge for the function child. Recurse into arguments.
136
+ if t == "call_expression":
137
+ args = node.child_by_field_name("arguments")
138
+ if args is not None:
139
+ for arg in args.children:
140
+ _c_walk_stmt(arg, source_fn, struct_name, var_types, result)
141
+ return
142
+
143
+ # A bare field_expression (not in call position) → read.
144
+ if t == "field_expression":
145
+ _c_emit_read_if_not_in_call(node, source_fn, struct_name, var_types, result)
146
+ return
147
+
148
+ # Recurse into all other nodes.
149
+ for child in node.children:
150
+ _c_walk_stmt(child, source_fn, struct_name, var_types, result)
151
+
152
+
153
+ def _c_handle_assignment(
154
+ node: Node,
155
+ source_fn: str,
156
+ struct_name: str | None,
157
+ var_types: dict[str, str],
158
+ result: list[FieldAccess],
159
+ ) -> None:
160
+ """Handle C assignment_expression: left field_expression → write; right → reads."""
161
+ left = node.child_by_field_name("left")
162
+ if left is not None and left.type == "field_expression":
163
+ acc = _c_classify_field_expr(left, struct_name, var_types)
164
+ if acc is not None:
165
+ target, _ = acc
166
+ result.append((source_fn, target, "writes", left.start_point[0] + 1))
167
+
168
+ right = node.child_by_field_name("right")
169
+ if right is not None:
170
+ _c_collect_reads_recursive(right, source_fn, struct_name, var_types, result)
171
+
172
+
173
+ def _c_handle_update(
174
+ node: Node,
175
+ source_fn: str,
176
+ struct_name: str | None,
177
+ var_types: dict[str, str],
178
+ result: list[FieldAccess],
179
+ ) -> None:
180
+ """Handle C update_expression (++/--): argument field_expression → write."""
181
+ operand = node.child_by_field_name("argument")
182
+ if operand is None:
183
+ for child in node.children:
184
+ if child.type == "field_expression":
185
+ operand = child
186
+ break
187
+ if operand is not None and operand.type == "field_expression":
188
+ acc = _c_classify_field_expr(operand, struct_name, var_types)
189
+ if acc is not None:
190
+ target, _ = acc
191
+ result.append((source_fn, target, "writes", operand.start_point[0] + 1))
192
+
193
+
194
+ def _c_emit_read_if_not_in_call(
195
+ node: Node,
196
+ source_fn: str,
197
+ struct_name: str | None,
198
+ var_types: dict[str, str],
199
+ result: list[FieldAccess],
200
+ ) -> None:
201
+ """Emit a reads edge for a C field_expression NOT in call position.
202
+
203
+ A field_expression is in call position when its parent is call_expression AND
204
+ it IS the 'function' field of that call.
205
+ """
206
+ parent = node.parent
207
+ if parent is not None and parent.type == "call_expression":
208
+ func_field = parent.child_by_field_name("function")
209
+ if func_field is not None and func_field.start_point == node.start_point:
210
+ return # Call position — do not emit field read
211
+
212
+ acc = _c_classify_field_expr(node, struct_name, var_types)
213
+ if acc is not None:
214
+ target, _ = acc
215
+ result.append((source_fn, target, "reads", node.start_point[0] + 1))
216
+
217
+
218
+ def _c_collect_reads_recursive(
219
+ node: Node,
220
+ source_fn: str,
221
+ struct_name: str | None,
222
+ var_types: dict[str, str],
223
+ result: list[FieldAccess],
224
+ ) -> None:
225
+ """Recursively collect reads from a C expression node."""
226
+ t = node.type
227
+ if t == "field_expression":
228
+ _c_emit_read_if_not_in_call(node, source_fn, struct_name, var_types, result)
229
+ return
230
+
231
+ for child in node.children:
232
+ _c_collect_reads_recursive(child, source_fn, struct_name, var_types, result)
233
+
234
+
235
+ def _c_classify_field_expr(
236
+ node: Node,
237
+ struct_name: str | None,
238
+ var_types: dict[str, str],
239
+ ) -> tuple[str, str | None] | None:
240
+ """Resolve a C field_expression node to (qualified_target, receiver_text).
241
+
242
+ C field_expression:
243
+ argument: the receiver expression (e.g. 'p', 's')
244
+ field: field_identifier — the accessed field name
245
+ operator: '.' (dot) or '->' (arrow)
246
+
247
+ Returns None when the node lacks the expected shape.
248
+ Returns (target, receiver_text) where:
249
+ - target = 'StructName.field' when receiver type is known in var_types
250
+ - target = bare 'field' when unresolvable (AMBIGUOUS confidence)
251
+
252
+ Conservatism contract: NEVER emit a wrong qualified target.
253
+ Never raises (returns None on any exception).
254
+ """
255
+ try:
256
+ arg_node = node.child_by_field_name("argument")
257
+ field_node = node.child_by_field_name("field")
258
+ if arg_node is None or field_node is None:
259
+ return None
260
+
261
+ # Only field_identifier fields are real struct field accesses.
262
+ if field_node.type != "field_identifier":
263
+ return None
264
+
265
+ field_name = _text(field_node)
266
+ if not field_name:
267
+ return None
268
+
269
+ receiver_text = _text(arg_node)
270
+ # Strip leading '*' for pointer dereference (e.g. (*p).field or *p → p).
271
+ # WHY strip: C struct access via pointer uses `->` (tree-sitter handles both
272
+ # dot and arrow as field_expression), but explicit dereference `(*p).x` also
273
+ # occurs. The receiver text would be `*p`; after stripping, `p` can be looked
274
+ # up in var_types to resolve the struct type.
275
+ clean_recv = receiver_text.lstrip("*").strip()
276
+
277
+ # C has no self convention — pass empty frozenset().
278
+ # WHY: Unlike C++/Java/Python, C free functions have no implicit receiver.
279
+ # All struct type resolution must come from explicit var_types bindings
280
+ # (e.g. `Account *a` → var_types['a'] = 'Account'). If the receiver is not
281
+ # in var_types, we emit a bare field name rather than guessing.
282
+ resolved_type = resolve_receiver_type_ext(
283
+ clean_recv, struct_name, var_types, frozenset()
284
+ )
285
+
286
+ if resolved_type is not None:
287
+ return f"{resolved_type}.{field_name}", receiver_text
288
+ else:
289
+ return field_name, receiver_text
290
+
291
+ except Exception as exc: # noqa: BLE001
292
+ logger.debug("_c_classify_field_expr: failed: %r", exc)
293
+ return None
294
+
295
+
296
+ def collect_field_symbols_c(
297
+ struct_node: Node,
298
+ struct_name: str,
299
+ ) -> list[tuple[str, int]]:
300
+ """Collect (qualified_field_name, line) pairs from a C struct_specifier node.
301
+
302
+ Returns field symbols for ALL field_declaration nodes in the struct body.
303
+ Each field_declaration contains one or more field_identifier children (field names).
304
+
305
+ Unlike the holds collector (collect_composition_types_*), we do NOT filter by type —
306
+ ALL fields are indexed regardless of their type.
307
+
308
+ Returns [] on any error. Never raises.
309
+ """
310
+ try:
311
+ result: list[tuple[str, int]] = []
312
+
313
+ # struct_specifier contains a field_declaration_list child.
314
+ for child in struct_node.children:
315
+ if child.type != "field_declaration_list":
316
+ continue
317
+ for field_decl in child.named_children:
318
+ if field_decl.type != "field_declaration":
319
+ continue
320
+ try:
321
+ _c_collect_field_decl(field_decl, struct_name, result)
322
+ except Exception: # noqa: BLE001
323
+ pass
324
+ break # Only one field_declaration_list per struct
325
+
326
+ return result
327
+
328
+ except Exception as exc: # noqa: BLE001
329
+ logger.debug("collect_field_symbols_c: failed for struct %r: %r", struct_name, exc)
330
+ return []
331
+
332
+
333
+ def _c_collect_field_decl(
334
+ field_decl: Node,
335
+ struct_name: str,
336
+ result: list[tuple[str, int]],
337
+ ) -> None:
338
+ """Extract field symbols from a single C field_declaration node.
339
+
340
+ C field_declaration:
341
+ type_specifier field_identifier (',' field_identifier)* ';'
342
+ (Each field_identifier is a field name)
343
+ """
344
+ for child in field_decl.children:
345
+ if child.type == "field_identifier":
346
+ field_name = _text(child).strip()
347
+ if field_name:
348
+ qualified = f"{struct_name}.{field_name}"
349
+ result.append((qualified, field_decl.start_point[0] + 1))
350
+
351
+
352
+ # ══════════════════════════════════════════════════════════════════════════════
353
+ # C++ field-access classification (A3 Slice 4)
354
+ # ══════════════════════════════════════════════════════════════════════════════
355
+ #
356
+ # C++ AST patterns for field accesses:
357
+ # field_expression → same node type as C for BOTH dot and arrow access
358
+ # this->field: argument='this', operator='->'
359
+ # call_expression function=field_expression → method/function-pointer call — NOT field
360
+ # assignment_expression left=field_expression → WRITE
361
+ # update_expression argument=field_expression → WRITE (++ / --)
362
+ #
363
+ # Receiver resolution for C++:
364
+ # 'this' → enclosing class via _CPP_SELF_NAMES (EXTRACTED confidence).
365
+ # Others via resolve_receiver_type_ext using var_types.
366
+
367
+
368
+ def extract_field_accesses_cpp(
369
+ func_body: Node,
370
+ source_fn: str,
371
+ class_name: str | None,
372
+ var_types: dict[str, str],
373
+ ) -> list[FieldAccess]:
374
+ """Extract field accesses (reads and writes) from a C++ method/function body.
375
+
376
+ Args:
377
+ func_body: The 'compound_statement' (body) Node of the function.
378
+ source_fn: Qualified name of the enclosing function, e.g. 'Account.getBalance'.
379
+ class_name: Enclosing class/struct name (None for free functions).
380
+ var_types: Scope map: param/local name → type name.
381
+
382
+ Returns:
383
+ List of (source_fn, target_field, mode, line) tuples.
384
+ Returns [] on any error.
385
+
386
+ Never raises.
387
+ """
388
+ result: list[FieldAccess] = []
389
+ try:
390
+ _cpp_walk_body(func_body, source_fn, class_name, var_types, result)
391
+ except Exception as exc: # noqa: BLE001
392
+ logger.debug(
393
+ "extract_field_accesses_cpp: failed for source=%r: %r", source_fn, exc
394
+ )
395
+ return result
396
+
397
+
398
+ def _cpp_walk_body(
399
+ node: Node,
400
+ source_fn: str,
401
+ class_name: str | None,
402
+ var_types: dict[str, str],
403
+ result: list[FieldAccess],
404
+ ) -> None:
405
+ """Recursively walk a C++ function body collecting field accesses."""
406
+ for child in node.children:
407
+ _cpp_walk_stmt(child, source_fn, class_name, var_types, result)
408
+
409
+
410
+ def _cpp_walk_stmt(
411
+ node: Node,
412
+ source_fn: str,
413
+ class_name: str | None,
414
+ var_types: dict[str, str],
415
+ result: list[FieldAccess],
416
+ ) -> None:
417
+ """Walk a single C++ statement node for field accesses."""
418
+ t = node.type
419
+
420
+ # Skip nested scope-creating constructs.
421
+ if t in ("class_specifier", "struct_specifier", "function_definition",
422
+ "lambda_expression"):
423
+ return
424
+
425
+ # assignment_expression: left=field_expression → write.
426
+ if t == "assignment_expression":
427
+ _cpp_handle_assignment(node, source_fn, class_name, var_types, result)
428
+ return
429
+
430
+ # update_expression (++/--): → write.
431
+ if t == "update_expression":
432
+ _cpp_handle_update(node, source_fn, class_name, var_types, result)
433
+ return
434
+
435
+ # call_expression: function child may be field_expression (method/function-ptr call).
436
+ # Do NOT emit a field edge for the function child. Recurse into arguments.
437
+ if t == "call_expression":
438
+ args = node.child_by_field_name("arguments")
439
+ if args is not None:
440
+ for arg in args.children:
441
+ _cpp_walk_stmt(arg, source_fn, class_name, var_types, result)
442
+ return
443
+
444
+ # A bare field_expression (not in call position) → read.
445
+ if t == "field_expression":
446
+ _cpp_emit_read_if_not_in_call(node, source_fn, class_name, var_types, result)
447
+ return
448
+
449
+ # Recurse into all other nodes.
450
+ for child in node.children:
451
+ _cpp_walk_stmt(child, source_fn, class_name, var_types, result)
452
+
453
+
454
+ def _cpp_handle_assignment(
455
+ node: Node,
456
+ source_fn: str,
457
+ class_name: str | None,
458
+ var_types: dict[str, str],
459
+ result: list[FieldAccess],
460
+ ) -> None:
461
+ """Handle C++ assignment_expression: left field_expression → write; right → reads."""
462
+ left = node.child_by_field_name("left")
463
+ if left is not None and left.type == "field_expression":
464
+ acc = _cpp_classify_field_expr(left, class_name, var_types)
465
+ if acc is not None:
466
+ target, _ = acc
467
+ result.append((source_fn, target, "writes", left.start_point[0] + 1))
468
+
469
+ right = node.child_by_field_name("right")
470
+ if right is not None:
471
+ _cpp_collect_reads_recursive(right, source_fn, class_name, var_types, result)
472
+
473
+
474
+ def _cpp_handle_update(
475
+ node: Node,
476
+ source_fn: str,
477
+ class_name: str | None,
478
+ var_types: dict[str, str],
479
+ result: list[FieldAccess],
480
+ ) -> None:
481
+ """Handle C++ update_expression (++/--): argument field_expression → write."""
482
+ operand = node.child_by_field_name("argument")
483
+ if operand is None:
484
+ for child in node.children:
485
+ if child.type == "field_expression":
486
+ operand = child
487
+ break
488
+ if operand is not None and operand.type == "field_expression":
489
+ acc = _cpp_classify_field_expr(operand, class_name, var_types)
490
+ if acc is not None:
491
+ target, _ = acc
492
+ result.append((source_fn, target, "writes", operand.start_point[0] + 1))
493
+
494
+
495
+ def _cpp_emit_read_if_not_in_call(
496
+ node: Node,
497
+ source_fn: str,
498
+ class_name: str | None,
499
+ var_types: dict[str, str],
500
+ result: list[FieldAccess],
501
+ ) -> None:
502
+ """Emit a reads edge for a C++ field_expression NOT in call position."""
503
+ parent = node.parent
504
+ if parent is not None and parent.type == "call_expression":
505
+ func_field = parent.child_by_field_name("function")
506
+ if func_field is not None and func_field.start_point == node.start_point:
507
+ return # Call position — do not emit field read
508
+
509
+ acc = _cpp_classify_field_expr(node, class_name, var_types)
510
+ if acc is not None:
511
+ target, _ = acc
512
+ result.append((source_fn, target, "reads", node.start_point[0] + 1))
513
+
514
+
515
+ def _cpp_collect_reads_recursive(
516
+ node: Node,
517
+ source_fn: str,
518
+ class_name: str | None,
519
+ var_types: dict[str, str],
520
+ result: list[FieldAccess],
521
+ ) -> None:
522
+ """Recursively collect reads from a C++ expression node."""
523
+ t = node.type
524
+ if t in ("class_specifier", "struct_specifier", "function_definition",
525
+ "lambda_expression"):
526
+ return
527
+
528
+ if t == "field_expression":
529
+ _cpp_emit_read_if_not_in_call(node, source_fn, class_name, var_types, result)
530
+ return
531
+
532
+ for child in node.children:
533
+ _cpp_collect_reads_recursive(child, source_fn, class_name, var_types, result)
534
+
535
+
536
+ def _cpp_classify_field_expr(
537
+ node: Node,
538
+ class_name: str | None,
539
+ var_types: dict[str, str],
540
+ ) -> tuple[str, str | None] | None:
541
+ """Resolve a C++ field_expression node to (qualified_target, receiver_text).
542
+
543
+ C++ field_expression (same structure as C):
544
+ argument: the receiver expression (e.g. 'this', 'p', 's')
545
+ field: field_identifier — the accessed field name
546
+ operator: '.' or '->'
547
+
548
+ 'this' resolves to the enclosing class via _CPP_SELF_NAMES.
549
+ Other receivers resolved via var_types (resolve_receiver_type_ext).
550
+
551
+ Conservatism contract: NEVER emit a wrong qualified target.
552
+ Never raises (returns None on any exception).
553
+ """
554
+ try:
555
+ arg_node = node.child_by_field_name("argument")
556
+ field_node = node.child_by_field_name("field")
557
+ if arg_node is None or field_node is None:
558
+ return None
559
+
560
+ if field_node.type != "field_identifier":
561
+ return None
562
+
563
+ field_name = _text(field_node)
564
+ if not field_name:
565
+ return None
566
+
567
+ receiver_text = _text(arg_node)
568
+ clean_recv = receiver_text.lstrip("*").strip()
569
+
570
+ resolved_type = resolve_receiver_type_ext(
571
+ clean_recv, class_name, var_types, _CPP_SELF_NAMES
572
+ )
573
+
574
+ if resolved_type is not None:
575
+ return f"{resolved_type}.{field_name}", receiver_text
576
+ else:
577
+ return field_name, receiver_text
578
+
579
+ except Exception as exc: # noqa: BLE001
580
+ logger.debug("_cpp_classify_field_expr: failed: %r", exc)
581
+ return None
582
+
583
+
584
+ def collect_field_symbols_cpp(
585
+ class_node: Node,
586
+ class_name: str,
587
+ ) -> list[tuple[str, int]]:
588
+ """Collect (qualified_field_name, line) pairs from a C++ class_specifier or struct_specifier.
589
+
590
+ Returns field symbols for ALL field_declaration nodes in the class/struct body.
591
+ C++ field_declaration_list body → field_declaration nodes.
592
+
593
+ Unlike the holds collector, we do NOT filter by user-type constraints —
594
+ ALL fields are indexed regardless of their type.
595
+
596
+ Returns [] on any error. Never raises.
597
+ """
598
+ try:
599
+ result: list[tuple[str, int]] = []
600
+ body = class_node.child_by_field_name("body")
601
+ if body is None:
602
+ return result
603
+
604
+ for child in body.named_children:
605
+ if child.type == "field_declaration":
606
+ try:
607
+ _cpp_collect_field_decl(child, class_name, result)
608
+ except Exception: # noqa: BLE001
609
+ pass
610
+
611
+ return result
612
+
613
+ except Exception as exc: # noqa: BLE001
614
+ logger.debug("collect_field_symbols_cpp: failed for class %r: %r", class_name, exc)
615
+ return []
616
+
617
+
618
+ def _cpp_collect_field_decl(
619
+ field_decl: Node,
620
+ class_name: str,
621
+ result: list[tuple[str, int]],
622
+ ) -> None:
623
+ """Extract field symbols from a single C++ field_declaration node.
624
+
625
+ C++ field_declaration in a class body:
626
+ type_specifier declarator+ ';'
627
+ The declarator(s) can be identifier or field_identifier nodes.
628
+ """
629
+ for child in field_decl.children:
630
+ # In C++ class body, field names appear as field_identifier nodes.
631
+ # Simple cases also have identifier nodes.
632
+ if child.type in ("field_identifier", "identifier"):
633
+ field_name = _text(child).strip()
634
+ # WHY keyword filter: C++ tree-sitter sometimes surfaces access specifiers
635
+ # and storage/qualifier keywords as bare identifier tokens inside a
636
+ # field_declaration (e.g. `static int x` may produce an 'identifier' child
637
+ # with text 'static'). These are not field names and must be excluded to
638
+ # avoid creating phantom field symbols with names like 'static' or 'const'.
639
+ if field_name and field_name not in ("public", "private", "protected",
640
+ "static", "const", "virtual",
641
+ "inline", "explicit", "override"):
642
+ qualified = f"{class_name}.{field_name}"
643
+ result.append((qualified, field_decl.start_point[0] + 1))