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,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
|
+
|