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