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