seam-code 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- seam/__init__.py +3 -0
- seam/_data/schema.sql +225 -0
- seam/_web/assets/index-BL_tqprR.js +216 -0
- seam/_web/assets/index-GTKUhVyD.css +1 -0
- seam/_web/index.html +13 -0
- seam/analysis/__init__.py +14 -0
- seam/analysis/affected.py +254 -0
- seam/analysis/builtins.py +966 -0
- seam/analysis/byte_budget.py +217 -0
- seam/analysis/changes.py +709 -0
- seam/analysis/cluster_naming.py +260 -0
- seam/analysis/clustering.py +216 -0
- seam/analysis/confidence.py +699 -0
- seam/analysis/embeddings.py +195 -0
- seam/analysis/flows.py +708 -0
- seam/analysis/impact.py +444 -0
- seam/analysis/imports.py +994 -0
- seam/analysis/imports_ext.py +780 -0
- seam/analysis/imports_resolve.py +176 -0
- seam/analysis/processes.py +453 -0
- seam/analysis/relevance.py +155 -0
- seam/analysis/rwr.py +129 -0
- seam/analysis/staleness.py +328 -0
- seam/analysis/steer.py +282 -0
- seam/analysis/synthesis.py +253 -0
- seam/analysis/synthesis_channels.py +433 -0
- seam/analysis/testpaths.py +103 -0
- seam/analysis/traversal.py +470 -0
- seam/cli/__init__.py +0 -0
- seam/cli/install.py +232 -0
- seam/cli/main.py +2602 -0
- seam/cli/output.py +137 -0
- seam/cli/read.py +244 -0
- seam/cli/serve.py +145 -0
- seam/config.py +551 -0
- seam/indexer/__init__.py +0 -0
- seam/indexer/cluster_index.py +425 -0
- seam/indexer/db.py +496 -0
- seam/indexer/embedding_index.py +183 -0
- seam/indexer/field_access.py +536 -0
- seam/indexer/field_access_c_cpp.py +643 -0
- seam/indexer/field_access_ext.py +708 -0
- seam/indexer/field_access_ext2.py +408 -0
- seam/indexer/field_access_go_rust.py +737 -0
- seam/indexer/field_access_php_swift.py +888 -0
- seam/indexer/field_access_ts.py +626 -0
- seam/indexer/graph.py +321 -0
- seam/indexer/graph_c.py +562 -0
- seam/indexer/graph_c_cpp.py +39 -0
- seam/indexer/graph_common.py +644 -0
- seam/indexer/graph_cpp.py +615 -0
- seam/indexer/graph_csharp.py +651 -0
- seam/indexer/graph_go.py +723 -0
- seam/indexer/graph_go_rust.py +39 -0
- seam/indexer/graph_java.py +689 -0
- seam/indexer/graph_java_csharp.py +38 -0
- seam/indexer/graph_php.py +914 -0
- seam/indexer/graph_python.py +628 -0
- seam/indexer/graph_ruby.py +748 -0
- seam/indexer/graph_rust.py +653 -0
- seam/indexer/graph_scope_infer.py +902 -0
- seam/indexer/graph_scope_infer_ext.py +723 -0
- seam/indexer/graph_scope_infer_ext2.py +992 -0
- seam/indexer/graph_swift.py +1014 -0
- seam/indexer/graph_swift_infer.py +515 -0
- seam/indexer/graph_typescript.py +663 -0
- seam/indexer/migrations.py +816 -0
- seam/indexer/parser.py +204 -0
- seam/indexer/pipeline.py +197 -0
- seam/indexer/signatures.py +634 -0
- seam/indexer/signatures_ext.py +780 -0
- seam/indexer/sync.py +287 -0
- seam/indexer/synthesis_index.py +291 -0
- seam/indexer/tokenize.py +79 -0
- seam/installer/__init__.py +67 -0
- seam/installer/claude.py +97 -0
- seam/installer/codex.py +94 -0
- seam/installer/core.py +127 -0
- seam/installer/cursor.py +61 -0
- seam/installer/guide.py +110 -0
- seam/installer/jsonfile.py +85 -0
- seam/installer/markdownfile.py +146 -0
- seam/installer/tomlfile.py +72 -0
- seam/query/__init__.py +0 -0
- seam/query/clusters.py +206 -0
- seam/query/comments.py +217 -0
- seam/query/context.py +293 -0
- seam/query/engine.py +940 -0
- seam/query/fts.py +328 -0
- seam/query/names.py +470 -0
- seam/query/pack.py +433 -0
- seam/query/semantic.py +339 -0
- seam/query/structure.py +727 -0
- seam/server/__init__.py +0 -0
- seam/server/graph_api.py +437 -0
- seam/server/handler_common.py +323 -0
- seam/server/impact_handler.py +615 -0
- seam/server/mcp.py +556 -0
- seam/server/tools.py +697 -0
- seam/server/trace_handler.py +184 -0
- seam/server/web.py +922 -0
- seam/watcher/__init__.py +0 -0
- seam/watcher/__main__.py +56 -0
- seam/watcher/daemon.py +237 -0
- seam_code-0.3.0.dist-info/METADATA +318 -0
- seam_code-0.3.0.dist-info/RECORD +109 -0
- seam_code-0.3.0.dist-info/WHEEL +4 -0
- seam_code-0.3.0.dist-info/entry_points.txt +2 -0
- seam_code-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,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 []
|