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,723 @@
1
+ """Scope-inference extension module — shared resolver + Go and Rust language families.
2
+
3
+ LAYER: leaf — imports from graph_common (leaf) and stdlib only. Never imports from
4
+ graph.py, graph_scope_infer, or any other seam module with side effects.
5
+
6
+ LAYERING:
7
+ graph_common (leaf — no seam deps)
8
+
9
+ graph_scope_infer_ext (this file — shared resolver + Go/Rust type-binding helpers)
10
+
11
+ graph_go_rust.py (_extract_edges_go / _extract_edges_rust use these helpers)
12
+ graph_scope_infer_ext2 (Java/C#/C++/Ruby/PHP type-binding helpers — same leaf layer)
13
+
14
+ WHY a split from graph_scope_infer.py (B4, Python/TS/JS) and from graph_scope_infer_ext2.py:
15
+ graph_scope_infer.py covers Python + TypeScript/JS (B4). Adding 7 more language
16
+ families inline would push it past 1000 lines. Go/Rust live here (with shared resolver);
17
+ Java/C#/C++/Ruby/PHP live in graph_scope_infer_ext2.py — keeping both files under 1000
18
+ lines and following the Phase 9 split precedent.
19
+
20
+ CONSERVATISM CONTRACT (identical to graph_swift_infer + graph_scope_infer):
21
+ NEVER emit a wrong edge. resolve_receiver_type_ext returns None on ANY uncertainty.
22
+ Self/this/Self normalization → enclosing class name (or None if unknown).
23
+
24
+ All public functions never raise (backstop try/except in each body).
25
+ """
26
+
27
+ import logging
28
+
29
+ from tree_sitter import Node
30
+
31
+ from seam.analysis.builtins import is_builtin
32
+ from seam.indexer.graph_common import _text
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ # ── Self/this aliases per language ────────────────────────────────────────────
37
+
38
+ # Go: no universal self; method receivers vary by programmer convention. We do NOT infer
39
+ # via receiver parameter name — receiver typing comes from param annotation (param/local scope).
40
+ # (No _SELF set for Go.)
41
+
42
+ # Rust: 'self' and 'Self' both refer to the enclosing impl type.
43
+ _RUST_SELF_NAMES: frozenset[str] = frozenset({"self", "Self"})
44
+
45
+
46
+ # ── Shared: resolve receiver text to a declared type name ─────────────────────
47
+
48
+
49
+ def resolve_receiver_type_ext(
50
+ receiver_text: str,
51
+ class_name: str | None,
52
+ var_types: dict[str, str],
53
+ self_names: frozenset[str],
54
+ ) -> str | None:
55
+ """Resolve a receiver expression to its declared type name.
56
+
57
+ Identical contract to resolve_receiver_type in graph_scope_infer — just called
58
+ from different extractors. Extracted into this module to avoid importing
59
+ graph_scope_infer from the family extractors (would create an indirect cycle
60
+ through the caller chain).
61
+
62
+ Args:
63
+ receiver_text: Raw receiver text captured from the AST (e.g. 'self', 'client',
64
+ 'this', '$this', 'self.field').
65
+ class_name: Name of the enclosing class (or None for module-level functions).
66
+ var_types: scope map — name → type for all in-scope bindings (class fields
67
+ merged with per-function params and locals).
68
+ self_names: Set of receiver strings that alias 'self' (e.g. {'self'} for
69
+ Ruby, {'this'} for Java/C#/C++, {'self','Self'} for Rust).
70
+
71
+ Returns:
72
+ The resolved type name string (e.g. 'Client'), or None when the type cannot
73
+ be determined with confidence. None → caller keeps the bare target.
74
+ """
75
+ try:
76
+ if not receiver_text:
77
+ return None
78
+
79
+ # Multi-part receiver: only handle 'self.field' / 'this.field' patterns.
80
+ # a.b.method() where 'a' is not a self-alias → refuse.
81
+ if "." in receiver_text:
82
+ return _resolve_chained_ext(receiver_text, class_name, var_types, self_names)
83
+
84
+ # Plain identifier — check self/this alias first.
85
+ if receiver_text in self_names:
86
+ return class_name # May be None (top-level scope)
87
+
88
+ # Look up in the scope map (class fields + params + locals).
89
+ return var_types.get(receiver_text) # None if not in scope
90
+
91
+ except Exception as exc: # noqa: BLE001
92
+ logger.debug("resolve_receiver_type_ext: failed for %r: %r", receiver_text, exc)
93
+ return None
94
+
95
+
96
+ def _resolve_chained_ext(
97
+ receiver_text: str,
98
+ class_name: str | None,
99
+ var_types: dict[str, str],
100
+ self_names: frozenset[str],
101
+ ) -> str | None:
102
+ """Resolve a dotted receiver like 'self.field' or 'this.repo'.
103
+
104
+ Only ONE level of chaining is supported:
105
+ self.field / this.field → look up field's type from var_types
106
+ Everything else → None (refuse — would need cross-class field typing).
107
+ """
108
+ parts = receiver_text.split(".", 1)
109
+ if len(parts) != 2:
110
+ return None
111
+ head, field = parts
112
+ if head not in self_names:
113
+ # Foreign chain (a.b, not self.b) → refuse.
114
+ return None
115
+ return var_types.get(field)
116
+
117
+
118
+ # ── Type-name extraction helpers ───────────────────────────────────────────────
119
+
120
+
121
+ def _strip_ref_wrapper(type_text: str) -> str | None:
122
+ """Strip Go/Rust/C++ reference/pointer wrappers to extract the plain type name.
123
+
124
+ Conservative: only strips a SINGLE leading pointer/reference indicator:
125
+ *Client → 'Client' (Go pointer receiver, C++ pointer)
126
+ &Client → 'Client' (Rust reference, C++ reference)
127
+ **Client → None (double pointer — refuse; too complex)
128
+ *[]Client → None (pointer to slice — refuse)
129
+ Vec<Client>→ None (generic — refuse; may carry multiple types)
130
+
131
+ WHY refuse generics and double-pointers: the conservatism contract requires that we
132
+ only bind types we can be CERTAIN about. A double-pointer `**Foo` is an unusual
133
+ pattern where the variable holds a pointer-to-pointer — the actual type is unclear.
134
+ A generic like `Vec<Client>` has multiple possible element types; binding the outer
135
+ container to 'Vec' would produce wrong edges. Refusing preserves correctness over
136
+ coverage.
137
+
138
+ Returns the stripped name, or None if the shape is not a plain single-ref.
139
+ """
140
+ if not type_text:
141
+ return None
142
+ # Refuse generics (contain '<' or '[')
143
+ if "<" in type_text or "[" in type_text:
144
+ return None
145
+ # Strip single leading * or &
146
+ stripped = type_text.lstrip("*&").strip()
147
+ # After stripping, must not contain further * or &
148
+ if "*" in stripped or "&" in stripped:
149
+ return None
150
+ return stripped if stripped else None
151
+
152
+
153
+ # ── Go: parameter and local-variable type binding ─────────────────────────────
154
+
155
+
156
+ def record_go_param_types(func_node: Node, var_types: dict[str, str]) -> None:
157
+ """Bind Go function/method parameter names → declared type into var_types.
158
+
159
+ Go parameter shapes (inside parameter_list):
160
+ parameter_declaration — one or more 'name' identifiers + 'type' field
161
+ func f(client *Client) → binds client → Client
162
+ func f(a, b string) → binds both a → string (builtin → skip)
163
+
164
+ Conservative: strips single * or & pointer/reference prefix from type name.
165
+ Refuses generics (Vec<T>, map[K]V) — these are container types, not plain user types.
166
+ Never raises.
167
+ """
168
+ try:
169
+ params = func_node.child_by_field_name("parameters")
170
+ if params is None:
171
+ return
172
+ for child in params.named_children:
173
+ if child.type == "parameter_declaration":
174
+ _record_go_single_param(child, var_types)
175
+ except Exception as exc: # noqa: BLE001
176
+ logger.debug("record_go_param_types: failed: %r", exc)
177
+
178
+
179
+ def _record_go_single_param(param_node: Node, var_types: dict[str, str]) -> None:
180
+ """Record a single Go parameter_declaration's name → type binding.
181
+
182
+ A parameter_declaration has:
183
+ - One or more 'name' identifiers (parameter names)
184
+ - A 'type' field (the declared type node)
185
+ """
186
+ try:
187
+ type_node = param_node.child_by_field_name("type")
188
+ if type_node is None:
189
+ return
190
+ type_text = _text(type_node).strip()
191
+ type_name = _strip_ref_wrapper(type_text)
192
+ if not type_name or not type_name[0].isupper():
193
+ return # conservative: only bind PascalCase types (user types)
194
+ # Collect all identifier children as parameter names.
195
+ for child in param_node.named_children:
196
+ if child.type == "identifier":
197
+ name = _text(child).strip()
198
+ if name:
199
+ var_types[name] = type_name
200
+ except Exception as exc: # noqa: BLE001
201
+ logger.debug("_record_go_single_param: failed at %r: %r", param_node.start_point, exc)
202
+
203
+
204
+ def record_go_local_types(stmt_node: Node, var_types: dict[str, str]) -> None:
205
+ """Record type bindings from a Go local statement.
206
+
207
+ Handles two Go declaration patterns:
208
+ short_var_declaration: p := &Parser{} → p → Parser (composite literal)
209
+ var_declaration: var p *Parser → p → Parser
210
+ assignment_statement: p = &Parser{} → p → Parser (less conservative)
211
+
212
+ Conservative: only composite literals `&Type{}` or `Type{}` (Go struct literals)
213
+ bind without an explicit type annotation. Pointer receivers are stripped.
214
+ Never raises.
215
+ """
216
+ try:
217
+ ntype = stmt_node.type
218
+ if ntype == "short_var_declaration":
219
+ _record_go_short_var(stmt_node, var_types)
220
+ elif ntype in ("var_declaration", "var_spec"):
221
+ _record_go_var_decl(stmt_node, var_types)
222
+ except Exception as exc: # noqa: BLE001
223
+ logger.debug("record_go_local_types: failed: %r", exc)
224
+
225
+
226
+ def _record_go_short_var(node: Node, var_types: dict[str, str]) -> None:
227
+ """Record binding from Go short_var_declaration: name := value.
228
+
229
+ Handles `p := &Parser{}` → p → Parser (composite literal).
230
+ Conservative: only composite_literal_value (Type{}) or unary_expression (&Type{}).
231
+ """
232
+ try:
233
+ # short_var_declaration: left, ':=', right
234
+ left_node = node.child_by_field_name("left")
235
+ right_node = node.child_by_field_name("right")
236
+ if left_node is None or right_node is None:
237
+ return
238
+ # Left side: expression_list containing identifiers
239
+ names: list[str] = []
240
+ for child in left_node.children:
241
+ if child.type == "identifier":
242
+ names.append(_text(child).strip())
243
+ if not names:
244
+ return
245
+ # Right side: expression_list; take first expression
246
+ values = [c for c in right_node.children if c.type not in (",",)]
247
+ if not values:
248
+ return
249
+ value_node = values[0]
250
+ cls = _go_constructor_class(value_node)
251
+ if cls and names:
252
+ var_types[names[0]] = cls
253
+ except Exception as exc: # noqa: BLE001
254
+ logger.debug("_record_go_short_var: failed: %r", exc)
255
+
256
+
257
+ def _record_go_var_decl(node: Node, var_types: dict[str, str]) -> None:
258
+ """Record binding from Go var_declaration or var_spec: var name Type.
259
+
260
+ Handles:
261
+ var client *Client → client → Client
262
+ var client Client → client → Client
263
+ """
264
+ try:
265
+ # var_declaration has var_spec children; var_spec has name + type fields
266
+ if node.type == "var_declaration":
267
+ for child in node.named_children:
268
+ if child.type == "var_spec":
269
+ _record_go_var_spec(child, var_types)
270
+ elif node.type == "var_spec":
271
+ _record_go_var_spec(node, var_types)
272
+ except Exception as exc: # noqa: BLE001
273
+ logger.debug("_record_go_var_decl: failed: %r", exc)
274
+
275
+
276
+ def _record_go_var_spec(node: Node, var_types: dict[str, str]) -> None:
277
+ """Record one Go var_spec (name + type) binding."""
278
+ try:
279
+ type_node = node.child_by_field_name("type")
280
+ if type_node is None:
281
+ return
282
+ type_text = _text(type_node).strip()
283
+ type_name = _strip_ref_wrapper(type_text)
284
+ if not type_name or not type_name[0].isupper():
285
+ return
286
+ # Name field: identifier or expression_list
287
+ name_node = node.child_by_field_name("name")
288
+ if name_node is not None and name_node.type == "identifier":
289
+ name = _text(name_node).strip()
290
+ if name:
291
+ var_types[name] = type_name
292
+ except Exception as exc: # noqa: BLE001
293
+ logger.debug("_record_go_var_spec: failed: %r", exc)
294
+
295
+
296
+ def _go_constructor_class(value_node: Node) -> str | None:
297
+ """If value_node is a Go `Type{}` or `&Type{}` literal, return 'Type', else None.
298
+
299
+ Handles:
300
+ composite_literal (Parser{...}) → 'Parser'
301
+ unary_expression (&Parser{...}) → 'Parser' (pointer to composite literal)
302
+ call_expression (Parser.New()) → None (function call, not literal)
303
+ """
304
+ try:
305
+ if value_node.type == "composite_literal":
306
+ # composite_literal → type field (or first child)
307
+ type_node = value_node.child_by_field_name("type")
308
+ if type_node is None and value_node.children:
309
+ type_node = value_node.children[0]
310
+ if type_node is not None:
311
+ type_text = _text(type_node).strip()
312
+ name = _strip_ref_wrapper(type_text) or type_text
313
+ if name and name[0].isupper():
314
+ return name
315
+ elif value_node.type == "unary_expression":
316
+ # &Type{} → operand is composite_literal
317
+ operand = value_node.child_by_field_name("operand")
318
+ if operand is not None:
319
+ return _go_constructor_class(operand)
320
+ elif value_node.type == "call_expression":
321
+ # Type.New() or Type{} as a call — extract function (may be selector)
322
+ func = value_node.child_by_field_name("function")
323
+ if func is not None and func.type == "identifier":
324
+ name = _text(func).strip()
325
+ if name and name[0].isupper():
326
+ return name
327
+ except Exception: # noqa: BLE001
328
+ pass
329
+ return None
330
+
331
+
332
+ def scan_class_fields_go(struct_node: Node) -> dict[str, str]:
333
+ """Pre-scan a Go struct_type body for field-level type bindings.
334
+
335
+ WHY pre-scan: methods may reference a struct field before its declaration in source
336
+ order. Go structs appear as type_spec with a struct_type child.
337
+
338
+ Captures:
339
+ FieldName *Client → field → Client (pointer field)
340
+ FieldName Client → field → Client (value field)
341
+
342
+ Returns name → type for direct struct fields. Never raises.
343
+ """
344
+ out: dict[str, str] = {}
345
+ try:
346
+ # struct_type has a field_declaration_list child
347
+ for child in struct_node.children:
348
+ if child.type == "field_declaration_list":
349
+ for field in child.named_children:
350
+ if field.type == "field_declaration":
351
+ _record_go_field(field, out)
352
+ break
353
+ except Exception as exc: # noqa: BLE001
354
+ logger.debug("scan_class_fields_go: failed: %r", exc)
355
+ return out
356
+
357
+
358
+ def _record_go_field(field_node: Node, out: dict[str, str]) -> None:
359
+ """Record a single Go field_declaration's name → type binding."""
360
+ try:
361
+ type_node = field_node.child_by_field_name("type")
362
+ if type_node is None:
363
+ return
364
+ type_text = _text(type_node).strip()
365
+ type_name = _strip_ref_wrapper(type_text) or type_text
366
+ if not type_name or not type_name[0].isupper():
367
+ return
368
+ # 'name' field may be an identifier or a field_identifier_list
369
+ name_node = field_node.child_by_field_name("name")
370
+ if name_node is not None:
371
+ if name_node.type == "identifier":
372
+ name = _text(name_node).strip()
373
+ if name:
374
+ out[name] = type_name
375
+ except Exception as exc: # noqa: BLE001
376
+ logger.debug("_record_go_field: failed: %r", exc)
377
+
378
+
379
+ # ── Rust: parameter and local-variable type binding ───────────────────────────
380
+
381
+
382
+ def record_rust_param_types(func_node: Node, var_types: dict[str, str]) -> None:
383
+ """Bind Rust function parameter names → declared type into var_types.
384
+
385
+ Rust parameter shapes (inside parameters):
386
+ &self, &mut self, self → handled by _RUST_SELF_NAMES (caller uses them directly)
387
+ name: &Client → binds name → Client (reference stripped)
388
+ name: Client → binds name → Client (plain type)
389
+ name: Option<Client> → refused (generic)
390
+
391
+ Conservative: only plain type_identifier and reference-wrapped type_identifier bind.
392
+ Never raises.
393
+ """
394
+ try:
395
+ params = func_node.child_by_field_name("parameters")
396
+ if params is None:
397
+ return
398
+ for child in params.named_children:
399
+ if child.type == "parameter":
400
+ _record_rust_single_param(child, var_types)
401
+ except Exception as exc: # noqa: BLE001
402
+ logger.debug("record_rust_param_types: failed: %r", exc)
403
+
404
+
405
+ def _record_rust_single_param(param_node: Node, var_types: dict[str, str]) -> None:
406
+ """Record a single Rust parameter's name → type binding.
407
+
408
+ Rust parameter shapes:
409
+ pattern: type (pattern is 'identifier', type is 'type_identifier' or 'reference_type')
410
+ """
411
+ try:
412
+ pattern = param_node.child_by_field_name("pattern")
413
+ type_node = param_node.child_by_field_name("type")
414
+ if pattern is None or type_node is None:
415
+ return
416
+ if pattern.type not in ("identifier", "mutable_specifier"):
417
+ return
418
+ # Get the parameter name
419
+ if pattern.type == "mutable_specifier":
420
+ # mut name: &Type — name is the next sibling
421
+ name_node = next(
422
+ (c for c in param_node.named_children if c.type == "identifier"), None
423
+ )
424
+ if name_node is None:
425
+ return
426
+ name = _text(name_node).strip()
427
+ else:
428
+ name = _text(pattern).strip()
429
+ if not name or name in _RUST_SELF_NAMES:
430
+ return
431
+ type_name = _rust_plain_type(type_node)
432
+ if type_name:
433
+ var_types[name] = type_name
434
+ except Exception as exc: # noqa: BLE001
435
+ logger.debug("_record_rust_single_param: failed: %r", exc)
436
+
437
+
438
+ def _rust_plain_type(type_node: Node) -> str | None:
439
+ """Extract a plain user type name from a Rust type node.
440
+
441
+ Conservative: only accepts:
442
+ type_identifier → bare name (e.g. Client)
443
+ reference_type → &T or &mut T (strip ref, take T if type_identifier)
444
+ mutable_specifier + type_identifier (inside reference_type)
445
+
446
+ Refuses generics (generic_type: Vec<T>), arrays (array_type), tuples, etc.
447
+ """
448
+ if type_node is None:
449
+ return None
450
+ t = type_node.type
451
+ if t == "type_identifier":
452
+ name = _text(type_node).strip()
453
+ return name if (name and name[0].isupper()) else None
454
+ if t == "reference_type":
455
+ # &T or &mut T — find the inner type_identifier
456
+ for child in type_node.named_children:
457
+ if child.type == "type_identifier":
458
+ name = _text(child).strip()
459
+ return name if (name and name[0].isupper()) else None
460
+ if child.type == "mutable_specifier":
461
+ continue # skip 'mut'
462
+ return None
463
+
464
+
465
+ def record_rust_local_types(stmt_node: Node, var_types: dict[str, str]) -> None:
466
+ """Record type bindings from a Rust local statement.
467
+
468
+ Handles:
469
+ let_declaration: let name: &Type = ... → name → Type (annotation)
470
+ let name = Type::new() → name → Type (constructor call)
471
+ let name = Type { ... } → name → Type (struct literal)
472
+
473
+ Never raises.
474
+ """
475
+ try:
476
+ if stmt_node.type != "let_declaration":
477
+ return
478
+ pattern = stmt_node.child_by_field_name("pattern")
479
+ type_node = stmt_node.child_by_field_name("type")
480
+ value_node = stmt_node.child_by_field_name("value")
481
+
482
+ if pattern is None or pattern.type != "identifier":
483
+ return
484
+ name = _text(pattern).strip()
485
+ if not name:
486
+ return
487
+
488
+ # Annotation wins (more reliable than inference from value)
489
+ if type_node is not None:
490
+ type_name = _rust_plain_type(type_node)
491
+ if type_name:
492
+ var_types[name] = type_name
493
+ return
494
+
495
+ # Fall back to constructor/struct-literal inference
496
+ if value_node is not None:
497
+ cls = _rust_constructor_class(value_node)
498
+ if cls:
499
+ var_types[name] = cls
500
+ except Exception as exc: # noqa: BLE001
501
+ logger.debug("record_rust_local_types: failed: %r", exc)
502
+
503
+
504
+ def _rust_constructor_class(value_node: Node) -> str | None:
505
+ """If value_node is Type::new() or Type { ... }, return 'Type', else None.
506
+
507
+ Handles:
508
+ call_expression with scoped_identifier Type::new → 'Type'
509
+ (path field of scoped_identifier is 'identifier' in tree-sitter-rust 0.24.x)
510
+ struct_expression (Type { ... }) → 'Type'
511
+ """
512
+ try:
513
+ if value_node.type == "call_expression":
514
+ func = value_node.child_by_field_name("function")
515
+ if func is not None and func.type == "scoped_identifier":
516
+ # Type::new — the 'path' field is an identifier (the type name),
517
+ # and 'name' field is the method (e.g. 'new').
518
+ # In tree-sitter-rust the path can be 'identifier' or 'type_identifier'.
519
+ path = func.child_by_field_name("path")
520
+ if path is not None and path.type in ("identifier", "type_identifier"):
521
+ name = _text(path).strip()
522
+ return name if (name and name[0].isupper()) else None
523
+ elif value_node.type == "struct_expression":
524
+ # Type { field: val, ... } — name is the first type_identifier child
525
+ for child in value_node.named_children:
526
+ if child.type in ("type_identifier", "identifier"):
527
+ name = _text(child).strip()
528
+ return name if (name and name[0].isupper()) else None
529
+ except Exception: # noqa: BLE001
530
+ pass
531
+ return None
532
+
533
+
534
+ def scan_class_fields_rust(impl_node: Node) -> dict[str, str]:
535
+ """Pre-scan a Rust struct_item body for field-level type bindings.
536
+
537
+ In Rust, struct fields live in the struct_item, not the impl block. This
538
+ function scans a struct_item's field_declaration_list.
539
+
540
+ Returns name → type for plain field declarations. Never raises.
541
+ """
542
+ out: dict[str, str] = {}
543
+ try:
544
+ for child in impl_node.children:
545
+ if child.type == "field_declaration_list":
546
+ for field in child.named_children:
547
+ if field.type == "field_declaration":
548
+ name_node = field.child_by_field_name("name")
549
+ type_node = field.child_by_field_name("type")
550
+ if name_node and type_node:
551
+ name = _text(name_node).strip()
552
+ type_name = _rust_plain_type(type_node)
553
+ if name and type_name:
554
+ out[name] = type_name
555
+ except Exception as exc: # noqa: BLE001
556
+ logger.debug("scan_class_fields_rust: failed: %r", exc)
557
+ return out
558
+
559
+
560
+ # ── Slice #78: Composition (holds) collectors for Go and Rust ─────────────────
561
+ #
562
+ # These functions return deduped (held_type_name, line) pairs for a struct node.
563
+ # They REUSE scan_class_fields_go and scan_class_fields_rust for the stored-field
564
+ # half (no constructor/init-parameter pass in Go/Rust — struct fields ARE the
565
+ # composition declaration for these languages).
566
+ #
567
+ # CONSERVATISM CONTRACT (same as Python/TS collectors and receiver-type inference):
568
+ # NEVER emit a wrong type. Only plain user-type identifiers are accepted.
569
+ # Slices, maps, generics, primitives, and lowercase names are all refused by
570
+ # the existing _strip_ref_wrapper + PascalCase guard in scan_class_fields_go,
571
+ # and by _rust_plain_type in scan_class_fields_rust.
572
+ #
573
+ # DEDUPE: a type name that appears in multiple fields is emitted only ONCE.
574
+ # Dedupe is per target type name — the set returned here is deduplicated.
575
+ #
576
+ # NEVER RAISES: all public functions have a backstop try/except, return [] on error.
577
+
578
+
579
+ def collect_composition_types_go(struct_node: Node) -> list[tuple[str, int]]:
580
+ """Collect (held_type_name, line) pairs from a Go struct_type node.
581
+
582
+ Single pass: iterates the field_declaration_list, accepting only PascalCase
583
+ plain-type fields (pointer fields have the pointer stripped). Deduped by type name.
584
+
585
+ Go does not have constructors in the same sense as Python/__init__ — the struct
586
+ field declarations ARE the composition declaration. No constructor param pass needed.
587
+
588
+ WHY struct_node is the struct_type node (not the type_spec or type_declaration):
589
+ scan_class_fields_go takes the struct_type node directly. The emission site in
590
+ graph_go.py has the type_spec; it passes type_spec.child 'type' (the struct_type)
591
+ to this collector.
592
+
593
+ Returns [] on any error. Never raises.
594
+ """
595
+ try:
596
+ seen: set[str] = set()
597
+ result: list[tuple[str, int]] = []
598
+
599
+ # struct_type has a field_declaration_list child
600
+ for child in struct_node.children:
601
+ if child.type != "field_declaration_list":
602
+ continue
603
+ for field in child.named_children:
604
+ if field.type != "field_declaration":
605
+ continue
606
+ try:
607
+ type_node = field.child_by_field_name("type")
608
+ if type_node is None:
609
+ continue
610
+ type_text = _text(type_node).strip()
611
+ # _strip_ref_wrapper handles *Type → Type (and refuses slices/generics).
612
+ type_name = _strip_ref_wrapper(type_text) or type_text
613
+ # Require PascalCase (user-defined type) and non-empty.
614
+ if not type_name or not type_name[0].isupper():
615
+ continue
616
+ # Refuse types that still contain pointer/ref chars after strip
617
+ # (double-pointer or slice after strip → residual * or [).
618
+ if "*" in type_name or "[" in type_name or "<" in type_name:
619
+ continue
620
+ if type_name not in seen:
621
+ seen.add(type_name)
622
+ result.append((type_name, field.start_point[0] + 1))
623
+ except Exception: # noqa: BLE001
624
+ pass
625
+ break # Only one field_declaration_list per struct
626
+
627
+ # Post-filter: drop dotted qualified targets (e.g. 'pkg.Client' — would be a
628
+ # wrong/dangling edge since edges are bare-name-keyed) and language builtins
629
+ # (the PascalCase heuristic alone admits stdlib types). is_builtin is the
630
+ # authoritative source, mirroring graph_swift_infer.
631
+ return [(t, ln) for (t, ln) in result if "." not in t and not is_builtin(t, "go")]
632
+ except Exception as exc: # noqa: BLE001
633
+ logger.debug("collect_composition_types_go: failed: %r", exc)
634
+ return []
635
+
636
+
637
+ def collect_composition_types_rust(struct_node: Node) -> list[tuple[str, int]]:
638
+ """Collect (held_type_name, line) pairs from a Rust struct_item node.
639
+
640
+ Single pass: iterates the field_declaration_list, accepting only PascalCase
641
+ plain-type fields (reference types &T have the ref stripped via _rust_plain_type).
642
+ Deduped by type name.
643
+
644
+ Rust has no constructor-as-composition pattern that is statically visible in the
645
+ struct AST (::new() is an associated function, not a struct declaration), so
646
+ only struct field declarations are scanned. This mirrors how scan_class_fields_rust
647
+ works, but also captures the line number for the Edge.
648
+
649
+ WHY struct_node is the struct_item node:
650
+ The emission site in graph_rust.py has the struct_item node directly available
651
+ at the point where we index the struct symbol.
652
+
653
+ Returns [] on any error. Never raises.
654
+ """
655
+ try:
656
+ seen: set[str] = set()
657
+ result: list[tuple[str, int]] = []
658
+
659
+ for child in struct_node.children:
660
+ if child.type != "field_declaration_list":
661
+ continue
662
+ for field in child.named_children:
663
+ if field.type != "field_declaration":
664
+ continue
665
+ try:
666
+ type_node = field.child_by_field_name("type")
667
+ if type_node is None:
668
+ continue
669
+ type_name = _rust_plain_type(type_node)
670
+ if not type_name:
671
+ continue
672
+ if type_name not in seen:
673
+ seen.add(type_name)
674
+ result.append((type_name, field.start_point[0] + 1))
675
+ except Exception: # noqa: BLE001
676
+ pass
677
+ break # Only one field_declaration_list per struct
678
+
679
+ # Post-filter builtins (e.g. Rust 'String', 'Box' used bare) via the
680
+ # authoritative is_builtin source — the PascalCase heuristic alone admits them.
681
+ return [(t, ln) for (t, ln) in result if not is_builtin(t, "rust")]
682
+ except Exception as exc: # noqa: BLE001
683
+ logger.debug("collect_composition_types_rust: failed: %r", exc)
684
+ return []
685
+
686
+
687
+ def param_types_via_recorder(func_node: Node, recorder, language: str) -> list[tuple[str, int]]:
688
+ """Shared 'uses'-collector: run a record_<lang>_param_types recorder and return its
689
+ bound (plain-user-type, function-line) pairs, deduped and builtin-filtered.
690
+
691
+ The receiver-inference recorders already bind only plain user types (refusing
692
+ builtins/optionals/generics/containers/pointers-stripped) — exactly the conservatism
693
+ `uses` needs — so reusing them keeps `uses` and receiver inference byte-aligned. The
694
+ recorder maps param-name → type; we discard names and keep distinct types. The edge
695
+ line is the function's start line (the recorder does not track per-param lines).
696
+
697
+ Never raises (recorders never raise; this wraps defensively anyway).
698
+ """
699
+ try:
700
+ tmp: dict[str, str] = {}
701
+ recorder(func_node, tmp)
702
+ line = func_node.start_point[0] + 1
703
+ seen: set[str] = set()
704
+ out: list[tuple[str, int]] = []
705
+ for t in tmp.values():
706
+ if t and t not in seen and not is_builtin(t, language):
707
+ seen.add(t)
708
+ out.append((t, line))
709
+ return out
710
+ except Exception as exc: # noqa: BLE001
711
+ logger.debug("param_types_via_recorder(%s): failed: %r", language, exc)
712
+ return []
713
+
714
+
715
+ def collect_param_types_go(func_node: Node) -> list[tuple[str, int]]:
716
+ """(param_type, line) pairs for a Go function/method — reuses record_go_param_types."""
717
+ return param_types_via_recorder(func_node, record_go_param_types, "go")
718
+
719
+
720
+ def collect_param_types_rust(func_node: Node) -> list[tuple[str, int]]:
721
+ """(param_type, line) pairs for a Rust function — reuses record_rust_param_types."""
722
+ return param_types_via_recorder(func_node, record_rust_param_types, "rust")
723
+