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.
Files changed (109) hide show
  1. seam/__init__.py +3 -0
  2. seam/_data/schema.sql +225 -0
  3. seam/_web/assets/index-BL_tqprR.js +216 -0
  4. seam/_web/assets/index-GTKUhVyD.css +1 -0
  5. seam/_web/index.html +13 -0
  6. seam/analysis/__init__.py +14 -0
  7. seam/analysis/affected.py +254 -0
  8. seam/analysis/builtins.py +966 -0
  9. seam/analysis/byte_budget.py +217 -0
  10. seam/analysis/changes.py +709 -0
  11. seam/analysis/cluster_naming.py +260 -0
  12. seam/analysis/clustering.py +216 -0
  13. seam/analysis/confidence.py +699 -0
  14. seam/analysis/embeddings.py +195 -0
  15. seam/analysis/flows.py +708 -0
  16. seam/analysis/impact.py +444 -0
  17. seam/analysis/imports.py +994 -0
  18. seam/analysis/imports_ext.py +780 -0
  19. seam/analysis/imports_resolve.py +176 -0
  20. seam/analysis/processes.py +453 -0
  21. seam/analysis/relevance.py +155 -0
  22. seam/analysis/rwr.py +129 -0
  23. seam/analysis/staleness.py +328 -0
  24. seam/analysis/steer.py +282 -0
  25. seam/analysis/synthesis.py +253 -0
  26. seam/analysis/synthesis_channels.py +433 -0
  27. seam/analysis/testpaths.py +103 -0
  28. seam/analysis/traversal.py +470 -0
  29. seam/cli/__init__.py +0 -0
  30. seam/cli/install.py +232 -0
  31. seam/cli/main.py +2602 -0
  32. seam/cli/output.py +137 -0
  33. seam/cli/read.py +244 -0
  34. seam/cli/serve.py +145 -0
  35. seam/config.py +551 -0
  36. seam/indexer/__init__.py +0 -0
  37. seam/indexer/cluster_index.py +425 -0
  38. seam/indexer/db.py +496 -0
  39. seam/indexer/embedding_index.py +183 -0
  40. seam/indexer/field_access.py +536 -0
  41. seam/indexer/field_access_c_cpp.py +643 -0
  42. seam/indexer/field_access_ext.py +708 -0
  43. seam/indexer/field_access_ext2.py +408 -0
  44. seam/indexer/field_access_go_rust.py +737 -0
  45. seam/indexer/field_access_php_swift.py +888 -0
  46. seam/indexer/field_access_ts.py +626 -0
  47. seam/indexer/graph.py +321 -0
  48. seam/indexer/graph_c.py +562 -0
  49. seam/indexer/graph_c_cpp.py +39 -0
  50. seam/indexer/graph_common.py +644 -0
  51. seam/indexer/graph_cpp.py +615 -0
  52. seam/indexer/graph_csharp.py +651 -0
  53. seam/indexer/graph_go.py +723 -0
  54. seam/indexer/graph_go_rust.py +39 -0
  55. seam/indexer/graph_java.py +689 -0
  56. seam/indexer/graph_java_csharp.py +38 -0
  57. seam/indexer/graph_php.py +914 -0
  58. seam/indexer/graph_python.py +628 -0
  59. seam/indexer/graph_ruby.py +748 -0
  60. seam/indexer/graph_rust.py +653 -0
  61. seam/indexer/graph_scope_infer.py +902 -0
  62. seam/indexer/graph_scope_infer_ext.py +723 -0
  63. seam/indexer/graph_scope_infer_ext2.py +992 -0
  64. seam/indexer/graph_swift.py +1014 -0
  65. seam/indexer/graph_swift_infer.py +515 -0
  66. seam/indexer/graph_typescript.py +663 -0
  67. seam/indexer/migrations.py +816 -0
  68. seam/indexer/parser.py +204 -0
  69. seam/indexer/pipeline.py +197 -0
  70. seam/indexer/signatures.py +634 -0
  71. seam/indexer/signatures_ext.py +780 -0
  72. seam/indexer/sync.py +287 -0
  73. seam/indexer/synthesis_index.py +291 -0
  74. seam/indexer/tokenize.py +79 -0
  75. seam/installer/__init__.py +67 -0
  76. seam/installer/claude.py +97 -0
  77. seam/installer/codex.py +94 -0
  78. seam/installer/core.py +127 -0
  79. seam/installer/cursor.py +61 -0
  80. seam/installer/guide.py +110 -0
  81. seam/installer/jsonfile.py +85 -0
  82. seam/installer/markdownfile.py +146 -0
  83. seam/installer/tomlfile.py +72 -0
  84. seam/query/__init__.py +0 -0
  85. seam/query/clusters.py +206 -0
  86. seam/query/comments.py +217 -0
  87. seam/query/context.py +293 -0
  88. seam/query/engine.py +940 -0
  89. seam/query/fts.py +328 -0
  90. seam/query/names.py +470 -0
  91. seam/query/pack.py +433 -0
  92. seam/query/semantic.py +339 -0
  93. seam/query/structure.py +727 -0
  94. seam/server/__init__.py +0 -0
  95. seam/server/graph_api.py +437 -0
  96. seam/server/handler_common.py +323 -0
  97. seam/server/impact_handler.py +615 -0
  98. seam/server/mcp.py +556 -0
  99. seam/server/tools.py +697 -0
  100. seam/server/trace_handler.py +184 -0
  101. seam/server/web.py +922 -0
  102. seam/watcher/__init__.py +0 -0
  103. seam/watcher/__main__.py +56 -0
  104. seam/watcher/daemon.py +237 -0
  105. seam_code-0.3.0.dist-info/METADATA +318 -0
  106. seam_code-0.3.0.dist-info/RECORD +109 -0
  107. seam_code-0.3.0.dist-info/WHEEL +4 -0
  108. seam_code-0.3.0.dist-info/entry_points.txt +2 -0
  109. 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