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,408 @@
|
|
|
1
|
+
"""Field-access edge helper — Ruby classification (façade for PHP + Swift too).
|
|
2
|
+
|
|
3
|
+
This file contains the Ruby field-access implementation and re-exports the PHP and
|
|
4
|
+
Swift implementations from field_access_php_swift so callers can continue using
|
|
5
|
+
`from seam.indexer.field_access_ext2 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_ext2 (leaf — Java/C#/C++/Ruby/PHP receiver-type inference)
|
|
13
|
+
↑
|
|
14
|
+
field_access_php_swift (leaf — PHP and Swift field-access implementation)
|
|
15
|
+
field_access_ext2 (this file — Ruby implementation + façade re-exports)
|
|
16
|
+
↑
|
|
17
|
+
graph_ruby (calls extract_field_accesses_ruby + collect_field_symbols_ruby)
|
|
18
|
+
graph_php (calls extract_field_accesses_php + collect_field_symbols_php)
|
|
19
|
+
graph_swift (calls extract_field_accesses_swift + collect_field_symbols_swift)
|
|
20
|
+
|
|
21
|
+
WHY split: the combined Ruby/PHP/Swift implementation exceeded the 1000-line limit.
|
|
22
|
+
PHP and Swift are in field_access_php_swift.py; all public names re-exported here.
|
|
23
|
+
|
|
24
|
+
Ruby:
|
|
25
|
+
instance_variable node (@balance) — standalone, belongs to enclosing class.
|
|
26
|
+
assignment with instance_variable LHS → write.
|
|
27
|
+
operator_assignment (+=, -=, etc.) with instance_variable LHS → write.
|
|
28
|
+
Every other instance_variable occurrence → read.
|
|
29
|
+
Call nodes (call) → NOT a field edge (stays a 'call' edge).
|
|
30
|
+
|
|
31
|
+
NEVER RAISES: all public functions have a backstop try/except and return [] on error.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import logging
|
|
35
|
+
|
|
36
|
+
from tree_sitter import Node
|
|
37
|
+
|
|
38
|
+
# Re-export PHP and Swift implementations so callers need no import changes.
|
|
39
|
+
# noqa: F401 — intentional re-exports for the façade pattern.
|
|
40
|
+
from seam.indexer.field_access_php_swift import ( # noqa: F401
|
|
41
|
+
collect_field_symbols_php,
|
|
42
|
+
collect_field_symbols_swift,
|
|
43
|
+
extract_field_accesses_php,
|
|
44
|
+
extract_field_accesses_swift,
|
|
45
|
+
)
|
|
46
|
+
from seam.indexer.graph_common import _text
|
|
47
|
+
|
|
48
|
+
logger = logging.getLogger(__name__)
|
|
49
|
+
|
|
50
|
+
# Return type for the public API: (source_fn, target_field, mode, line)
|
|
51
|
+
# mode is "reads" or "writes"
|
|
52
|
+
FieldAccess = tuple[str, str, str, int]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
56
|
+
# Ruby field-access classification (A3 Slice 5)
|
|
57
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
58
|
+
#
|
|
59
|
+
# Ruby instance variables (@ivar) are bespoke:
|
|
60
|
+
# - They appear as 'instance_variable' AST nodes (no separate receiver/object).
|
|
61
|
+
# - They always belong to the enclosing class (no receiver to resolve).
|
|
62
|
+
# - The field symbol name strips the '@' prefix: '@balance' → 'balance'.
|
|
63
|
+
# - qualified target = 'ClassName.balance' (always EXTRACTED when class_name known).
|
|
64
|
+
#
|
|
65
|
+
# Assignment forms:
|
|
66
|
+
# assignment: LHS is instance_variable → write
|
|
67
|
+
# operator_assignment (+=, -=, etc.): LHS is instance_variable → write
|
|
68
|
+
# All other instance_variable occurrences → read
|
|
69
|
+
#
|
|
70
|
+
# NOT a field access:
|
|
71
|
+
# call node (Ruby call AST for foo() or obj.foo()) → these stay 'call' edges.
|
|
72
|
+
# Ruby instance methods use 'call' nodes — no confusion with instance_variable nodes.
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def extract_field_accesses_ruby(
|
|
76
|
+
method_body: Node,
|
|
77
|
+
source_fn: str,
|
|
78
|
+
class_name: str | None,
|
|
79
|
+
) -> list[FieldAccess]:
|
|
80
|
+
"""Extract field accesses (reads and writes) from a Ruby method body.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
method_body: The 'body_statement' Node of the method body.
|
|
84
|
+
source_fn: Qualified name of the enclosing method, e.g. 'Account.deposit'.
|
|
85
|
+
class_name: Enclosing class/module name (None for top-level methods).
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
List of (source_fn, target_field, mode, line) tuples.
|
|
89
|
+
target_field = 'ClassName.field' when class_name known; bare 'field' otherwise.
|
|
90
|
+
mode = 'reads' | 'writes'.
|
|
91
|
+
line = 1-based source line.
|
|
92
|
+
Returns [] on any error.
|
|
93
|
+
|
|
94
|
+
Never raises.
|
|
95
|
+
"""
|
|
96
|
+
result: list[FieldAccess] = []
|
|
97
|
+
try:
|
|
98
|
+
_ruby_walk_body(method_body, source_fn, class_name, result)
|
|
99
|
+
except Exception as exc: # noqa: BLE001
|
|
100
|
+
logger.debug(
|
|
101
|
+
"extract_field_accesses_ruby: failed for source=%r: %r", source_fn, exc
|
|
102
|
+
)
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _ruby_walk_body(
|
|
107
|
+
node: Node,
|
|
108
|
+
source_fn: str,
|
|
109
|
+
class_name: str | None,
|
|
110
|
+
result: list[FieldAccess],
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Recursively walk a Ruby method body collecting @ivar field accesses.
|
|
113
|
+
|
|
114
|
+
Does NOT recurse into nested method/singleton_method nodes (their own scope).
|
|
115
|
+
Does NOT recurse into nested class/module nodes (their own scope).
|
|
116
|
+
"""
|
|
117
|
+
for child in node.children:
|
|
118
|
+
_ruby_walk_stmt(child, source_fn, class_name, result)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _ruby_walk_stmt(
|
|
122
|
+
node: Node,
|
|
123
|
+
source_fn: str,
|
|
124
|
+
class_name: str | None,
|
|
125
|
+
result: list[FieldAccess],
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Walk a single Ruby statement node for @ivar field accesses.
|
|
128
|
+
|
|
129
|
+
Handles:
|
|
130
|
+
assignment with instance_variable LHS → write; RHS may contain reads.
|
|
131
|
+
operator_assignment (+=, -=, etc.) with instance_variable LHS → write.
|
|
132
|
+
instance_variable as a standalone expression → read.
|
|
133
|
+
All other nodes: recurse collecting reads from instance_variable nodes.
|
|
134
|
+
Skips nested method/singleton_method/class/module definitions (own scope).
|
|
135
|
+
"""
|
|
136
|
+
t = node.type
|
|
137
|
+
|
|
138
|
+
# Skip nested scope-creating constructs.
|
|
139
|
+
if t in ("method", "singleton_method", "class", "module"):
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
# assignment: LHS instance_variable → write; RHS may contain reads.
|
|
143
|
+
if t == "assignment":
|
|
144
|
+
_ruby_handle_assignment(node, source_fn, class_name, result)
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
# operator_assignment (+=, -=, etc.): LHS instance_variable → write.
|
|
148
|
+
if t == "operator_assignment":
|
|
149
|
+
_ruby_handle_operator_assignment(node, source_fn, class_name, result)
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
# A bare instance_variable in non-assignment context → read.
|
|
153
|
+
if t == "instance_variable":
|
|
154
|
+
_ruby_emit_read(node, source_fn, class_name, result)
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
# Recurse into all other node types (return, if, while, etc.).
|
|
158
|
+
for child in node.children:
|
|
159
|
+
_ruby_walk_stmt(child, source_fn, class_name, result)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _ruby_handle_assignment(
|
|
163
|
+
node: Node,
|
|
164
|
+
source_fn: str,
|
|
165
|
+
class_name: str | None,
|
|
166
|
+
result: list[FieldAccess],
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Handle Ruby assignment: LHS instance_variable → write; RHS → reads.
|
|
169
|
+
|
|
170
|
+
Ruby assignment node children: [LHS, '=', RHS]
|
|
171
|
+
The LHS is the first child; the RHS is the last child.
|
|
172
|
+
"""
|
|
173
|
+
children = list(node.children)
|
|
174
|
+
if not children:
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
# LHS: first child (may be instance_variable)
|
|
178
|
+
lhs = children[0]
|
|
179
|
+
if lhs.type == "instance_variable":
|
|
180
|
+
field_name = _ruby_ivar_field_name(lhs)
|
|
181
|
+
if field_name:
|
|
182
|
+
target = _ruby_qualify(field_name, class_name)
|
|
183
|
+
result.append((source_fn, target, "writes", lhs.start_point[0] + 1))
|
|
184
|
+
|
|
185
|
+
# RHS: last non-'=' child — collect reads.
|
|
186
|
+
if len(children) >= 3:
|
|
187
|
+
rhs = children[-1]
|
|
188
|
+
_ruby_collect_reads_recursive(rhs, source_fn, class_name, result)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _ruby_handle_operator_assignment(
|
|
192
|
+
node: Node,
|
|
193
|
+
source_fn: str,
|
|
194
|
+
class_name: str | None,
|
|
195
|
+
result: list[FieldAccess],
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Handle Ruby operator_assignment (+=, -=, etc.): LHS instance_variable → write.
|
|
198
|
+
|
|
199
|
+
operator_assignment node children: [LHS, operator, RHS]
|
|
200
|
+
The LHS is the first child; the RHS is the last child.
|
|
201
|
+
Both reads (implicit in +=) and writes are captured:
|
|
202
|
+
- The LHS instance_variable is a WRITE (mutation takes place).
|
|
203
|
+
- The RHS may contain additional reads.
|
|
204
|
+
WHY only write: story 13 requires augmented assignment to be classified as write.
|
|
205
|
+
We do NOT also emit a read for the LHS (the pre-mutation read is implicit and
|
|
206
|
+
capturing it would produce a duplicate reads edge for the same ivar in the same
|
|
207
|
+
operation, which misleads blast-radius analysis).
|
|
208
|
+
"""
|
|
209
|
+
children = list(node.children)
|
|
210
|
+
if not children:
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
# LHS: first child
|
|
214
|
+
lhs = children[0]
|
|
215
|
+
if lhs.type == "instance_variable":
|
|
216
|
+
field_name = _ruby_ivar_field_name(lhs)
|
|
217
|
+
if field_name:
|
|
218
|
+
target = _ruby_qualify(field_name, class_name)
|
|
219
|
+
result.append((source_fn, target, "writes", lhs.start_point[0] + 1))
|
|
220
|
+
|
|
221
|
+
# RHS: last child — collect reads.
|
|
222
|
+
if len(children) >= 3:
|
|
223
|
+
rhs = children[-1]
|
|
224
|
+
_ruby_collect_reads_recursive(rhs, source_fn, class_name, result)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _ruby_emit_read(
|
|
228
|
+
node: Node,
|
|
229
|
+
source_fn: str,
|
|
230
|
+
class_name: str | None,
|
|
231
|
+
result: list[FieldAccess],
|
|
232
|
+
) -> None:
|
|
233
|
+
"""Emit a reads edge for a Ruby instance_variable node.
|
|
234
|
+
|
|
235
|
+
Ruby instance variables are NEVER in call position (calls use a separate 'call'
|
|
236
|
+
node type — this is different from Python/TS where the attribute node itself
|
|
237
|
+
may appear as the function of a call expression).
|
|
238
|
+
So any bare instance_variable node that reaches this function is a read.
|
|
239
|
+
|
|
240
|
+
WHY no receiver resolution step: Ruby @ivars are implicitly bound to the enclosing
|
|
241
|
+
class — there is no receiver expression to resolve. `@balance` always belongs to
|
|
242
|
+
`self` (the current instance), so the qualified target is always `ClassName.balance`
|
|
243
|
+
when inside a class method. No var_types lookup is needed or possible.
|
|
244
|
+
"""
|
|
245
|
+
field_name = _ruby_ivar_field_name(node)
|
|
246
|
+
if field_name:
|
|
247
|
+
target = _ruby_qualify(field_name, class_name)
|
|
248
|
+
result.append((source_fn, target, "reads", node.start_point[0] + 1))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _ruby_collect_reads_recursive(
|
|
252
|
+
node: Node,
|
|
253
|
+
source_fn: str,
|
|
254
|
+
class_name: str | None,
|
|
255
|
+
result: list[FieldAccess],
|
|
256
|
+
) -> None:
|
|
257
|
+
"""Recursively collect reads from a Ruby expression node.
|
|
258
|
+
|
|
259
|
+
Skips nested scope-creating constructs (method/class/module).
|
|
260
|
+
Emits reads for instance_variable nodes that are not in write position.
|
|
261
|
+
"""
|
|
262
|
+
t = node.type
|
|
263
|
+
if t in ("method", "singleton_method", "class", "module"):
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
if t == "instance_variable":
|
|
267
|
+
_ruby_emit_read(node, source_fn, class_name, result)
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
for child in node.children:
|
|
271
|
+
_ruby_collect_reads_recursive(child, source_fn, class_name, result)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _ruby_ivar_field_name(ivar_node: Node) -> str | None:
|
|
275
|
+
"""Extract the field name from a Ruby instance_variable node, stripping '@'.
|
|
276
|
+
|
|
277
|
+
Ruby instance_variable text is '@field_name'. We strip the '@' prefix to produce
|
|
278
|
+
the field name used in qualified targets ('ClassName.field_name').
|
|
279
|
+
|
|
280
|
+
Returns None if the text is empty or only '@'.
|
|
281
|
+
"""
|
|
282
|
+
text = _text(ivar_node)
|
|
283
|
+
if not text or not text.startswith("@"):
|
|
284
|
+
return None
|
|
285
|
+
field_name = text[1:] # Strip leading '@'
|
|
286
|
+
return field_name if field_name else None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _ruby_qualify(field_name: str, class_name: str | None) -> str:
|
|
290
|
+
"""Qualify a Ruby field name with the enclosing class name.
|
|
291
|
+
|
|
292
|
+
Returns 'ClassName.field_name' when class_name is known (EXTRACTED),
|
|
293
|
+
or bare 'field_name' when at module level (AMBIGUOUS at read time).
|
|
294
|
+
"""
|
|
295
|
+
if class_name:
|
|
296
|
+
return f"{class_name}.{field_name}"
|
|
297
|
+
return field_name
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def collect_field_symbols_ruby(
|
|
301
|
+
class_node: Node,
|
|
302
|
+
class_name: str,
|
|
303
|
+
) -> list[tuple[str, int]]:
|
|
304
|
+
"""Collect (qualified_field_name, line) pairs from a Ruby class node.
|
|
305
|
+
|
|
306
|
+
Returns field symbols for:
|
|
307
|
+
@ivar first-assignment sites in ANY method body (not just initialize).
|
|
308
|
+
The first occurrence of each @ivar (by field name) is used as the symbol
|
|
309
|
+
location. Dedup by (class_name, field_name) — multiple assignments → one symbol.
|
|
310
|
+
|
|
311
|
+
NOTE: Ruby has no static field declarations (unlike Python's class-level annotations).
|
|
312
|
+
The only way to discover @ivar fields is from assignment sites in method bodies.
|
|
313
|
+
We scan ALL methods (not just initialize) to catch ivars assigned in setters etc.
|
|
314
|
+
First-seen wins for the line number.
|
|
315
|
+
|
|
316
|
+
WHY scan all methods and not just initialize: Ruby setters like `def balance=(v)`
|
|
317
|
+
assign `@balance = v` — these are the canonical assignment sites for that field.
|
|
318
|
+
Restricting to initialize would miss such setter-defined fields entirely.
|
|
319
|
+
The first-seen line number gives a reasonable canonical location even when an ivar
|
|
320
|
+
is first assigned in a setter rather than the constructor.
|
|
321
|
+
|
|
322
|
+
Returns [] on any error. Never raises.
|
|
323
|
+
"""
|
|
324
|
+
try:
|
|
325
|
+
seen_fields: set[str] = set()
|
|
326
|
+
result: list[tuple[str, int]] = []
|
|
327
|
+
|
|
328
|
+
body = class_node.child_by_field_name("body")
|
|
329
|
+
if body is None:
|
|
330
|
+
# Ruby class body is in a body_statement child
|
|
331
|
+
for child in class_node.children:
|
|
332
|
+
if child.type == "body_statement":
|
|
333
|
+
body = child
|
|
334
|
+
break
|
|
335
|
+
if body is None:
|
|
336
|
+
return result
|
|
337
|
+
|
|
338
|
+
# Scan all method/singleton_method children for @ivar first-assignments.
|
|
339
|
+
for child in body.children:
|
|
340
|
+
try:
|
|
341
|
+
if child.type in ("method", "singleton_method"):
|
|
342
|
+
_ruby_scan_method_for_ivar_assignments(
|
|
343
|
+
child, class_name, seen_fields, result
|
|
344
|
+
)
|
|
345
|
+
except Exception: # noqa: BLE001
|
|
346
|
+
pass
|
|
347
|
+
|
|
348
|
+
return result
|
|
349
|
+
|
|
350
|
+
except Exception as exc: # noqa: BLE001
|
|
351
|
+
logger.debug("collect_field_symbols_ruby: failed for class %r: %r", class_name, exc)
|
|
352
|
+
return []
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _ruby_scan_method_for_ivar_assignments(
|
|
356
|
+
method_node: Node,
|
|
357
|
+
class_name: str,
|
|
358
|
+
seen: set[str],
|
|
359
|
+
result: list[tuple[str, int]],
|
|
360
|
+
) -> None:
|
|
361
|
+
"""Scan a Ruby method body for @ivar = ... assignments, collecting field symbols.
|
|
362
|
+
|
|
363
|
+
Scans the method's body_statement for any assignment or operator_assignment
|
|
364
|
+
whose LHS is an instance_variable. Only the FIRST occurrence of each @ivar
|
|
365
|
+
(by field name) is recorded.
|
|
366
|
+
"""
|
|
367
|
+
body = method_node.child_by_field_name("body")
|
|
368
|
+
if body is None:
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
for stmt in body.children:
|
|
372
|
+
_ruby_try_collect_ivar_stmt(stmt, class_name, seen, result)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _ruby_try_collect_ivar_stmt(
|
|
376
|
+
stmt: Node,
|
|
377
|
+
class_name: str,
|
|
378
|
+
seen: set[str],
|
|
379
|
+
result: list[tuple[str, int]],
|
|
380
|
+
) -> None:
|
|
381
|
+
"""Try to collect an @ivar field symbol from a single Ruby statement.
|
|
382
|
+
|
|
383
|
+
Handles:
|
|
384
|
+
assignment: @ivar = ...
|
|
385
|
+
operator_assignment: @ivar += ...
|
|
386
|
+
Both forms indicate the @ivar is a stored field of the class.
|
|
387
|
+
"""
|
|
388
|
+
t = stmt.type
|
|
389
|
+
if t not in ("assignment", "operator_assignment"):
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
children = list(stmt.children)
|
|
393
|
+
if not children:
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
lhs = children[0]
|
|
397
|
+
if lhs.type != "instance_variable":
|
|
398
|
+
return
|
|
399
|
+
|
|
400
|
+
field_name = _ruby_ivar_field_name(lhs)
|
|
401
|
+
if not field_name:
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
qualified = f"{class_name}.{field_name}"
|
|
405
|
+
if qualified not in seen:
|
|
406
|
+
seen.add(qualified)
|
|
407
|
+
result.append((qualified, stmt.start_point[0] + 1))
|
|
408
|
+
|