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,626 @@
|
|
|
1
|
+
"""Field-access edge helper — TypeScript / JavaScript read/write classification.
|
|
2
|
+
|
|
3
|
+
LAYER: leaf — imports only stdlib + tree_sitter + the existing resolve_receiver_type
|
|
4
|
+
helpers from graph_scope_infer. Never imports from graph.py, db.py, or any non-leaf
|
|
5
|
+
seam module.
|
|
6
|
+
|
|
7
|
+
LAYERING:
|
|
8
|
+
graph_common (leaf — no seam deps)
|
|
9
|
+
graph_scope_infer (leaf — Python/TS/JS receiver-type inference)
|
|
10
|
+
↑
|
|
11
|
+
field_access_ts (this file — field-access classification for TypeScript/JS)
|
|
12
|
+
↑
|
|
13
|
+
field_access (re-exports extract_field_accesses_typescript +
|
|
14
|
+
collect_field_symbols_typescript for backward compat)
|
|
15
|
+
graph_typescript (calls those functions via field_access import)
|
|
16
|
+
|
|
17
|
+
WHY a separate module from field_access.py:
|
|
18
|
+
field_access.py would otherwise exceed the 1000-line limit when Python + TS/JS +
|
|
19
|
+
Go + Rust are all in one file. This follows the graph_scope_infer precedent.
|
|
20
|
+
|
|
21
|
+
TS/JS AST patterns for field accesses:
|
|
22
|
+
member_expression → the node type for both reads AND call-position references
|
|
23
|
+
assignment_expression LHS member_expression → write
|
|
24
|
+
augmented_assignment_expression LHS member_expression → write
|
|
25
|
+
unary_expression 'delete' → write (subtree contains the member_expression)
|
|
26
|
+
call_expression function=member_expression → NOT a field access (method call)
|
|
27
|
+
|
|
28
|
+
Receiver resolution reuses _TS_SELF_NAMES ({'this'}) and resolve_receiver_type,
|
|
29
|
+
matching exactly the approach used by the call-edge extractor in graph_typescript.py.
|
|
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
|
+
from seam.indexer.graph_common import _text
|
|
39
|
+
from seam.indexer.graph_scope_infer import (
|
|
40
|
+
_TS_SELF_NAMES,
|
|
41
|
+
resolve_receiver_type,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
# Return type for the public API: (source_fn, target_field, mode, line)
|
|
47
|
+
# mode is "reads" or "writes"
|
|
48
|
+
FieldAccess = tuple[str, str, str, int]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def extract_field_accesses_typescript(
|
|
52
|
+
func_body: Node,
|
|
53
|
+
source_fn: str,
|
|
54
|
+
class_name: str | None,
|
|
55
|
+
var_types: dict[str, str],
|
|
56
|
+
) -> list[FieldAccess]:
|
|
57
|
+
"""Extract field accesses (reads and writes) from a TypeScript/JS function body.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
func_body: The statement_block Node of the method/function.
|
|
61
|
+
source_fn: Qualified name of the enclosing function, e.g. 'Account.get'.
|
|
62
|
+
class_name: Enclosing class name (None for module-level functions).
|
|
63
|
+
var_types: Scope map: param/local name → type name (class fields merged
|
|
64
|
+
with per-function param/local bindings by the caller).
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
List of (source_fn, target_field, mode, line) tuples.
|
|
68
|
+
target_field = 'Type.field' when receiver resolves; bare 'field' when unknown.
|
|
69
|
+
mode = 'reads' | 'writes'.
|
|
70
|
+
line = 1-based source line of the member_expression.
|
|
71
|
+
Returns [] on any error.
|
|
72
|
+
|
|
73
|
+
Never raises.
|
|
74
|
+
"""
|
|
75
|
+
result: list[FieldAccess] = []
|
|
76
|
+
try:
|
|
77
|
+
_ts_walk_body(func_body, source_fn, class_name, var_types, result)
|
|
78
|
+
except Exception as exc: # noqa: BLE001
|
|
79
|
+
logger.debug(
|
|
80
|
+
"extract_field_accesses_typescript: failed for source=%r: %r", source_fn, exc
|
|
81
|
+
)
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _ts_walk_body(
|
|
86
|
+
node: Node,
|
|
87
|
+
source_fn: str,
|
|
88
|
+
class_name: str | None,
|
|
89
|
+
var_types: dict[str, str],
|
|
90
|
+
result: list[FieldAccess],
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Recursively walk a TS/JS function body collecting field accesses.
|
|
93
|
+
|
|
94
|
+
Does NOT recurse into nested function/class definitions (separate scope).
|
|
95
|
+
"""
|
|
96
|
+
for child in node.children:
|
|
97
|
+
_ts_walk_stmt(child, source_fn, class_name, var_types, result)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _ts_walk_stmt(
|
|
101
|
+
node: Node,
|
|
102
|
+
source_fn: str,
|
|
103
|
+
class_name: str | None,
|
|
104
|
+
var_types: dict[str, str],
|
|
105
|
+
result: list[FieldAccess],
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Walk a single TS/JS statement node for field accesses.
|
|
108
|
+
|
|
109
|
+
Handles:
|
|
110
|
+
expression_statement → assignment_expression / augmented_assignment_expression
|
|
111
|
+
/ plain member_expression reads / unary delete
|
|
112
|
+
return_statement, if_statement, for/while loops → recurse
|
|
113
|
+
Skips nested function/class/method definitions (their own scope).
|
|
114
|
+
"""
|
|
115
|
+
t = node.type
|
|
116
|
+
|
|
117
|
+
# Skip nested scope-creating constructs — they have their own scope.
|
|
118
|
+
if t in (
|
|
119
|
+
"function_declaration", "function_expression", "arrow_function",
|
|
120
|
+
"method_definition", "class_declaration",
|
|
121
|
+
):
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
# assignment_expression: this.x = v → write; RHS may contain reads.
|
|
125
|
+
if t == "assignment_expression":
|
|
126
|
+
_ts_handle_assignment(node, source_fn, class_name, var_types, result)
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
# augmented_assignment_expression: this.x += v → write; RHS may contain reads.
|
|
130
|
+
if t == "augmented_assignment_expression":
|
|
131
|
+
_ts_handle_augmented_assignment(node, source_fn, class_name, var_types, result)
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
# unary_expression: 'delete this.x' → write.
|
|
135
|
+
if t == "unary_expression":
|
|
136
|
+
_ts_handle_delete(node, source_fn, class_name, var_types, result)
|
|
137
|
+
# Also recurse in case there are sub-reads inside complex delete targets.
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
# call_expression: the function child may be a member_expression (method call).
|
|
141
|
+
# We do NOT emit a field edge for it — it's a call. But arguments may contain reads.
|
|
142
|
+
# WHY handle call_expression explicitly instead of relying on _ts_emit_read_if_not_in_call:
|
|
143
|
+
# the recursive fallback path below would recurse into the call_expression's children and
|
|
144
|
+
# eventually reach the callee member_expression. _ts_emit_read_if_not_in_call does guard
|
|
145
|
+
# against that, but handling call_expression here first short-circuits cleanly: only the
|
|
146
|
+
# arguments are recursed, the callee is never reached.
|
|
147
|
+
if t == "call_expression":
|
|
148
|
+
args = node.child_by_field_name("arguments")
|
|
149
|
+
if args is not None:
|
|
150
|
+
for arg in args.children:
|
|
151
|
+
_ts_walk_stmt(arg, source_fn, class_name, var_types, result)
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
# A bare member_expression (not in call position and not an assignment LHS)
|
|
155
|
+
# e.g. `this.balance;` as an expression statement → read.
|
|
156
|
+
if t == "member_expression":
|
|
157
|
+
_ts_emit_read_if_not_in_call(node, source_fn, class_name, var_types, result)
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
# Recurse into all other node types (return_statement, if_statement, for_*,
|
|
161
|
+
# while_statement, block, expression_statement, etc.).
|
|
162
|
+
for child in node.children:
|
|
163
|
+
_ts_walk_stmt(child, source_fn, class_name, var_types, result)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _ts_handle_assignment(
|
|
167
|
+
node: Node,
|
|
168
|
+
source_fn: str,
|
|
169
|
+
class_name: str | None,
|
|
170
|
+
var_types: dict[str, str],
|
|
171
|
+
result: list[FieldAccess],
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Handle TS assignment_expression: left=member_expression → write; right → reads."""
|
|
174
|
+
left = node.child_by_field_name("left")
|
|
175
|
+
if left is not None and left.type == "member_expression":
|
|
176
|
+
acc = _ts_classify_member(left, class_name, var_types)
|
|
177
|
+
if acc is not None:
|
|
178
|
+
target, _recv = acc
|
|
179
|
+
result.append((source_fn, target, "writes", left.start_point[0] + 1))
|
|
180
|
+
|
|
181
|
+
right = node.child_by_field_name("right")
|
|
182
|
+
if right is not None:
|
|
183
|
+
_ts_collect_reads_recursive(right, source_fn, class_name, var_types, result)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _ts_handle_augmented_assignment(
|
|
187
|
+
node: Node,
|
|
188
|
+
source_fn: str,
|
|
189
|
+
class_name: str | None,
|
|
190
|
+
var_types: dict[str, str],
|
|
191
|
+
result: list[FieldAccess],
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Handle TS augmented_assignment_expression: left → write; right → reads.
|
|
194
|
+
|
|
195
|
+
TS tree-sitter grammar: augmented_assignment_expression has 'left' and 'right' fields.
|
|
196
|
+
"""
|
|
197
|
+
left = node.child_by_field_name("left")
|
|
198
|
+
if left is not None and left.type == "member_expression":
|
|
199
|
+
acc = _ts_classify_member(left, class_name, var_types)
|
|
200
|
+
if acc is not None:
|
|
201
|
+
target, _recv = acc
|
|
202
|
+
result.append((source_fn, target, "writes", left.start_point[0] + 1))
|
|
203
|
+
|
|
204
|
+
right = node.child_by_field_name("right")
|
|
205
|
+
if right is not None:
|
|
206
|
+
_ts_collect_reads_recursive(right, source_fn, class_name, var_types, result)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _ts_handle_delete(
|
|
210
|
+
node: Node,
|
|
211
|
+
source_fn: str,
|
|
212
|
+
class_name: str | None,
|
|
213
|
+
var_types: dict[str, str],
|
|
214
|
+
result: list[FieldAccess],
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Handle TS delete unary_expression: any member_expression child → write.
|
|
217
|
+
|
|
218
|
+
Tree-sitter: unary_expression has operator ('delete') + argument. The argument
|
|
219
|
+
may be a member_expression directly, or wrapped in a parenthesized_expression
|
|
220
|
+
(e.g. `delete (this as any).balance`). We scan the first non-operator child.
|
|
221
|
+
"""
|
|
222
|
+
for child in node.children:
|
|
223
|
+
if child.type in ("delete",):
|
|
224
|
+
continue # Skip the operator keyword node
|
|
225
|
+
# Walk into parenthesized_expression or directly handle member_expression.
|
|
226
|
+
_ts_find_member_writes(child, source_fn, class_name, var_types, result)
|
|
227
|
+
break # Only one operand for 'delete'
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _ts_find_member_writes(
|
|
231
|
+
node: Node,
|
|
232
|
+
source_fn: str,
|
|
233
|
+
class_name: str | None,
|
|
234
|
+
var_types: dict[str, str],
|
|
235
|
+
result: list[FieldAccess],
|
|
236
|
+
) -> None:
|
|
237
|
+
"""Find any member_expression inside a delete operand and emit as writes.
|
|
238
|
+
|
|
239
|
+
Handles both direct member_expression and member inside parenthesized_expression
|
|
240
|
+
or type_assertion expressions (e.g. `(this as any).balance`).
|
|
241
|
+
"""
|
|
242
|
+
if node.type == "member_expression":
|
|
243
|
+
acc = _ts_classify_member(node, class_name, var_types)
|
|
244
|
+
if acc is not None:
|
|
245
|
+
target, _recv = acc
|
|
246
|
+
result.append((source_fn, target, "writes", node.start_point[0] + 1))
|
|
247
|
+
return
|
|
248
|
+
# Recurse into wrappers (parenthesized_expression, type_assertion, as_expression).
|
|
249
|
+
for child in node.children:
|
|
250
|
+
_ts_find_member_writes(child, source_fn, class_name, var_types, result)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _ts_emit_read_if_not_in_call(
|
|
254
|
+
node: Node,
|
|
255
|
+
source_fn: str,
|
|
256
|
+
class_name: str | None,
|
|
257
|
+
var_types: dict[str, str],
|
|
258
|
+
result: list[FieldAccess],
|
|
259
|
+
) -> None:
|
|
260
|
+
"""Emit a reads edge for a member_expression node that is NOT in call position.
|
|
261
|
+
|
|
262
|
+
A member_expression is in call position when its parent is a call_expression AND
|
|
263
|
+
it IS the 'function' field of that call. We skip those (method calls).
|
|
264
|
+
|
|
265
|
+
WHY start_point comparison: same reasoning as Python — tree-sitter creates new
|
|
266
|
+
Node objects on each field access call; start_point is the stable identity.
|
|
267
|
+
"""
|
|
268
|
+
parent = node.parent
|
|
269
|
+
if parent is not None and parent.type == "call_expression":
|
|
270
|
+
func_field = parent.child_by_field_name("function")
|
|
271
|
+
if func_field is not None and func_field.start_point == node.start_point:
|
|
272
|
+
return # Call position — do not emit field read
|
|
273
|
+
|
|
274
|
+
acc = _ts_classify_member(node, class_name, var_types)
|
|
275
|
+
if acc is not None:
|
|
276
|
+
target, _recv = acc
|
|
277
|
+
result.append((source_fn, target, "reads", node.start_point[0] + 1))
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _ts_collect_reads_recursive(
|
|
281
|
+
node: Node,
|
|
282
|
+
source_fn: str,
|
|
283
|
+
class_name: str | None,
|
|
284
|
+
var_types: dict[str, str],
|
|
285
|
+
result: list[FieldAccess],
|
|
286
|
+
) -> None:
|
|
287
|
+
"""Recursively collect reads from a TS/JS expression node.
|
|
288
|
+
|
|
289
|
+
Walks the expression tree and emits reads edges for member_expression nodes
|
|
290
|
+
that are NOT in call position. Skips nested scope-creating constructs.
|
|
291
|
+
"""
|
|
292
|
+
t = node.type
|
|
293
|
+
if t in (
|
|
294
|
+
"function_declaration", "function_expression", "arrow_function",
|
|
295
|
+
"method_definition", "class_declaration",
|
|
296
|
+
):
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
if t == "member_expression":
|
|
300
|
+
_ts_emit_read_if_not_in_call(node, source_fn, class_name, var_types, result)
|
|
301
|
+
# Do NOT recurse into member_expression children — the object part ('this')
|
|
302
|
+
# is not itself a field access target we want to re-emit.
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
for child in node.children:
|
|
306
|
+
_ts_collect_reads_recursive(child, source_fn, class_name, var_types, result)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _ts_classify_member(
|
|
310
|
+
node: Node,
|
|
311
|
+
class_name: str | None,
|
|
312
|
+
var_types: dict[str, str],
|
|
313
|
+
) -> tuple[str, str | None] | None:
|
|
314
|
+
"""Resolve a TS/JS member_expression node to (qualified_target, receiver_text).
|
|
315
|
+
|
|
316
|
+
Returns None when the node lacks the expected object/property shape.
|
|
317
|
+
Returns (target, receiver_text) where:
|
|
318
|
+
- target = 'Type.field' when receiver resolves (EXTRACTED confidence)
|
|
319
|
+
- target = bare 'field' when unresolvable (AMBIGUOUS confidence)
|
|
320
|
+
- receiver_text = raw receiver text (e.g. 'this', 'client')
|
|
321
|
+
|
|
322
|
+
Conservatism contract (mirrors resolve_receiver_type):
|
|
323
|
+
NEVER emit a wrong qualified target. Bare name kept on uncertain receiver.
|
|
324
|
+
|
|
325
|
+
TS grammar: member_expression has 'object' field and 'property' field.
|
|
326
|
+
The property is typically a property_identifier node.
|
|
327
|
+
|
|
328
|
+
WHY unwrap as_expression/parenthesized_expression:
|
|
329
|
+
`delete (this as any).cache` wraps `this` in a parenthesized as_expression.
|
|
330
|
+
The object field is `parenthesized_expression → as_expression → this`.
|
|
331
|
+
We unwrap one level to recover `this` and correctly classify the access.
|
|
332
|
+
|
|
333
|
+
Never raises (returns None on any exception).
|
|
334
|
+
"""
|
|
335
|
+
try:
|
|
336
|
+
obj_node = node.child_by_field_name("object")
|
|
337
|
+
prop_node = node.child_by_field_name("property")
|
|
338
|
+
if obj_node is None or prop_node is None:
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
# property_identifier is the standard field-name node in TS member expressions.
|
|
342
|
+
# Other property types (computed, string literal) are skipped conservatively.
|
|
343
|
+
if prop_node.type != "property_identifier":
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
field_name = _text(prop_node)
|
|
347
|
+
if not field_name:
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
# Unwrap type-assertion wrappers to recover the underlying receiver.
|
|
351
|
+
# Handles: (this as any).x → resolve as 'this'
|
|
352
|
+
# (this as SomeType).x → resolve as 'this'
|
|
353
|
+
effective_obj = _ts_unwrap_type_assertion(obj_node)
|
|
354
|
+
receiver_text = _text(effective_obj)
|
|
355
|
+
|
|
356
|
+
# Resolve receiver to a type using existing scope-inference.
|
|
357
|
+
resolved_type = resolve_receiver_type(
|
|
358
|
+
receiver_text, class_name, var_types, _TS_SELF_NAMES
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
if resolved_type is not None:
|
|
362
|
+
return f"{resolved_type}.{field_name}", receiver_text
|
|
363
|
+
else:
|
|
364
|
+
return field_name, receiver_text
|
|
365
|
+
|
|
366
|
+
except Exception as exc: # noqa: BLE001
|
|
367
|
+
logger.debug("_ts_classify_member: failed: %r", exc)
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _ts_unwrap_type_assertion(node: Node) -> Node:
|
|
372
|
+
"""Unwrap parenthesized_expression and as_expression wrappers one level deep.
|
|
373
|
+
|
|
374
|
+
Handles: (this as any) → as_expression → 'this' node
|
|
375
|
+
(this) → parenthesized_expression → 'this' node
|
|
376
|
+
|
|
377
|
+
WHY one level: deeper nesting is not common in practice, and the conservatism
|
|
378
|
+
contract requires refusing uncertain receivers rather than over-unwrapping.
|
|
379
|
+
A two-level unwrap like `((this as any) as T).field` is exotic enough that keeping
|
|
380
|
+
the outer wrapper (and thus resolving the bare field name) is safer than guessing.
|
|
381
|
+
Returns the original node if unwrapping does not yield a simpler node.
|
|
382
|
+
Never raises.
|
|
383
|
+
"""
|
|
384
|
+
try:
|
|
385
|
+
if node.type == "parenthesized_expression":
|
|
386
|
+
# Find the content inside the parentheses (skip '(' and ')' tokens).
|
|
387
|
+
for child in node.children:
|
|
388
|
+
if child.type not in ("(", ")"):
|
|
389
|
+
return _ts_unwrap_type_assertion(child)
|
|
390
|
+
if node.type in ("as_expression", "type_assertion"):
|
|
391
|
+
# as_expression: first named child is the value expression (before 'as').
|
|
392
|
+
for child in node.named_children:
|
|
393
|
+
# The first named child of an as_expression is the wrapped value.
|
|
394
|
+
return child
|
|
395
|
+
except Exception: # noqa: BLE001
|
|
396
|
+
pass
|
|
397
|
+
return node
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def collect_field_symbols_typescript(
|
|
401
|
+
class_node: Node,
|
|
402
|
+
class_name: str,
|
|
403
|
+
) -> list[tuple[str, int]]:
|
|
404
|
+
"""Collect (qualified_field_name, line) pairs from a TypeScript class_declaration.
|
|
405
|
+
|
|
406
|
+
Returns field symbols for:
|
|
407
|
+
1. public_field_definition nodes in the class body (typed or untyped field decls).
|
|
408
|
+
2. this.x = ... assignment sites in the constructor body (first occurrence only).
|
|
409
|
+
3. Constructor parameter properties: required_parameter with accessibility_modifier
|
|
410
|
+
(e.g. constructor(private x: Foo, public y: Bar)) — these become stored fields.
|
|
411
|
+
|
|
412
|
+
Dedup: by (class_name, field_name) — multiple occurrences of the same field name
|
|
413
|
+
produce ONE symbol. Class-body field declarations win over constructor assignments
|
|
414
|
+
(body is scanned first), which win over parameter properties.
|
|
415
|
+
|
|
416
|
+
Returns [] on any error. Never raises.
|
|
417
|
+
"""
|
|
418
|
+
try:
|
|
419
|
+
seen_fields: set[str] = set()
|
|
420
|
+
result: list[tuple[str, int]] = []
|
|
421
|
+
|
|
422
|
+
body = class_node.child_by_field_name("body")
|
|
423
|
+
if body is None:
|
|
424
|
+
return result
|
|
425
|
+
|
|
426
|
+
# Pass 1: class body field declarations (public_field_definition).
|
|
427
|
+
for child in body.children:
|
|
428
|
+
try:
|
|
429
|
+
_ts_try_collect_field_definition(child, class_name, seen_fields, result)
|
|
430
|
+
except Exception: # noqa: BLE001
|
|
431
|
+
pass
|
|
432
|
+
|
|
433
|
+
# Pass 2: constructor body this.x = ... assignments.
|
|
434
|
+
for child in body.children:
|
|
435
|
+
try:
|
|
436
|
+
ctor = _ts_get_constructor(child)
|
|
437
|
+
if ctor is None:
|
|
438
|
+
continue
|
|
439
|
+
_ts_collect_constructor_assignments(ctor, class_name, seen_fields, result)
|
|
440
|
+
except Exception: # noqa: BLE001
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
# Pass 3: constructor parameter properties (private/public/protected params).
|
|
444
|
+
for child in body.children:
|
|
445
|
+
try:
|
|
446
|
+
ctor = _ts_get_constructor(child)
|
|
447
|
+
if ctor is None:
|
|
448
|
+
continue
|
|
449
|
+
_ts_collect_param_properties(ctor, class_name, seen_fields, result)
|
|
450
|
+
except Exception: # noqa: BLE001
|
|
451
|
+
pass
|
|
452
|
+
|
|
453
|
+
return result
|
|
454
|
+
|
|
455
|
+
except Exception as exc: # noqa: BLE001
|
|
456
|
+
logger.debug("collect_field_symbols_typescript: failed for class %r: %r", class_name, exc)
|
|
457
|
+
return []
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _ts_try_collect_field_definition(
|
|
461
|
+
node: Node,
|
|
462
|
+
class_name: str,
|
|
463
|
+
seen: set[str],
|
|
464
|
+
result: list[tuple[str, int]],
|
|
465
|
+
) -> None:
|
|
466
|
+
"""Try to extract a field symbol from a public_field_definition node.
|
|
467
|
+
|
|
468
|
+
TS grammar: public_field_definition has a 'name' field (property_identifier)
|
|
469
|
+
and an optional 'type' field (type_annotation). We index ALL field definitions
|
|
470
|
+
regardless of type (unlike holds, which filters builtins) — 'balance: number'
|
|
471
|
+
is a valid field even though 'number' is a builtin type.
|
|
472
|
+
"""
|
|
473
|
+
if node.type not in ("public_field_definition", "field_definition"):
|
|
474
|
+
return
|
|
475
|
+
name_node = node.child_by_field_name("name")
|
|
476
|
+
if name_node is None:
|
|
477
|
+
return
|
|
478
|
+
field_name = _text(name_node)
|
|
479
|
+
if not field_name:
|
|
480
|
+
return
|
|
481
|
+
qualified = f"{class_name}.{field_name}"
|
|
482
|
+
if qualified not in seen:
|
|
483
|
+
seen.add(qualified)
|
|
484
|
+
result.append((qualified, node.start_point[0] + 1))
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _ts_collect_constructor_assignments(
|
|
488
|
+
ctor_node: Node,
|
|
489
|
+
class_name: str,
|
|
490
|
+
seen: set[str],
|
|
491
|
+
result: list[tuple[str, int]],
|
|
492
|
+
) -> None:
|
|
493
|
+
"""Scan constructor body for this.x = ... assignments and collect as field symbols.
|
|
494
|
+
|
|
495
|
+
Only the FIRST occurrence of each (class_name, field_name) pair is emitted.
|
|
496
|
+
Uses a shallow walk of the constructor's statement_block.
|
|
497
|
+
"""
|
|
498
|
+
body = ctor_node.child_by_field_name("body")
|
|
499
|
+
if body is None:
|
|
500
|
+
return
|
|
501
|
+
for stmt in body.children:
|
|
502
|
+
_ts_try_collect_ctor_stmt(stmt, class_name, seen, result)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _ts_try_collect_ctor_stmt(
|
|
506
|
+
stmt: Node,
|
|
507
|
+
class_name: str,
|
|
508
|
+
seen: set[str],
|
|
509
|
+
result: list[tuple[str, int]],
|
|
510
|
+
) -> None:
|
|
511
|
+
"""Try to extract a this.x = ... field assignment from a constructor statement.
|
|
512
|
+
|
|
513
|
+
Handles expression_statement wrapping an assignment_expression:
|
|
514
|
+
expression_statement → assignment_expression { left: member_expression, right: ... }
|
|
515
|
+
where left member_expression has object='this' and property=field_name.
|
|
516
|
+
"""
|
|
517
|
+
# Unwrap expression_statement wrapper.
|
|
518
|
+
node = stmt
|
|
519
|
+
if stmt.type == "expression_statement" and stmt.children:
|
|
520
|
+
node = stmt.children[0]
|
|
521
|
+
|
|
522
|
+
if node.type not in ("assignment_expression", "augmented_assignment_expression"):
|
|
523
|
+
return
|
|
524
|
+
|
|
525
|
+
left = node.child_by_field_name("left")
|
|
526
|
+
if left is None or left.type != "member_expression":
|
|
527
|
+
return
|
|
528
|
+
|
|
529
|
+
obj_node = left.child_by_field_name("object")
|
|
530
|
+
prop_node = left.child_by_field_name("property")
|
|
531
|
+
if obj_node is None or prop_node is None:
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
receiver = _text(obj_node)
|
|
535
|
+
if receiver not in _TS_SELF_NAMES:
|
|
536
|
+
return # Only this.x = ... patterns
|
|
537
|
+
|
|
538
|
+
if prop_node.type != "property_identifier":
|
|
539
|
+
return
|
|
540
|
+
|
|
541
|
+
field_name = _text(prop_node)
|
|
542
|
+
if not field_name:
|
|
543
|
+
return
|
|
544
|
+
|
|
545
|
+
qualified = f"{class_name}.{field_name}"
|
|
546
|
+
if qualified not in seen:
|
|
547
|
+
seen.add(qualified)
|
|
548
|
+
result.append((qualified, stmt.start_point[0] + 1))
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _ts_collect_param_properties(
|
|
552
|
+
ctor_node: Node,
|
|
553
|
+
class_name: str,
|
|
554
|
+
seen: set[str],
|
|
555
|
+
result: list[tuple[str, int]],
|
|
556
|
+
) -> None:
|
|
557
|
+
"""Collect field symbols from constructor parameter properties.
|
|
558
|
+
|
|
559
|
+
A parameter property is a required_parameter with an accessibility_modifier
|
|
560
|
+
(private, public, protected) — tree-sitter emits it as a required_parameter
|
|
561
|
+
whose first child is an accessibility_modifier node, followed by an identifier
|
|
562
|
+
and an optional type_annotation.
|
|
563
|
+
|
|
564
|
+
These parameters implicitly define stored fields on the class, so they should
|
|
565
|
+
become field symbols regardless of whether they have a type annotation.
|
|
566
|
+
"""
|
|
567
|
+
params = ctor_node.child_by_field_name("parameters")
|
|
568
|
+
if params is None:
|
|
569
|
+
return
|
|
570
|
+
for param in params.children:
|
|
571
|
+
_ts_try_collect_param_property(param, class_name, seen, result)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _ts_try_collect_param_property(
|
|
575
|
+
param: Node,
|
|
576
|
+
class_name: str,
|
|
577
|
+
seen: set[str],
|
|
578
|
+
result: list[tuple[str, int]],
|
|
579
|
+
) -> None:
|
|
580
|
+
"""Try to collect a field symbol from a constructor parameter property node.
|
|
581
|
+
|
|
582
|
+
A parameter property is identified by having an accessibility_modifier child
|
|
583
|
+
(private/public/protected/readonly) as one of its children. The identifier
|
|
584
|
+
child following the modifier is the field name.
|
|
585
|
+
"""
|
|
586
|
+
if param.type not in ("required_parameter", "optional_parameter"):
|
|
587
|
+
return
|
|
588
|
+
|
|
589
|
+
# Check for accessibility_modifier child (marks this as a parameter property).
|
|
590
|
+
has_modifier = any(c.type == "accessibility_modifier" for c in param.children)
|
|
591
|
+
if not has_modifier:
|
|
592
|
+
return
|
|
593
|
+
|
|
594
|
+
# The identifier child is the field name.
|
|
595
|
+
name_node = param.child_by_field_name("pattern")
|
|
596
|
+
if name_node is None:
|
|
597
|
+
# Fallback: find identifier child directly (grammar varies).
|
|
598
|
+
for child in param.children:
|
|
599
|
+
if child.type == "identifier":
|
|
600
|
+
name_node = child
|
|
601
|
+
break
|
|
602
|
+
if name_node is None:
|
|
603
|
+
return
|
|
604
|
+
|
|
605
|
+
field_name = _text(name_node)
|
|
606
|
+
if not field_name:
|
|
607
|
+
return
|
|
608
|
+
|
|
609
|
+
qualified = f"{class_name}.{field_name}"
|
|
610
|
+
if qualified not in seen:
|
|
611
|
+
seen.add(qualified)
|
|
612
|
+
result.append((qualified, param.start_point[0] + 1))
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _ts_get_constructor(node: Node) -> Node | None:
|
|
616
|
+
"""Return the method_definition node if it is the TypeScript constructor.
|
|
617
|
+
|
|
618
|
+
TS grammar: constructor is a method_definition whose 'name' property_identifier
|
|
619
|
+
is 'constructor'.
|
|
620
|
+
"""
|
|
621
|
+
if node.type != "method_definition":
|
|
622
|
+
return None
|
|
623
|
+
name_node = node.child_by_field_name("name")
|
|
624
|
+
if name_node is not None and _text(name_node) == "constructor":
|
|
625
|
+
return node
|
|
626
|
+
return None
|