seam-code 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- seam/__init__.py +3 -0
- seam/_data/schema.sql +225 -0
- seam/_web/assets/index-BL_tqprR.js +216 -0
- seam/_web/assets/index-GTKUhVyD.css +1 -0
- seam/_web/index.html +13 -0
- seam/analysis/__init__.py +14 -0
- seam/analysis/affected.py +254 -0
- seam/analysis/builtins.py +966 -0
- seam/analysis/byte_budget.py +217 -0
- seam/analysis/changes.py +709 -0
- seam/analysis/cluster_naming.py +260 -0
- seam/analysis/clustering.py +216 -0
- seam/analysis/confidence.py +699 -0
- seam/analysis/embeddings.py +195 -0
- seam/analysis/flows.py +708 -0
- seam/analysis/impact.py +444 -0
- seam/analysis/imports.py +994 -0
- seam/analysis/imports_ext.py +780 -0
- seam/analysis/imports_resolve.py +176 -0
- seam/analysis/processes.py +453 -0
- seam/analysis/relevance.py +155 -0
- seam/analysis/rwr.py +129 -0
- seam/analysis/staleness.py +328 -0
- seam/analysis/steer.py +282 -0
- seam/analysis/synthesis.py +253 -0
- seam/analysis/synthesis_channels.py +433 -0
- seam/analysis/testpaths.py +103 -0
- seam/analysis/traversal.py +470 -0
- seam/cli/__init__.py +0 -0
- seam/cli/install.py +232 -0
- seam/cli/main.py +2602 -0
- seam/cli/output.py +137 -0
- seam/cli/read.py +244 -0
- seam/cli/serve.py +145 -0
- seam/config.py +551 -0
- seam/indexer/__init__.py +0 -0
- seam/indexer/cluster_index.py +425 -0
- seam/indexer/db.py +496 -0
- seam/indexer/embedding_index.py +183 -0
- seam/indexer/field_access.py +536 -0
- seam/indexer/field_access_c_cpp.py +643 -0
- seam/indexer/field_access_ext.py +708 -0
- seam/indexer/field_access_ext2.py +408 -0
- seam/indexer/field_access_go_rust.py +737 -0
- seam/indexer/field_access_php_swift.py +888 -0
- seam/indexer/field_access_ts.py +626 -0
- seam/indexer/graph.py +321 -0
- seam/indexer/graph_c.py +562 -0
- seam/indexer/graph_c_cpp.py +39 -0
- seam/indexer/graph_common.py +644 -0
- seam/indexer/graph_cpp.py +615 -0
- seam/indexer/graph_csharp.py +651 -0
- seam/indexer/graph_go.py +723 -0
- seam/indexer/graph_go_rust.py +39 -0
- seam/indexer/graph_java.py +689 -0
- seam/indexer/graph_java_csharp.py +38 -0
- seam/indexer/graph_php.py +914 -0
- seam/indexer/graph_python.py +628 -0
- seam/indexer/graph_ruby.py +748 -0
- seam/indexer/graph_rust.py +653 -0
- seam/indexer/graph_scope_infer.py +902 -0
- seam/indexer/graph_scope_infer_ext.py +723 -0
- seam/indexer/graph_scope_infer_ext2.py +992 -0
- seam/indexer/graph_swift.py +1014 -0
- seam/indexer/graph_swift_infer.py +515 -0
- seam/indexer/graph_typescript.py +663 -0
- seam/indexer/migrations.py +816 -0
- seam/indexer/parser.py +204 -0
- seam/indexer/pipeline.py +197 -0
- seam/indexer/signatures.py +634 -0
- seam/indexer/signatures_ext.py +780 -0
- seam/indexer/sync.py +287 -0
- seam/indexer/synthesis_index.py +291 -0
- seam/indexer/tokenize.py +79 -0
- seam/installer/__init__.py +67 -0
- seam/installer/claude.py +97 -0
- seam/installer/codex.py +94 -0
- seam/installer/core.py +127 -0
- seam/installer/cursor.py +61 -0
- seam/installer/guide.py +110 -0
- seam/installer/jsonfile.py +85 -0
- seam/installer/markdownfile.py +146 -0
- seam/installer/tomlfile.py +72 -0
- seam/query/__init__.py +0 -0
- seam/query/clusters.py +206 -0
- seam/query/comments.py +217 -0
- seam/query/context.py +293 -0
- seam/query/engine.py +940 -0
- seam/query/fts.py +328 -0
- seam/query/names.py +470 -0
- seam/query/pack.py +433 -0
- seam/query/semantic.py +339 -0
- seam/query/structure.py +727 -0
- seam/server/__init__.py +0 -0
- seam/server/graph_api.py +437 -0
- seam/server/handler_common.py +323 -0
- seam/server/impact_handler.py +615 -0
- seam/server/mcp.py +556 -0
- seam/server/tools.py +697 -0
- seam/server/trace_handler.py +184 -0
- seam/server/web.py +922 -0
- seam/watcher/__init__.py +0 -0
- seam/watcher/__main__.py +56 -0
- seam/watcher/daemon.py +237 -0
- seam_code-0.3.0.dist-info/METADATA +318 -0
- seam_code-0.3.0.dist-info/RECORD +109 -0
- seam_code-0.3.0.dist-info/WHEEL +4 -0
- seam_code-0.3.0.dist-info/entry_points.txt +2 -0
- seam_code-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,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))
|