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,992 @@
1
+ """Scope-inference extension module — Java, C#, C++, Ruby, and PHP language families.
2
+
3
+ LAYER: leaf — imports from graph_common (leaf) and stdlib only. Never imports from
4
+ graph.py, graph_scope_infer, graph_scope_infer_ext, or any other seam module with effects.
5
+
6
+ LAYERING:
7
+ graph_common (leaf — no seam deps)
8
+
9
+ graph_scope_infer_ext2 (this file — Java/C#/C++/Ruby/PHP type-binding helpers)
10
+
11
+ graph_java_csharp.py (_extract_edges_java / _extract_edges_csharp use these helpers)
12
+ graph_c_cpp.py (_extract_edges_cpp uses these helpers)
13
+ graph_ruby.py (_walk_ruby_edges uses these helpers)
14
+ graph_php.py (_walk_php_edges uses these helpers)
15
+
16
+ WHY a split from graph_scope_infer_ext.py:
17
+ graph_scope_infer_ext.py would exceed 1000 lines if it contained all 7 language families
18
+ inline. Following the Phase 9 split precedent (graph_java_csharp / graph_c_cpp / etc.),
19
+ the Java/C#/C++/Ruby/PHP families live here while Go/Rust + shared helpers live in ext.
20
+
21
+ CONSERVATISM CONTRACT: identical to graph_scope_infer_ext.py — all functions never raise.
22
+ """
23
+
24
+ import logging
25
+
26
+ from tree_sitter import Node
27
+
28
+ from seam.analysis.builtins import is_builtin
29
+ from seam.indexer.graph_common import _text
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # ── Self/this aliases ─────────────────────────────────────────────────────────
34
+
35
+ # Java: 'this' — super is excluded (we don't know the superclass statically).
36
+ _JAVA_SELF_NAMES: frozenset[str] = frozenset({"this"})
37
+
38
+ # C#: 'this' — same reasoning as Java.
39
+ _CS_SELF_NAMES: frozenset[str] = frozenset({"this"})
40
+
41
+ # C++: 'this' is always a pointer to the current object.
42
+ _CPP_SELF_NAMES: frozenset[str] = frozenset({"this"})
43
+
44
+ # Ruby: 'self' is the conventional receiver.
45
+ _RUBY_SELF_NAMES: frozenset[str] = frozenset({"self"})
46
+
47
+ # PHP: '$this' is the conventional receiver (with the $ prefix from receiver text).
48
+ _PHP_SELF_NAMES: frozenset[str] = frozenset({"$this", "this"})
49
+
50
+
51
+ # ── Java: parameter and local-variable type binding ───────────────────────────
52
+
53
+
54
+ def record_java_param_types(method_node: Node, var_types: dict[str, str]) -> None:
55
+ """Bind Java method parameter names → declared type into var_types.
56
+
57
+ Java parameter shape (inside formal_parameters):
58
+ formal_parameter → type + name (e.g. `Client client`)
59
+
60
+ Conservative: only plain type_identifier binds. generic_type (List<T>) → refused.
61
+ Never raises.
62
+ """
63
+ try:
64
+ params = method_node.child_by_field_name("parameters")
65
+ if params is None:
66
+ return
67
+ for child in params.named_children:
68
+ if child.type == "formal_parameter":
69
+ _record_java_single_param(child, var_types)
70
+ except Exception as exc: # noqa: BLE001
71
+ logger.debug("record_java_param_types: failed: %r", exc)
72
+
73
+
74
+ def _record_java_single_param(param_node: Node, var_types: dict[str, str]) -> None:
75
+ """Record a single Java formal_parameter's name → type binding."""
76
+ try:
77
+ type_node = param_node.child_by_field_name("type")
78
+ name_node = param_node.child_by_field_name("name")
79
+ if type_node is None or name_node is None:
80
+ return
81
+ # Only plain type_identifier (no generics, no arrays)
82
+ if type_node.type != "type_identifier":
83
+ return
84
+ type_name = _text(type_node).strip()
85
+ if not type_name or not type_name[0].isupper():
86
+ return
87
+ name = _text(name_node).strip()
88
+ if name and name not in _JAVA_SELF_NAMES:
89
+ var_types[name] = type_name
90
+ except Exception as exc: # noqa: BLE001
91
+ logger.debug("_record_java_single_param: failed: %r", exc)
92
+
93
+
94
+ def record_java_local_types(stmt_node: Node, var_types: dict[str, str]) -> None:
95
+ """Record type bindings from a Java local statement.
96
+
97
+ Handles:
98
+ local_variable_declaration: `Client c = new Client()` → c → Client
99
+
100
+ Conservative: only plain type_identifier (no generics). Never raises.
101
+ """
102
+ try:
103
+ if stmt_node.type != "local_variable_declaration":
104
+ return
105
+ type_node = stmt_node.child_by_field_name("type")
106
+ if type_node is None or type_node.type != "type_identifier":
107
+ return
108
+ type_name = _text(type_node).strip()
109
+ if not type_name or not type_name[0].isupper():
110
+ return
111
+ for child in stmt_node.named_children:
112
+ if child.type == "variable_declarator":
113
+ name_node = child.child_by_field_name("name")
114
+ if name_node is not None:
115
+ name = _text(name_node).strip()
116
+ if name:
117
+ var_types[name] = type_name
118
+ except Exception as exc: # noqa: BLE001
119
+ logger.debug("record_java_local_types: failed: %r", exc)
120
+
121
+
122
+ def scan_class_fields_java(class_node: Node) -> dict[str, str]:
123
+ """Pre-scan a Java class_declaration body for field-level type bindings.
124
+
125
+ Captures:
126
+ Repository repo; → repo → Repository
127
+ private Repository repo; → repo → Repository
128
+
129
+ Returns name → type for direct class body field declarations. Never raises.
130
+ """
131
+ out: dict[str, str] = {}
132
+ try:
133
+ body = class_node.child_by_field_name("body")
134
+ if body is None:
135
+ return out
136
+ for child in body.named_children:
137
+ if child.type == "field_declaration":
138
+ _record_java_field(child, out)
139
+ except Exception as exc: # noqa: BLE001
140
+ logger.debug("scan_class_fields_java: failed: %r", exc)
141
+ return out
142
+
143
+
144
+ def _record_java_field(field_node: Node, out: dict[str, str]) -> None:
145
+ """Record a single Java field_declaration's name → type binding."""
146
+ try:
147
+ type_node = field_node.child_by_field_name("type")
148
+ if type_node is None or type_node.type != "type_identifier":
149
+ return
150
+ type_name = _text(type_node).strip()
151
+ if not type_name or not type_name[0].isupper():
152
+ return
153
+ for child in field_node.named_children:
154
+ if child.type == "variable_declarator":
155
+ name_node = child.child_by_field_name("name")
156
+ if name_node is not None:
157
+ name = _text(name_node).strip()
158
+ if name:
159
+ out[name] = type_name
160
+ except Exception as exc: # noqa: BLE001
161
+ logger.debug("_record_java_field: failed: %r", exc)
162
+
163
+
164
+ # ── C#: parameter and local-variable type binding ─────────────────────────────
165
+
166
+
167
+ def record_cs_param_types(method_node: Node, var_types: dict[str, str]) -> None:
168
+ """Bind C# method parameter names → declared type into var_types.
169
+
170
+ C# parameter shape (inside parameter_list):
171
+ parameter → type + name (e.g. `Client client`)
172
+
173
+ Conservative: only plain identifier type nodes bind. Never raises.
174
+ """
175
+ try:
176
+ params = method_node.child_by_field_name("parameters")
177
+ if params is None:
178
+ return
179
+ for child in params.named_children:
180
+ if child.type == "parameter":
181
+ _record_cs_single_param(child, var_types)
182
+ except Exception as exc: # noqa: BLE001
183
+ logger.debug("record_cs_param_types: failed: %r", exc)
184
+
185
+
186
+ def _record_cs_single_param(param_node: Node, var_types: dict[str, str]) -> None:
187
+ """Record a single C# parameter's name → type binding."""
188
+ try:
189
+ type_node = param_node.child_by_field_name("type")
190
+ name_node = param_node.child_by_field_name("name")
191
+ if type_node is None or name_node is None:
192
+ return
193
+ # Only plain identifier (no nullable T?, no generic List<T>, no array T[])
194
+ if type_node.type != "identifier":
195
+ return
196
+ type_name = _text(type_node).strip()
197
+ if not type_name or not type_name[0].isupper():
198
+ return
199
+ name = _text(name_node).strip()
200
+ if name and name not in _CS_SELF_NAMES:
201
+ var_types[name] = type_name
202
+ except Exception as exc: # noqa: BLE001
203
+ logger.debug("_record_cs_single_param: failed: %r", exc)
204
+
205
+
206
+ def record_cs_local_types(stmt_node: Node, var_types: dict[str, str]) -> None:
207
+ """Record type bindings from a C# local statement.
208
+
209
+ Handles:
210
+ local_declaration_statement → variable_declaration → type(identifier) + variable_declarator
211
+ e.g. `Client c = new Client();` → c → Client
212
+
213
+ In tree-sitter-c-sharp, local_declaration_statement contains a variable_declaration
214
+ child (not a direct 'type' field). The first identifier child of variable_declaration
215
+ is the type name; subsequent children are variable_declarators.
216
+
217
+ Conservative: only plain identifier type annotations bind (no generics, no nullable).
218
+ Never raises.
219
+ """
220
+ try:
221
+ if stmt_node.type != "local_declaration_statement":
222
+ return
223
+ var_decl = None
224
+ for child in stmt_node.children:
225
+ if child.type == "variable_declaration":
226
+ var_decl = child
227
+ break
228
+ if var_decl is None:
229
+ return
230
+ type_name: str | None = None
231
+ for child in var_decl.children:
232
+ if child.type == "identifier":
233
+ type_name = _text(child).strip()
234
+ break
235
+ if child.type not in ("variable_declarator", "=", ","):
236
+ break # Non-identifier type → refuse
237
+ if not type_name or not type_name[0].isupper():
238
+ return
239
+ for child in var_decl.named_children:
240
+ if child.type == "variable_declarator":
241
+ for dc in child.children:
242
+ if dc.type == "identifier":
243
+ name = _text(dc).strip()
244
+ if name:
245
+ var_types[name] = type_name
246
+ break
247
+ except Exception as exc: # noqa: BLE001
248
+ logger.debug("record_cs_local_types: failed: %r", exc)
249
+
250
+
251
+ def scan_class_fields_cs(class_node: Node) -> dict[str, str]:
252
+ """Pre-scan a C# class_declaration body for field-level type bindings.
253
+
254
+ Captures:
255
+ private Client client; → client → Client
256
+ public Repository repo = new ...(); → repo → Repository
257
+
258
+ Returns name → type for direct class body field declarations. Never raises.
259
+ """
260
+ out: dict[str, str] = {}
261
+ try:
262
+ body = class_node.child_by_field_name("body")
263
+ if body is None:
264
+ return out
265
+ for child in body.named_children:
266
+ if child.type == "field_declaration":
267
+ _record_cs_field(child, out)
268
+ except Exception as exc: # noqa: BLE001
269
+ logger.debug("scan_class_fields_cs: failed: %r", exc)
270
+ return out
271
+
272
+
273
+ def _record_cs_field(field_node: Node, out: dict[str, str]) -> None:
274
+ """Record a single C# field_declaration's name → type binding.
275
+
276
+ C# field_declaration structure (tree-sitter-c-sharp):
277
+ field_declaration → modifier* + variable_declaration
278
+ variable_declaration → identifier(type) + variable_declarator*(name)
279
+ There is NO direct 'type' field on field_declaration.
280
+ """
281
+ try:
282
+ var_decl = None
283
+ for child in field_node.children:
284
+ if child.type == "variable_declaration":
285
+ var_decl = child
286
+ break
287
+ if var_decl is None:
288
+ return
289
+ type_name: str | None = None
290
+ for child in var_decl.children:
291
+ if child.type == "identifier":
292
+ type_name = _text(child).strip()
293
+ break
294
+ if not type_name or not type_name[0].isupper():
295
+ return
296
+ for child in var_decl.named_children:
297
+ if child.type == "variable_declarator":
298
+ for dc in child.children:
299
+ if dc.type == "identifier":
300
+ name = _text(dc).strip()
301
+ if name:
302
+ out[name] = type_name
303
+ break
304
+ except Exception as exc: # noqa: BLE001
305
+ logger.debug("_record_cs_field: failed: %r", exc)
306
+
307
+
308
+ # ── C++: parameter and local-variable type binding ────────────────────────────
309
+
310
+
311
+ def record_cpp_param_types(func_node: Node, var_types: dict[str, str]) -> None:
312
+ """Bind C++ function parameter names → declared type into var_types.
313
+
314
+ C++ function_definition has a 'declarator' (function_declarator) with
315
+ 'parameters' (parameter_list). Each parameter_declaration has type + name.
316
+
317
+ Conservative: only plain type_identifier binds. Pointers/refs (&T, *T) → strip.
318
+ Never raises.
319
+ """
320
+ try:
321
+ declarator = func_node.child_by_field_name("declarator")
322
+ if declarator is None or declarator.type != "function_declarator":
323
+ return
324
+ params = declarator.child_by_field_name("parameters")
325
+ if params is None:
326
+ return
327
+ for child in params.named_children:
328
+ if child.type == "parameter_declaration":
329
+ _record_cpp_single_param(child, var_types)
330
+ except Exception as exc: # noqa: BLE001
331
+ logger.debug("record_cpp_param_types: failed: %r", exc)
332
+
333
+
334
+ def _record_cpp_single_param(param_node: Node, var_types: dict[str, str]) -> None:
335
+ """Record a single C++ parameter_declaration's name → type binding."""
336
+ try:
337
+ type_name = _cpp_extract_type_name(param_node)
338
+ if not type_name or not type_name[0].isupper():
339
+ return
340
+ name = _cpp_extract_param_name(param_node)
341
+ if name and name not in _CPP_SELF_NAMES:
342
+ var_types[name] = type_name
343
+ except Exception as exc: # noqa: BLE001
344
+ logger.debug("_record_cpp_single_param: failed: %r", exc)
345
+
346
+
347
+ def _cpp_extract_type_name(param_node: Node) -> str | None:
348
+ """Extract the plain base type name from a C++ parameter_declaration.
349
+
350
+ Handles:
351
+ Client client → type specifier is identifier 'Client'
352
+ Client& client → reference_declarator; type specifier is 'Client'
353
+ Client* client → pointer_declarator; type specifier is 'Client'
354
+ const Client& client → type specifier (const Client); name = 'Client'
355
+
356
+ Refuses: templates (generic_type / template_type), qualified names with ::, arrays.
357
+ """
358
+ for child in param_node.children:
359
+ t = child.type
360
+ if t == "type_identifier":
361
+ name = _text(child).strip()
362
+ return name if (name and name[0].isupper()) else None
363
+ if t in ("type_qualifier", "storage_class_specifier"):
364
+ continue # skip 'const', 'volatile', 'static'
365
+ return None
366
+
367
+
368
+ def _cpp_extract_param_name(param_node: Node) -> str | None:
369
+ """Extract the parameter name from a C++ parameter_declaration."""
370
+ for child in param_node.children:
371
+ t = child.type
372
+ if t == "identifier":
373
+ return _text(child).strip()
374
+ if t in ("reference_declarator", "pointer_declarator"):
375
+ for gc in child.children:
376
+ if gc.type == "identifier":
377
+ return _text(gc).strip()
378
+ return None
379
+
380
+
381
+ def record_cpp_local_types(stmt_node: Node, var_types: dict[str, str]) -> None:
382
+ """Record type bindings from a C++ local statement.
383
+
384
+ Handles:
385
+ declaration: `Client c;` or `Client c = ...;` or `Client* c = new Client();`
386
+
387
+ Conservative: only plain type_identifier (no templates). Never raises.
388
+ """
389
+ try:
390
+ if stmt_node.type != "declaration":
391
+ return
392
+ type_name = _cpp_extract_type_name(stmt_node)
393
+ if not type_name:
394
+ return
395
+ for child in stmt_node.children:
396
+ if child.type in ("identifier", "init_declarator"):
397
+ if child.type == "identifier":
398
+ bare_name: str = _text(child).strip()
399
+ if bare_name and bare_name not in _CPP_SELF_NAMES:
400
+ var_types[bare_name] = type_name
401
+ elif child.type == "init_declarator":
402
+ decl = child.child_by_field_name("declarator")
403
+ if decl is None:
404
+ decl = child.children[0] if child.children else None
405
+ if decl is not None:
406
+ decl_name: str | None = _cpp_extract_param_name_from_decl(decl)
407
+ if decl_name and decl_name not in _CPP_SELF_NAMES:
408
+ var_types[decl_name] = type_name
409
+ except Exception as exc: # noqa: BLE001
410
+ logger.debug("record_cpp_local_types: failed: %r", exc)
411
+
412
+
413
+ def _cpp_extract_param_name_from_decl(decl_node: Node) -> str | None:
414
+ """Extract identifier name from a C++ declarator node."""
415
+ if decl_node.type == "identifier":
416
+ return _text(decl_node).strip()
417
+ if decl_node.type in ("reference_declarator", "pointer_declarator"):
418
+ for child in decl_node.children:
419
+ if child.type == "identifier":
420
+ return _text(child).strip()
421
+ return None
422
+
423
+
424
+ def scan_class_fields_cpp(class_node: Node) -> dict[str, str]:
425
+ """Pre-scan a C++ class_specifier or struct_specifier body for field type bindings.
426
+
427
+ Captures:
428
+ Client client; → client → Client (field declaration in class body)
429
+
430
+ Returns name → type for direct class body declarations. Never raises.
431
+ """
432
+ out: dict[str, str] = {}
433
+ try:
434
+ body = class_node.child_by_field_name("body")
435
+ if body is None:
436
+ return out
437
+ for child in body.named_children:
438
+ if child.type == "field_declaration":
439
+ type_name = _cpp_extract_type_name(child)
440
+ if not type_name:
441
+ continue
442
+ for decl_child in child.children:
443
+ if decl_child.type == "identifier":
444
+ name = _text(decl_child).strip()
445
+ if name and name not in _CPP_SELF_NAMES:
446
+ out[name] = type_name
447
+ except Exception as exc: # noqa: BLE001
448
+ logger.debug("scan_class_fields_cpp: failed: %r", exc)
449
+ return out
450
+
451
+
452
+ # ── Ruby: local-variable and ivar type binding ────────────────────────────────
453
+
454
+
455
+ def record_ruby_local_types(stmt_node: Node, var_types: dict[str, str]) -> None:
456
+ """Record type bindings from a Ruby statement.
457
+
458
+ Ruby has no static type annotations. We infer from constructor calls:
459
+ client = Client.new → client → Client
460
+ @repo = Repository.new → @repo → Repository (ivar)
461
+
462
+ Conservative: only `.new` calls with a plain constant (PascalCase) receiver bind.
463
+ Never raises.
464
+ """
465
+ try:
466
+ if stmt_node.type == "assignment":
467
+ left = stmt_node.child_by_field_name("left")
468
+ right = stmt_node.child_by_field_name("right")
469
+ if left is None or right is None:
470
+ return
471
+ cls = _ruby_new_call_class(right)
472
+ if cls:
473
+ name = _text(left).strip()
474
+ if name:
475
+ var_types[name] = cls
476
+ except Exception as exc: # noqa: BLE001
477
+ logger.debug("record_ruby_local_types: failed: %r", exc)
478
+
479
+
480
+ def _ruby_new_call_class(value_node: Node) -> str | None:
481
+ """If value_node is a `ClassName.new` call, return 'ClassName', else None."""
482
+ try:
483
+ if value_node.type != "call":
484
+ return None
485
+ method_node = value_node.child_by_field_name("method")
486
+ if method_node is None or _text(method_node).strip() != "new":
487
+ return None
488
+ receiver_node = value_node.child_by_field_name("receiver")
489
+ if receiver_node is None or receiver_node.type != "constant":
490
+ return None
491
+ name = _text(receiver_node).strip()
492
+ return name if (name and name[0].isupper()) else None
493
+ except Exception: # noqa: BLE001
494
+ return None
495
+
496
+
497
+ def scan_class_fields_ruby(class_node: Node) -> dict[str, str]:
498
+ """Pre-scan a Ruby class body for ivar type bindings from initialize.
499
+
500
+ Looks for `@name = ClassName.new` in the initialize method.
501
+ Returns name → type for @ivar bindings found in initialize. Never raises.
502
+ """
503
+ out: dict[str, str] = {}
504
+ try:
505
+ body = class_node.child_by_field_name("body")
506
+ if body is None:
507
+ return out
508
+ for child in body.named_children:
509
+ if child.type == "method":
510
+ method_name_node = child.child_by_field_name("name")
511
+ if method_name_node and _text(method_name_node).strip() == "initialize":
512
+ _scan_ruby_method_body(child, out)
513
+ except Exception as exc: # noqa: BLE001
514
+ logger.debug("scan_class_fields_ruby: failed: %r", exc)
515
+ return out
516
+
517
+
518
+ def _scan_ruby_method_body(method_node: Node, out: dict[str, str]) -> None:
519
+ """Walk a Ruby method body for ivar assignment patterns."""
520
+ try:
521
+ body = method_node.child_by_field_name("body")
522
+ if body is None:
523
+ return
524
+ for child in body.named_children:
525
+ if child.type == "assignment":
526
+ record_ruby_local_types(child, out)
527
+ except Exception: # noqa: BLE001
528
+ pass
529
+
530
+
531
+ # ── PHP: parameter and local-variable type binding ────────────────────────────
532
+
533
+
534
+ def record_php_param_types(method_node: Node, var_types: dict[str, str]) -> None:
535
+ """Bind PHP method parameter names → declared type into var_types.
536
+
537
+ PHP parameter shape (inside formal_parameters):
538
+ simple_parameter → type? + name (e.g. `Client $client`)
539
+
540
+ Conservative: only plain named_type (plain class name) binds. Union types,
541
+ nullable (?Client), generics → refused. Never raises.
542
+ """
543
+ try:
544
+ params = method_node.child_by_field_name("parameters")
545
+ if params is None:
546
+ return
547
+ for child in params.named_children:
548
+ if child.type == "simple_parameter":
549
+ _record_php_single_param(child, var_types)
550
+ except Exception as exc: # noqa: BLE001
551
+ logger.debug("record_php_param_types: failed: %r", exc)
552
+
553
+
554
+ def _record_php_single_param(param_node: Node, var_types: dict[str, str]) -> None:
555
+ """Record a single PHP simple_parameter's name → type binding."""
556
+ try:
557
+ type_node = None
558
+ name_node = None
559
+ for child in param_node.children:
560
+ if child.type == "named_type":
561
+ type_node = child
562
+ elif child.type == "variable_name":
563
+ name_node = child
564
+ if type_node is None or name_node is None:
565
+ return
566
+ type_name = None
567
+ for child in type_node.children:
568
+ if child.type == "name":
569
+ type_name = _text(child).strip()
570
+ break
571
+ if not type_name or not type_name[0].isupper():
572
+ return
573
+ name = _text(name_node).strip() # includes $ prefix: '$client'
574
+ if name:
575
+ var_types[name] = type_name
576
+ except Exception as exc: # noqa: BLE001
577
+ logger.debug("_record_php_single_param: failed: %r", exc)
578
+
579
+
580
+ def record_php_local_types(stmt_node: Node, var_types: dict[str, str]) -> None:
581
+ """Record type bindings from a PHP local statement.
582
+
583
+ Handles:
584
+ expression_statement with assignment: $e = new Engine() → $e → Engine
585
+
586
+ Conservative: only `new ClassName` (plain name, no generic) binds. Never raises.
587
+ """
588
+ try:
589
+ if stmt_node.type == "expression_statement" and stmt_node.children:
590
+ inner = stmt_node.children[0]
591
+ if inner.type == "assignment_expression":
592
+ left = inner.child_by_field_name("left")
593
+ right = inner.child_by_field_name("right")
594
+ if left is not None and right is not None:
595
+ cls = _php_new_class(right)
596
+ if cls:
597
+ name = _text(left).strip() # includes $ prefix
598
+ if name:
599
+ var_types[name] = cls
600
+ except Exception as exc: # noqa: BLE001
601
+ logger.debug("record_php_local_types: failed: %r", exc)
602
+
603
+
604
+ def _php_new_class(value_node: Node) -> str | None:
605
+ """If value_node is `new ClassName(...)` or `new ClassName`, return 'ClassName', else None."""
606
+ try:
607
+ if value_node.type != "object_creation_expression":
608
+ return None
609
+ for child in value_node.children:
610
+ if child.type in ("qualified_name", "name"):
611
+ if child.type == "name":
612
+ name = _text(child).strip()
613
+ return name if (name and name[0].isupper()) else None
614
+ if child.type == "qualified_name":
615
+ last = None
616
+ for qchild in child.children:
617
+ if qchild.type == "name":
618
+ last = _text(qchild).strip()
619
+ return last if (last and last[0].isupper()) else None
620
+ except Exception: # noqa: BLE001
621
+ pass
622
+ return None
623
+
624
+
625
+ def scan_class_fields_php(class_node: Node) -> dict[str, str]:
626
+ """Pre-scan a PHP class_declaration body for property type bindings.
627
+
628
+ Captures:
629
+ private Client $client; → $client → Client
630
+ public Repository $repo; → $repo → Repository
631
+
632
+ Returns name → type for declared class properties. Never raises.
633
+ """
634
+ out: dict[str, str] = {}
635
+ try:
636
+ body = class_node.child_by_field_name("body")
637
+ if body is None:
638
+ return out
639
+ for child in body.named_children:
640
+ if child.type == "property_declaration":
641
+ _record_php_property(child, out)
642
+ except Exception as exc: # noqa: BLE001
643
+ logger.debug("scan_class_fields_php: failed: %r", exc)
644
+ return out
645
+
646
+
647
+ def _record_php_property(prop_node: Node, out: dict[str, str]) -> None:
648
+ """Record a single PHP property_declaration's name → type binding."""
649
+ try:
650
+ type_name = None
651
+ prop_name = None
652
+ for child in prop_node.children:
653
+ if child.type == "named_type":
654
+ for tc in child.children:
655
+ if tc.type == "name":
656
+ type_name = _text(tc).strip()
657
+ break
658
+ elif child.type == "property_element":
659
+ for pc in child.children:
660
+ if pc.type == "variable_name":
661
+ prop_name = _text(pc).strip()
662
+ break
663
+ if type_name and prop_name and type_name[0].isupper():
664
+ out[prop_name] = type_name
665
+ except Exception: # noqa: BLE001
666
+ pass
667
+
668
+
669
+ # ── Slice #79: Composition (holds) collectors for Java, C#, C++, Ruby, PHP ───
670
+ # Reuse scan_class_fields_* helpers; add ctor/init param pass where applicable.
671
+ # Conservatism: plain user-type identifiers only. Generics/nullable/primitives refused.
672
+ # Dedupe: same type from field + ctor → one entry. Never raises; returns [] on error.
673
+
674
+
675
+ def collect_composition_types_java(class_node: Node) -> list[tuple[str, int]]:
676
+ """Collect (held_type_name, line) pairs from a Java class_declaration node.
677
+
678
+ Pass 1: field_declaration with plain type_identifier. Pass 2: constructor_declaration
679
+ formal_parameter with plain type_identifier. Deduped by type name.
680
+ Returns [] on any error. Never raises.
681
+ """
682
+ try:
683
+ seen: set[str] = set()
684
+ result: list[tuple[str, int]] = []
685
+
686
+ body = class_node.child_by_field_name("body")
687
+ if body is None:
688
+ return result
689
+
690
+ # Pass 1: class field declarations (plain type_identifier only).
691
+ for child in body.named_children:
692
+ if child.type != "field_declaration":
693
+ continue
694
+ try:
695
+ type_node = child.child_by_field_name("type")
696
+ if type_node is None or type_node.type != "type_identifier":
697
+ continue
698
+ type_name = _text(type_node).strip()
699
+ if not type_name or not type_name[0].isupper():
700
+ continue
701
+ if type_name not in seen:
702
+ seen.add(type_name)
703
+ result.append((type_name, child.start_point[0] + 1))
704
+ except Exception: # noqa: BLE001
705
+ pass
706
+
707
+ # Pass 2: constructor parameter types (constructor_declaration formal_parameters).
708
+ for child in body.named_children:
709
+ if child.type != "constructor_declaration":
710
+ continue
711
+ try:
712
+ params = child.child_by_field_name("parameters")
713
+ if params is None:
714
+ continue
715
+ for param in params.named_children:
716
+ if param.type != "formal_parameter":
717
+ continue
718
+ type_node = param.child_by_field_name("type")
719
+ if type_node is None or type_node.type != "type_identifier":
720
+ continue
721
+ type_name = _text(type_node).strip()
722
+ if not type_name or not type_name[0].isupper():
723
+ continue
724
+ if type_name not in seen:
725
+ seen.add(type_name)
726
+ result.append((type_name, param.start_point[0] + 1))
727
+ except Exception: # noqa: BLE001
728
+ pass
729
+
730
+ # Post-filter builtins (String/Integer/List/Map/etc.) — the PascalCase guard
731
+ # admits them; is_builtin is the authoritative cross-language source.
732
+ return [(t, ln) for (t, ln) in result if not is_builtin(t, "java")]
733
+ except Exception as exc: # noqa: BLE001
734
+ logger.debug("collect_composition_types_java: failed: %r", exc)
735
+ return []
736
+
737
+
738
+ def collect_composition_types_cs(class_node: Node) -> list[tuple[str, int]]:
739
+ """Collect (held_type_name, line) pairs from a C# class/struct/record_declaration node.
740
+
741
+ Pass 1: field_declaration → variable_declaration first identifier (type name).
742
+ Pass 2: constructor_declaration parameter → plain identifier type (no nullable/generic).
743
+ Deduped by type name. Returns [] on any error. Never raises.
744
+ """
745
+ try:
746
+ seen: set[str] = set()
747
+ result: list[tuple[str, int]] = []
748
+
749
+ body = class_node.child_by_field_name("body")
750
+ if body is None:
751
+ return result
752
+
753
+ # Pass 1: field declarations.
754
+ for child in body.named_children:
755
+ if child.type != "field_declaration":
756
+ continue
757
+ try:
758
+ # C# field_declaration → variable_declaration → first identifier = type name.
759
+ var_decl = None
760
+ for fc in child.children:
761
+ if fc.type == "variable_declaration":
762
+ var_decl = fc
763
+ break
764
+ if var_decl is None:
765
+ continue
766
+ type_name: str | None = None
767
+ for vc in var_decl.children:
768
+ if vc.type == "identifier":
769
+ type_name = _text(vc).strip()
770
+ break
771
+ if not type_name or not type_name[0].isupper():
772
+ continue
773
+ if type_name not in seen:
774
+ seen.add(type_name)
775
+ result.append((type_name, child.start_point[0] + 1))
776
+ except Exception: # noqa: BLE001
777
+ pass
778
+
779
+ # Pass 2: constructor parameter types.
780
+ for child in body.named_children:
781
+ if child.type != "constructor_declaration":
782
+ continue
783
+ try:
784
+ params = child.child_by_field_name("parameters")
785
+ if params is None:
786
+ continue
787
+ for param in params.named_children:
788
+ if param.type != "parameter":
789
+ continue
790
+ type_node = param.child_by_field_name("type")
791
+ name_node = param.child_by_field_name("name")
792
+ if type_node is None or name_node is None:
793
+ continue
794
+ # Only plain identifier (no nullable, no generic).
795
+ if type_node.type != "identifier":
796
+ continue
797
+ type_name = _text(type_node).strip()
798
+ if not type_name or not type_name[0].isupper():
799
+ continue
800
+ if type_name not in seen:
801
+ seen.add(type_name)
802
+ result.append((type_name, param.start_point[0] + 1))
803
+ except Exception: # noqa: BLE001
804
+ pass
805
+
806
+ # Post-filter builtins (String/Int32/List/etc.) via the authoritative source.
807
+ return [(t, ln) for (t, ln) in result if not is_builtin(t, "csharp")]
808
+ except Exception as exc: # noqa: BLE001
809
+ logger.debug("collect_composition_types_cs: failed: %r", exc)
810
+ return []
811
+
812
+
813
+ def collect_composition_types_cpp(class_node: Node) -> list[tuple[str, int]]:
814
+ """Collect (held_type_name, line) pairs from a C++ class_specifier/struct_specifier node.
815
+
816
+ Single pass: field_declaration nodes in the body with a plain type_identifier.
817
+ Uses _cpp_extract_type_name (same as scan_class_fields_cpp). No ctor-param pass
818
+ (C++ constructors may be out-of-line). Deduped. Returns [] on error. Never raises.
819
+
820
+ WHY no constructor-parameter pass:
821
+ C++ constructors are often defined out-of-line (in the .cpp file, not the header).
822
+ The class_specifier/struct_specifier node only contains the declaration, not the
823
+ definition. Scanning ctor params would require finding the matching
824
+ function_definition elsewhere in the AST — complex and unreliable. Field
825
+ declarations in the class body are always present and authoritative.
826
+ """
827
+ try:
828
+ seen: set[str] = set()
829
+ result: list[tuple[str, int]] = []
830
+
831
+ body = class_node.child_by_field_name("body")
832
+ if body is None:
833
+ return result
834
+
835
+ for child in body.named_children:
836
+ if child.type != "field_declaration":
837
+ continue
838
+ try:
839
+ # _cpp_extract_type_name searches for type_identifier child.
840
+ type_name = _cpp_extract_type_name(child)
841
+ if not type_name or not type_name[0].isupper():
842
+ continue
843
+ if type_name not in seen:
844
+ seen.add(type_name)
845
+ result.append((type_name, child.start_point[0] + 1))
846
+ except Exception: # noqa: BLE001
847
+ pass
848
+
849
+ # Post-filter builtins (std::string surfaces as 'string'; PascalCase stdlib
850
+ # types) via the authoritative source.
851
+ return [(t, ln) for (t, ln) in result if not is_builtin(t, "cpp")]
852
+ except Exception as exc: # noqa: BLE001
853
+ logger.debug("collect_composition_types_cpp: failed: %r", exc)
854
+ return []
855
+
856
+
857
+ def collect_composition_types_ruby(class_node: Node) -> list[tuple[str, int]]:
858
+ """Collect (held_type_name, line) pairs from a Ruby class node.
859
+
860
+ Scans initialize method body for @ivar = ClassName.new patterns only.
861
+ Ruby has no static type annotations; .new calls on PascalCase constants are the
862
+ only reliable composition signal. No ctor-param pass (params are untyped).
863
+ Deduped by type name. Returns [] on any error. Never raises.
864
+
865
+ WHY only `initialize`, not other methods:
866
+ Assignments in other methods may be local temporaries or conditional re-bindings
867
+ — they don't reliably indicate a stored, owned dependency. Only ivar assignments
868
+ in `initialize` with a `.new` call are a strong signal that the class owns that
869
+ object for its lifetime. Other methods' @ivar assignments are intentionally
870
+ excluded to avoid false composition edges.
871
+ """
872
+ try:
873
+ seen: set[str] = set()
874
+ result: list[tuple[str, int]] = []
875
+
876
+ body = class_node.child_by_field_name("body")
877
+ if body is None:
878
+ return result
879
+
880
+ # Find the initialize method and scan its body for @ivar = ClassName.new.
881
+ for child in body.named_children:
882
+ if child.type != "method":
883
+ continue
884
+ try:
885
+ name_node = child.child_by_field_name("name")
886
+ if name_node is None or _text(name_node).strip() != "initialize":
887
+ continue
888
+ # Found initialize — scan its body for ivar assignments.
889
+ method_body = child.child_by_field_name("body")
890
+ if method_body is None:
891
+ continue
892
+ for stmt in method_body.named_children:
893
+ if stmt.type != "assignment":
894
+ continue
895
+ try:
896
+ right = stmt.child_by_field_name("right")
897
+ if right is None:
898
+ continue
899
+ type_name = _ruby_new_call_class(right)
900
+ if type_name and type_name not in seen:
901
+ seen.add(type_name)
902
+ result.append((type_name, stmt.start_point[0] + 1))
903
+ except Exception: # noqa: BLE001
904
+ pass
905
+ except Exception: # noqa: BLE001
906
+ pass
907
+
908
+ # Post-filter builtins (Array.new/Hash.new/String.new etc. are PascalCase
909
+ # constants but stdlib, not user composition) via the authoritative source.
910
+ return [(t, ln) for (t, ln) in result if not is_builtin(t, "ruby")]
911
+ except Exception as exc: # noqa: BLE001
912
+ logger.debug("collect_composition_types_ruby: failed: %r", exc)
913
+ return []
914
+
915
+
916
+ def collect_composition_types_php(class_node: Node) -> list[tuple[str, int]]:
917
+ """Collect (held_type_name, line) pairs from a PHP class_declaration node.
918
+
919
+ Pass 1: property_declaration with named_type → plain name child.
920
+ Pass 2: __construct simple_parameter nodes with named_type type hint.
921
+ PHP 8+ promoted constructor properties appear in pass 2.
922
+ Deduped by type name. Returns [] on any error. Never raises.
923
+ """
924
+ try:
925
+ seen: set[str] = set()
926
+ result: list[tuple[str, int]] = []
927
+
928
+ body = class_node.child_by_field_name("body")
929
+ if body is None:
930
+ return result
931
+
932
+ # Pass 1: typed property declarations.
933
+ for child in body.named_children:
934
+ if child.type != "property_declaration":
935
+ continue
936
+ try:
937
+ type_name: str | None = None
938
+ for pc in child.children:
939
+ if pc.type == "named_type":
940
+ for tc in pc.children:
941
+ if tc.type == "name":
942
+ type_name = _text(tc).strip()
943
+ break
944
+ break
945
+ if not type_name or not type_name[0].isupper():
946
+ continue
947
+ if type_name not in seen:
948
+ seen.add(type_name)
949
+ result.append((type_name, child.start_point[0] + 1))
950
+ except Exception: # noqa: BLE001
951
+ pass
952
+
953
+ # Pass 2: constructor parameter type hints (__construct).
954
+ for child in body.named_children:
955
+ if child.type != "method_declaration":
956
+ continue
957
+ try:
958
+ name_node = child.child_by_field_name("name")
959
+ if name_node is None or _text(name_node).strip() != "__construct":
960
+ continue
961
+ params = child.child_by_field_name("parameters")
962
+ if params is None:
963
+ continue
964
+ for param in params.named_children:
965
+ if param.type != "simple_parameter":
966
+ continue
967
+ type_node = None
968
+ for pc in param.children:
969
+ if pc.type == "named_type":
970
+ type_node = pc
971
+ break
972
+ if type_node is None:
973
+ continue
974
+ type_name = None
975
+ for tc in type_node.children:
976
+ if tc.type == "name":
977
+ type_name = _text(tc).strip()
978
+ break
979
+ if not type_name or not type_name[0].isupper():
980
+ continue
981
+ if type_name not in seen:
982
+ seen.add(type_name)
983
+ result.append((type_name, param.start_point[0] + 1))
984
+ except Exception: # noqa: BLE001
985
+ pass
986
+
987
+ # Post-filter builtins via the authoritative source (PHP type hints can name
988
+ # built-in classes like 'DateTime'/'ArrayObject').
989
+ return [(t, ln) for (t, ln) in result if not is_builtin(t, "php")]
990
+ except Exception as exc: # noqa: BLE001
991
+ logger.debug("collect_composition_types_php: failed: %r", exc)
992
+ return []