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
seam/indexer/graph_go.py
ADDED
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
"""Go symbol, edge, and comment extraction from tree-sitter ASTs.
|
|
2
|
+
|
|
3
|
+
LAYER: imports from graph_common (leaf), graph_scope_infer_ext (leaf), field_access (leaf) —
|
|
4
|
+
never from graph.py.
|
|
5
|
+
|
|
6
|
+
LAYERING:
|
|
7
|
+
graph_common (leaf — no seam deps)
|
|
8
|
+
field_access (leaf — field-access read/write classification + field symbols)
|
|
9
|
+
↑
|
|
10
|
+
graph_go (this file)
|
|
11
|
+
↑
|
|
12
|
+
graph_go_rust (thin re-exporter; graph.py imports from there)
|
|
13
|
+
|
|
14
|
+
WHY split from graph_go_rust.py: graph_go_rust.py exceeded 1000 lines after Tier B additions.
|
|
15
|
+
Go and Rust are now each large enough to stand alone.
|
|
16
|
+
|
|
17
|
+
All extractor functions follow the same contract:
|
|
18
|
+
- Accept a tree-sitter Node + filepath.
|
|
19
|
+
- Return a list — never raise, never return None.
|
|
20
|
+
- Edges carry confidence='INFERRED' by default.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from tree_sitter import Node
|
|
27
|
+
|
|
28
|
+
import seam.config as config
|
|
29
|
+
from seam.indexer.field_access import (
|
|
30
|
+
collect_field_symbols_go,
|
|
31
|
+
extract_field_accesses_go,
|
|
32
|
+
)
|
|
33
|
+
from seam.indexer.graph_common import (
|
|
34
|
+
Comment,
|
|
35
|
+
Edge,
|
|
36
|
+
Symbol,
|
|
37
|
+
_block_comment_lines,
|
|
38
|
+
_find_enclosing_function,
|
|
39
|
+
_go_recv_type_name,
|
|
40
|
+
_make_symbol,
|
|
41
|
+
_match_marker,
|
|
42
|
+
_node_name,
|
|
43
|
+
_text,
|
|
44
|
+
)
|
|
45
|
+
from seam.indexer.graph_scope_infer_ext import (
|
|
46
|
+
collect_composition_types_go,
|
|
47
|
+
collect_param_types_go,
|
|
48
|
+
record_go_local_types,
|
|
49
|
+
record_go_param_types,
|
|
50
|
+
resolve_receiver_type_ext,
|
|
51
|
+
)
|
|
52
|
+
from seam.indexer.signatures import extract_node_fields
|
|
53
|
+
|
|
54
|
+
logger = logging.getLogger(__name__)
|
|
55
|
+
|
|
56
|
+
# ── Doc-comment helper ─────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _go_doc_comment(decl_node: Node) -> str | None:
|
|
60
|
+
"""Capture a Go doc-comment: contiguous // lines immediately above the decl.
|
|
61
|
+
|
|
62
|
+
Walk prev_sibling collecting 'comment' nodes where each successive pair is
|
|
63
|
+
row-adjacent (no blank line gap). Stops at first non-comment or gap.
|
|
64
|
+
Joins lines in source order, stripping '//' prefix from each.
|
|
65
|
+
"""
|
|
66
|
+
lines: list[str] = []
|
|
67
|
+
current = decl_node.prev_sibling
|
|
68
|
+
|
|
69
|
+
while current is not None and current.type == "comment":
|
|
70
|
+
raw = _text(current)
|
|
71
|
+
if not raw.startswith("//"):
|
|
72
|
+
break
|
|
73
|
+
# Go comment nodes do NOT include trailing newline, so adjacency rule:
|
|
74
|
+
# comment's end_row + 1 == next_node's start_row.
|
|
75
|
+
next_node = current.next_sibling
|
|
76
|
+
if next_node is not None:
|
|
77
|
+
end_row = current.end_point[0]
|
|
78
|
+
next_start_row = next_node.start_point[0]
|
|
79
|
+
if end_row + 1 != next_start_row:
|
|
80
|
+
break
|
|
81
|
+
body = raw[2:].strip()
|
|
82
|
+
lines.append(body)
|
|
83
|
+
current = current.prev_sibling
|
|
84
|
+
|
|
85
|
+
if not lines:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
return "\n".join(reversed(lines))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ── Go extraction ──────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _extract_symbols_go(root: Node, filepath: Path) -> list[Symbol]:
|
|
95
|
+
"""Walk a Go AST and extract function, method, struct, interface, type, and field symbols.
|
|
96
|
+
|
|
97
|
+
Kind mapping (per spec, existing 5 kinds only for non-field symbols):
|
|
98
|
+
function_declaration → function
|
|
99
|
+
method_declaration → method (qualified as 'Recv.Name', *T normalized to T,
|
|
100
|
+
generic receivers Repo[T] → Repo)
|
|
101
|
+
type_spec struct_type → class
|
|
102
|
+
type_spec interface_type → interface
|
|
103
|
+
type_spec other → type
|
|
104
|
+
type_alias → type
|
|
105
|
+
|
|
106
|
+
A3 Slice 3: when SEAM_FIELD_ACCESS_EDGES='on', also emits kind='field' symbols
|
|
107
|
+
for each struct_type field_declaration, using collect_field_symbols_go.
|
|
108
|
+
"""
|
|
109
|
+
symbols: list[Symbol] = []
|
|
110
|
+
file_str = str(filepath)
|
|
111
|
+
field_access_on = config.SEAM_FIELD_ACCESS_EDGES == "on"
|
|
112
|
+
|
|
113
|
+
def _walk(node: Node) -> None:
|
|
114
|
+
if node.type == "function_declaration":
|
|
115
|
+
name = _node_name(node)
|
|
116
|
+
if name:
|
|
117
|
+
doc = _go_doc_comment(node)
|
|
118
|
+
fields = extract_node_fields(
|
|
119
|
+
node, "go", qualified_name=name, max_signature_len=config.SEAM_MAX_SIGNATURE_LEN
|
|
120
|
+
)
|
|
121
|
+
symbols.append(
|
|
122
|
+
_make_symbol(
|
|
123
|
+
name,
|
|
124
|
+
"function",
|
|
125
|
+
file_str,
|
|
126
|
+
node,
|
|
127
|
+
doc,
|
|
128
|
+
signature=fields["signature"],
|
|
129
|
+
decorators=fields["decorators"],
|
|
130
|
+
is_exported=fields["is_exported"],
|
|
131
|
+
visibility=fields["visibility"],
|
|
132
|
+
qualified_name=name,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
elif node.type == "method_declaration":
|
|
137
|
+
method_name = _node_name(node)
|
|
138
|
+
recv_name = _go_recv_type_name(node)
|
|
139
|
+
if method_name and recv_name:
|
|
140
|
+
qualified = f"{recv_name}.{method_name}"
|
|
141
|
+
doc = _go_doc_comment(node)
|
|
142
|
+
fields = extract_node_fields(
|
|
143
|
+
node,
|
|
144
|
+
"go",
|
|
145
|
+
qualified_name=qualified,
|
|
146
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
147
|
+
)
|
|
148
|
+
symbols.append(
|
|
149
|
+
_make_symbol(
|
|
150
|
+
qualified,
|
|
151
|
+
"method",
|
|
152
|
+
file_str,
|
|
153
|
+
node,
|
|
154
|
+
doc,
|
|
155
|
+
signature=fields["signature"],
|
|
156
|
+
decorators=fields["decorators"],
|
|
157
|
+
is_exported=fields["is_exported"],
|
|
158
|
+
visibility=fields["visibility"],
|
|
159
|
+
qualified_name=qualified,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
elif method_name:
|
|
163
|
+
# Receiver parse failed — emit as plain function to avoid silent drop.
|
|
164
|
+
doc = _go_doc_comment(node)
|
|
165
|
+
fields = extract_node_fields(
|
|
166
|
+
node,
|
|
167
|
+
"go",
|
|
168
|
+
qualified_name=method_name,
|
|
169
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
170
|
+
)
|
|
171
|
+
symbols.append(
|
|
172
|
+
_make_symbol(
|
|
173
|
+
method_name,
|
|
174
|
+
"function",
|
|
175
|
+
file_str,
|
|
176
|
+
node,
|
|
177
|
+
doc,
|
|
178
|
+
signature=fields["signature"],
|
|
179
|
+
decorators=fields["decorators"],
|
|
180
|
+
is_exported=fields["is_exported"],
|
|
181
|
+
visibility=fields["visibility"],
|
|
182
|
+
qualified_name=method_name,
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
elif node.type == "type_declaration":
|
|
187
|
+
for child in node.named_children:
|
|
188
|
+
if child.type == "type_spec":
|
|
189
|
+
_handle_go_type_spec(child, node, file_str, symbols)
|
|
190
|
+
# A3: emit field symbols for struct types when feature is on.
|
|
191
|
+
# WHY here: type_spec holds both the name and the struct_type node.
|
|
192
|
+
if field_access_on:
|
|
193
|
+
_go_emit_field_symbols(child, file_str, symbols)
|
|
194
|
+
elif child.type == "type_alias":
|
|
195
|
+
name_node = child.child_by_field_name("name")
|
|
196
|
+
if name_node:
|
|
197
|
+
type_name = _text(name_node)
|
|
198
|
+
doc = _go_doc_comment(node)
|
|
199
|
+
fields = extract_node_fields(
|
|
200
|
+
node,
|
|
201
|
+
"go",
|
|
202
|
+
qualified_name=type_name,
|
|
203
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
204
|
+
)
|
|
205
|
+
symbols.append(
|
|
206
|
+
_make_symbol(
|
|
207
|
+
type_name,
|
|
208
|
+
"type",
|
|
209
|
+
file_str,
|
|
210
|
+
node,
|
|
211
|
+
doc,
|
|
212
|
+
signature=fields["signature"],
|
|
213
|
+
decorators=fields["decorators"],
|
|
214
|
+
is_exported=fields["is_exported"],
|
|
215
|
+
visibility=fields["visibility"],
|
|
216
|
+
qualified_name=type_name,
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
else:
|
|
221
|
+
for child in node.children:
|
|
222
|
+
_walk(child)
|
|
223
|
+
|
|
224
|
+
for child in root.children:
|
|
225
|
+
_walk(child)
|
|
226
|
+
|
|
227
|
+
return symbols
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _go_emit_field_symbols(
|
|
231
|
+
type_spec: Node,
|
|
232
|
+
file_str: str,
|
|
233
|
+
symbols: list[Symbol],
|
|
234
|
+
) -> None:
|
|
235
|
+
"""Emit kind='field' symbols for all fields in a Go struct type_spec.
|
|
236
|
+
|
|
237
|
+
Called when SEAM_FIELD_ACCESS_EDGES='on'. Walks the type_spec's 'type' child
|
|
238
|
+
(which must be a struct_type) and calls collect_field_symbols_go to get the
|
|
239
|
+
(qualified_field, line) pairs, then appends Symbol entries.
|
|
240
|
+
|
|
241
|
+
Never raises (backstop try/except).
|
|
242
|
+
"""
|
|
243
|
+
try:
|
|
244
|
+
name_node = type_spec.child_by_field_name("name")
|
|
245
|
+
type_node = type_spec.child_by_field_name("type")
|
|
246
|
+
if name_node is None or type_node is None:
|
|
247
|
+
return
|
|
248
|
+
if type_node.type != "struct_type":
|
|
249
|
+
# WHY skip non-struct type_specs: interface_type and other named types
|
|
250
|
+
# have no stored fields — only struct_type has a field_declaration_list.
|
|
251
|
+
# Interfaces define method signatures, not data, so no field symbols.
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
struct_name = _text(name_node).strip()
|
|
255
|
+
if not struct_name:
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
for qualified_field, field_line in collect_field_symbols_go(type_node, struct_name):
|
|
259
|
+
symbols.append(Symbol(
|
|
260
|
+
name=qualified_field,
|
|
261
|
+
kind="field",
|
|
262
|
+
file=file_str,
|
|
263
|
+
start_line=field_line,
|
|
264
|
+
end_line=field_line,
|
|
265
|
+
docstring=None,
|
|
266
|
+
signature=None,
|
|
267
|
+
decorators=[],
|
|
268
|
+
is_exported=None,
|
|
269
|
+
visibility=None,
|
|
270
|
+
qualified_name=qualified_field,
|
|
271
|
+
))
|
|
272
|
+
except Exception as exc: # noqa: BLE001
|
|
273
|
+
logger.debug("_go_emit_field_symbols: failed: %r", exc)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _handle_go_type_spec(
|
|
277
|
+
type_spec: Node, decl_node: Node, file_str: str, symbols: list[Symbol]
|
|
278
|
+
) -> None:
|
|
279
|
+
"""Classify a Go type_spec node and append the appropriate symbol.
|
|
280
|
+
|
|
281
|
+
The doc-comment lives above the parent type_declaration (decl_node).
|
|
282
|
+
Phase 4: extract enrichment fields from the parent type_declaration node.
|
|
283
|
+
"""
|
|
284
|
+
name_node = type_spec.child_by_field_name("name")
|
|
285
|
+
if name_node is None:
|
|
286
|
+
return
|
|
287
|
+
name = _text(name_node)
|
|
288
|
+
|
|
289
|
+
type_node = type_spec.child_by_field_name("type")
|
|
290
|
+
doc = _go_doc_comment(decl_node)
|
|
291
|
+
|
|
292
|
+
fields = extract_node_fields(
|
|
293
|
+
decl_node, "go", qualified_name=name, max_signature_len=config.SEAM_MAX_SIGNATURE_LEN
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
if type_node is None:
|
|
297
|
+
kind = "type"
|
|
298
|
+
elif type_node.type == "struct_type":
|
|
299
|
+
kind = "class"
|
|
300
|
+
elif type_node.type == "interface_type":
|
|
301
|
+
kind = "interface"
|
|
302
|
+
else:
|
|
303
|
+
kind = "type"
|
|
304
|
+
|
|
305
|
+
symbols.append(
|
|
306
|
+
_make_symbol(
|
|
307
|
+
name,
|
|
308
|
+
kind,
|
|
309
|
+
file_str,
|
|
310
|
+
decl_node,
|
|
311
|
+
doc,
|
|
312
|
+
signature=fields["signature"],
|
|
313
|
+
decorators=fields["decorators"],
|
|
314
|
+
is_exported=fields["is_exported"],
|
|
315
|
+
visibility=fields["visibility"],
|
|
316
|
+
qualified_name=name,
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _extract_edges_go(root: Node, filepath: Path) -> list[Edge]:
|
|
322
|
+
"""Extract import, call, holds, reads, and writes edges from a Go AST.
|
|
323
|
+
|
|
324
|
+
Import heuristic:
|
|
325
|
+
import "pkg/path" → target = last path segment ('path')
|
|
326
|
+
import ( "pkg/path" ... ) → one edge per import_spec
|
|
327
|
+
|
|
328
|
+
Call heuristic:
|
|
329
|
+
call_expression where 'function' field is an identifier → bare call → target = identifier
|
|
330
|
+
call_expression where 'function' field is a selector_expression → recv.Method() →
|
|
331
|
+
receiver = operand text, target = method name.
|
|
332
|
+
|
|
333
|
+
Tier B B5: when SEAM_TYPE_INFERENCE is on, selector-expression calls are resolved to
|
|
334
|
+
'Type.method' qualified targets by looking up the receiver identifier in the per-function
|
|
335
|
+
scope map (params + locals). The scope map is rebuilt at each function/method entry.
|
|
336
|
+
|
|
337
|
+
Slice #78: when SEAM_COMPOSITION_EDGES is on, struct_type declarations emit
|
|
338
|
+
holds edges for each plain user-type field (pointer fields have * stripped).
|
|
339
|
+
|
|
340
|
+
A3 Slice 3: when SEAM_FIELD_ACCESS_EDGES is on, selector_expression nodes that are
|
|
341
|
+
NOT in call position emit reads/writes edges for struct field accesses.
|
|
342
|
+
"""
|
|
343
|
+
edges: list[Edge] = []
|
|
344
|
+
file_str = str(filepath)
|
|
345
|
+
file_stem = filepath.stem
|
|
346
|
+
infer = config.SEAM_TYPE_INFERENCE == "on"
|
|
347
|
+
composition_on = config.SEAM_COMPOSITION_EDGES == "on"
|
|
348
|
+
field_access_on = config.SEAM_FIELD_ACCESS_EDGES == "on"
|
|
349
|
+
param_edges_on = config.SEAM_PARAM_EDGES == "on"
|
|
350
|
+
|
|
351
|
+
def _walk(node: Node, var_types: dict[str, str]) -> None:
|
|
352
|
+
ntype = node.type
|
|
353
|
+
|
|
354
|
+
# Slice #78: emit composition (holds) edges for Go structs.
|
|
355
|
+
# WHY here: type_declaration wraps type_spec which contains the struct_type.
|
|
356
|
+
# We handle it at type_declaration level to get the struct name from the
|
|
357
|
+
# type_spec.name field, then pass the struct_type node to the collector.
|
|
358
|
+
if ntype == "type_declaration" and composition_on:
|
|
359
|
+
_handle_go_struct_holds(node, file_str, edges)
|
|
360
|
+
# Still recurse into children so nested composites are also visited.
|
|
361
|
+
for child in node.children:
|
|
362
|
+
_walk(child, var_types)
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
if ntype == "import_declaration":
|
|
366
|
+
_handle_go_import(node, file_str, file_stem, edges)
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
if ntype in ("function_declaration", "method_declaration"):
|
|
370
|
+
# Start a fresh scope for each function/method.
|
|
371
|
+
# WHY new dict: Go has no class-level field pre-scan equivalent (fields live
|
|
372
|
+
# on the struct_item, not the method/function). Each function gets only its
|
|
373
|
+
# parameter bindings (and locals accumulated below). A fresh dict prevents
|
|
374
|
+
# bindings from leaking between sibling functions at the same nesting level.
|
|
375
|
+
new_types: dict[str, str] = {}
|
|
376
|
+
if infer:
|
|
377
|
+
record_go_param_types(node, new_types)
|
|
378
|
+
# A3: Also record param types for field-access resolution even when
|
|
379
|
+
# SEAM_TYPE_INFERENCE is off. WHY: field-access receiver resolution
|
|
380
|
+
# uses the same var_types dict; we need the receiver param to be bound
|
|
381
|
+
# (e.g. 'r' → 'Account') for qualified target emission.
|
|
382
|
+
if field_access_on and not infer:
|
|
383
|
+
record_go_param_types(node, new_types)
|
|
384
|
+
# 'uses' edges: function/method references plain user types as params.
|
|
385
|
+
if param_edges_on:
|
|
386
|
+
if ntype == "function_declaration":
|
|
387
|
+
uses_src: str | None = _node_name(node) or None
|
|
388
|
+
else:
|
|
389
|
+
_mname = _node_name(node)
|
|
390
|
+
_recv = _go_recv_type_name(node)
|
|
391
|
+
uses_src = f"{_recv}.{_mname}" if (_mname and _recv) else None
|
|
392
|
+
if uses_src:
|
|
393
|
+
for ptype, pline in collect_param_types_go(node):
|
|
394
|
+
edges.append(Edge(
|
|
395
|
+
source=uses_src, target=ptype, kind="uses",
|
|
396
|
+
file=file_str, line=pline, confidence="INFERRED", receiver=None,
|
|
397
|
+
))
|
|
398
|
+
for child in node.children:
|
|
399
|
+
_walk(child, new_types)
|
|
400
|
+
# A3: emit field-access (reads/writes) edges for this function body.
|
|
401
|
+
# Done AFTER the body walk so that record_go_local_types has accumulated
|
|
402
|
+
# all local bindings into new_types (incremental during the walk above).
|
|
403
|
+
# WHY separate pass: same reasoning as Python/TS — fully-populated
|
|
404
|
+
# var_types gives better receiver resolution.
|
|
405
|
+
if field_access_on:
|
|
406
|
+
_go_emit_field_access_edges(node, file_str, new_types, edges)
|
|
407
|
+
return
|
|
408
|
+
|
|
409
|
+
if infer and ntype in (
|
|
410
|
+
"short_var_declaration",
|
|
411
|
+
"var_declaration",
|
|
412
|
+
"var_spec",
|
|
413
|
+
"assignment_statement",
|
|
414
|
+
):
|
|
415
|
+
record_go_local_types(node, var_types)
|
|
416
|
+
|
|
417
|
+
# Tier B B6: composite_literal (Foo{...}) → instantiates edge.
|
|
418
|
+
if ntype == "composite_literal":
|
|
419
|
+
type_child = node.child_by_field_name("type")
|
|
420
|
+
if type_child is None:
|
|
421
|
+
type_child = next(
|
|
422
|
+
(c for c in node.children if c.type == "type_identifier"), None
|
|
423
|
+
)
|
|
424
|
+
if type_child is not None and type_child.type == "type_identifier":
|
|
425
|
+
type_name = _text(type_child)
|
|
426
|
+
if type_name:
|
|
427
|
+
source = _find_enclosing_function(node, "go")
|
|
428
|
+
if source is not None:
|
|
429
|
+
edges.append(
|
|
430
|
+
Edge(
|
|
431
|
+
source=source,
|
|
432
|
+
target=type_name,
|
|
433
|
+
kind="instantiates",
|
|
434
|
+
file=file_str,
|
|
435
|
+
line=node.start_point[0] + 1,
|
|
436
|
+
confidence="INFERRED",
|
|
437
|
+
receiver=None,
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
for child in node.children:
|
|
441
|
+
_walk(child, var_types)
|
|
442
|
+
return
|
|
443
|
+
|
|
444
|
+
if ntype == "call_expression":
|
|
445
|
+
func_child = node.child_by_field_name("function")
|
|
446
|
+
callee_name: str | None = None
|
|
447
|
+
recv_text: str | None = None
|
|
448
|
+
|
|
449
|
+
if func_child and func_child.type == "identifier":
|
|
450
|
+
callee_name = _text(func_child)
|
|
451
|
+
elif func_child and func_child.type == "selector_expression":
|
|
452
|
+
operand = func_child.child_by_field_name("operand")
|
|
453
|
+
field = func_child.child_by_field_name("field")
|
|
454
|
+
if field is not None and field.type == "field_identifier":
|
|
455
|
+
callee_name = _text(field)
|
|
456
|
+
if operand is not None:
|
|
457
|
+
recv_text = _text(operand)
|
|
458
|
+
|
|
459
|
+
if callee_name:
|
|
460
|
+
final_target = callee_name
|
|
461
|
+
if infer and recv_text is not None:
|
|
462
|
+
# Go: pass frozenset() (empty) as self_names because Go has no
|
|
463
|
+
# universal 'self' keyword. The receiver variable name is set by
|
|
464
|
+
# the programmer (e.g. 'r', 's', 'c'). Receiver type comes purely
|
|
465
|
+
# from param type bindings (record_go_param_types), not from a
|
|
466
|
+
# conventional self-alias. Passing empty frozenset means no
|
|
467
|
+
# receiver text is treated as "this class instance" — each variable
|
|
468
|
+
# must be explicitly bound. Conservatism contract: no binding → None.
|
|
469
|
+
resolved_type = resolve_receiver_type_ext(
|
|
470
|
+
recv_text, None, var_types, frozenset()
|
|
471
|
+
)
|
|
472
|
+
if resolved_type:
|
|
473
|
+
final_target = f"{resolved_type}.{callee_name}"
|
|
474
|
+
|
|
475
|
+
source = _find_enclosing_function(node, "go")
|
|
476
|
+
if source is not None:
|
|
477
|
+
edges.append(
|
|
478
|
+
Edge(
|
|
479
|
+
source=source,
|
|
480
|
+
target=final_target,
|
|
481
|
+
kind="call",
|
|
482
|
+
file=file_str,
|
|
483
|
+
line=node.start_point[0] + 1,
|
|
484
|
+
confidence="INFERRED",
|
|
485
|
+
receiver=recv_text,
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
for child in node.children:
|
|
490
|
+
_walk(child, var_types)
|
|
491
|
+
|
|
492
|
+
for child in root.children:
|
|
493
|
+
_walk(child, {})
|
|
494
|
+
|
|
495
|
+
return edges
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _handle_go_struct_holds(
|
|
499
|
+
type_decl_node: Node, file_str: str, edges: list[Edge]
|
|
500
|
+
) -> None:
|
|
501
|
+
"""Emit holds edges for each plain user-type field in a Go struct_type.
|
|
502
|
+
|
|
503
|
+
Walks the type_declaration's type_spec children. For each type_spec whose
|
|
504
|
+
'type' field is a struct_type, collects (held_type, line) pairs from
|
|
505
|
+
collect_composition_types_go and emits one holds edge per unique held type.
|
|
506
|
+
|
|
507
|
+
WHY a separate helper (not inline in _walk):
|
|
508
|
+
Keeps _walk lean and mirrors the Python/TS pattern of a dedicated _handle_*
|
|
509
|
+
function for each edge kind. Also called for nested type declarations inside
|
|
510
|
+
function bodies if they arise (defensive programming).
|
|
511
|
+
|
|
512
|
+
Never raises (backstop try/except).
|
|
513
|
+
"""
|
|
514
|
+
try:
|
|
515
|
+
for child in type_decl_node.children:
|
|
516
|
+
if child.type != "type_spec":
|
|
517
|
+
continue
|
|
518
|
+
name_node = child.child_by_field_name("name")
|
|
519
|
+
type_node = child.child_by_field_name("type")
|
|
520
|
+
if name_node is None or type_node is None:
|
|
521
|
+
continue
|
|
522
|
+
if type_node.type != "struct_type":
|
|
523
|
+
continue
|
|
524
|
+
struct_name = _text(name_node).strip()
|
|
525
|
+
if not struct_name:
|
|
526
|
+
continue
|
|
527
|
+
for held_type, held_line in collect_composition_types_go(type_node):
|
|
528
|
+
edges.append(
|
|
529
|
+
Edge(
|
|
530
|
+
source=struct_name,
|
|
531
|
+
target=held_type,
|
|
532
|
+
kind="holds",
|
|
533
|
+
file=file_str,
|
|
534
|
+
line=held_line,
|
|
535
|
+
confidence="INFERRED",
|
|
536
|
+
receiver=None,
|
|
537
|
+
)
|
|
538
|
+
)
|
|
539
|
+
except Exception as exc: # noqa: BLE001
|
|
540
|
+
logger.debug("_handle_go_struct_holds: failed: %r", exc)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _handle_go_import(decl_node: Node, file_str: str, file_stem: str, edges: list[Edge]) -> None:
|
|
544
|
+
"""Extract import edges from a Go import_declaration node.
|
|
545
|
+
|
|
546
|
+
Handles both single imports (import "pkg") and grouped imports.
|
|
547
|
+
Target is the last path segment (e.g. "path/filepath" → "filepath").
|
|
548
|
+
"""
|
|
549
|
+
line = decl_node.start_point[0] + 1
|
|
550
|
+
|
|
551
|
+
def _emit_from_spec(spec: Node) -> None:
|
|
552
|
+
path_node = spec.child_by_field_name("path")
|
|
553
|
+
if path_node is None:
|
|
554
|
+
return
|
|
555
|
+
content_node = next(
|
|
556
|
+
(c for c in path_node.named_children if c.type == "interpreted_string_literal_content"),
|
|
557
|
+
None,
|
|
558
|
+
)
|
|
559
|
+
if content_node is None:
|
|
560
|
+
return
|
|
561
|
+
path_str = _text(content_node)
|
|
562
|
+
target = path_str.split("/")[-1] if path_str else ""
|
|
563
|
+
if target:
|
|
564
|
+
edges.append(
|
|
565
|
+
Edge(
|
|
566
|
+
source=file_stem,
|
|
567
|
+
target=target,
|
|
568
|
+
kind="import",
|
|
569
|
+
file=file_str,
|
|
570
|
+
line=line,
|
|
571
|
+
confidence="INFERRED",
|
|
572
|
+
receiver=None,
|
|
573
|
+
)
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
for child in decl_node.children:
|
|
577
|
+
if child.type == "import_spec":
|
|
578
|
+
_emit_from_spec(child)
|
|
579
|
+
elif child.type == "import_spec_list":
|
|
580
|
+
for spec in child.children:
|
|
581
|
+
if spec.type == "import_spec":
|
|
582
|
+
_emit_from_spec(spec)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _go_extract_receiver_binding(method_node: Node) -> tuple[str | None, str | None]:
|
|
586
|
+
"""Extract (receiver_var_name, receiver_type_name) from a Go method_declaration.
|
|
587
|
+
|
|
588
|
+
Returns (None, None) when the receiver list is absent or unexpected shape.
|
|
589
|
+
The receiver is in the 'receiver' field of the method_declaration node
|
|
590
|
+
(NOT the 'parameters' field, which holds regular function parameters).
|
|
591
|
+
|
|
592
|
+
Example: func (r *Account) M() → ('r', 'Account')
|
|
593
|
+
"""
|
|
594
|
+
try:
|
|
595
|
+
recv = method_node.child_by_field_name("receiver")
|
|
596
|
+
if recv is None:
|
|
597
|
+
return None, None
|
|
598
|
+
for pd in recv.named_children:
|
|
599
|
+
if pd.type == "parameter_declaration":
|
|
600
|
+
# Find the variable name (identifier child)
|
|
601
|
+
for child in pd.children:
|
|
602
|
+
if child.type == "identifier":
|
|
603
|
+
var_name = _text(child).strip()
|
|
604
|
+
# Get the type from _go_recv_type_name logic
|
|
605
|
+
recv_type = _go_recv_type_name(method_node)
|
|
606
|
+
return var_name if var_name else None, recv_type
|
|
607
|
+
return None, None
|
|
608
|
+
except Exception: # noqa: BLE001
|
|
609
|
+
return None, None
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _go_emit_field_access_edges(
|
|
613
|
+
func_node: Node,
|
|
614
|
+
file_str: str,
|
|
615
|
+
var_types: dict[str, str],
|
|
616
|
+
edges: list[Edge],
|
|
617
|
+
) -> None:
|
|
618
|
+
"""Emit reads/writes edges for field accesses in a Go function/method body.
|
|
619
|
+
|
|
620
|
+
Called after the main body walk so that var_types is fully populated.
|
|
621
|
+
Determines the qualified source name and impl_type from the function/method node,
|
|
622
|
+
then delegates to extract_field_accesses_go.
|
|
623
|
+
|
|
624
|
+
For method_declaration:
|
|
625
|
+
- source_fn = 'RecvType.MethodName'
|
|
626
|
+
- impl_type = 'RecvType' (for receiver resolution of 'r.Field' patterns)
|
|
627
|
+
- Also binds the receiver var (e.g. 'r') → 'Account' in var_types so that
|
|
628
|
+
_go_classify_selector can resolve 'r.Balance' → 'Account.Balance'.
|
|
629
|
+
For function_declaration:
|
|
630
|
+
- source_fn = 'FunctionName'
|
|
631
|
+
- impl_type = None
|
|
632
|
+
|
|
633
|
+
WHY bind receiver var: Go has no universal self/this keyword. The receiver
|
|
634
|
+
parameter (e.g. 'r' in 'func (r *Account) M()') is a regular variable binding.
|
|
635
|
+
We must add r → Account to var_types so that resolve_receiver_type_ext can
|
|
636
|
+
resolve it. record_go_param_types only handles the regular function parameters
|
|
637
|
+
(the second parameter_list), NOT the receiver (in the 'receiver' field).
|
|
638
|
+
|
|
639
|
+
Never raises (extract_field_accesses_go is backstopped).
|
|
640
|
+
"""
|
|
641
|
+
ntype = func_node.type
|
|
642
|
+
if ntype == "method_declaration":
|
|
643
|
+
method_name = _node_name(func_node)
|
|
644
|
+
recv_name = _go_recv_type_name(func_node)
|
|
645
|
+
if not method_name:
|
|
646
|
+
return
|
|
647
|
+
source_fn = f"{recv_name}.{method_name}" if recv_name else method_name
|
|
648
|
+
impl_type = recv_name
|
|
649
|
+
|
|
650
|
+
# Bind the receiver variable to the receiver type.
|
|
651
|
+
# WHY: var_types is populated by record_go_param_types for REGULAR params,
|
|
652
|
+
# but the Go receiver is in a separate 'receiver' field and is NOT covered
|
|
653
|
+
# by record_go_param_types. Without this binding, 'r.Balance' where r is
|
|
654
|
+
# the method receiver cannot be resolved to 'Account.Balance'.
|
|
655
|
+
if recv_name:
|
|
656
|
+
recv_var, recv_type = _go_extract_receiver_binding(func_node)
|
|
657
|
+
if recv_var and recv_type and recv_var not in var_types:
|
|
658
|
+
var_types[recv_var] = recv_type
|
|
659
|
+
|
|
660
|
+
elif ntype == "function_declaration":
|
|
661
|
+
func_name = _node_name(func_node)
|
|
662
|
+
if not func_name:
|
|
663
|
+
return
|
|
664
|
+
source_fn = func_name
|
|
665
|
+
impl_type = None
|
|
666
|
+
else:
|
|
667
|
+
return
|
|
668
|
+
|
|
669
|
+
# Find the body block node.
|
|
670
|
+
body = None
|
|
671
|
+
for child in func_node.children:
|
|
672
|
+
if child.type == "block":
|
|
673
|
+
body = child
|
|
674
|
+
break
|
|
675
|
+
if body is None:
|
|
676
|
+
return
|
|
677
|
+
|
|
678
|
+
for _src, target_field, mode, fa_line in extract_field_accesses_go(
|
|
679
|
+
body, source_fn, impl_type, var_types
|
|
680
|
+
):
|
|
681
|
+
edges.append(Edge(
|
|
682
|
+
source=source_fn,
|
|
683
|
+
target=target_field,
|
|
684
|
+
kind=mode,
|
|
685
|
+
file=file_str,
|
|
686
|
+
line=fa_line,
|
|
687
|
+
confidence="INFERRED",
|
|
688
|
+
receiver=None,
|
|
689
|
+
))
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _extract_comments_go(root: Node, filepath: Path) -> list[Comment]:
|
|
693
|
+
"""Walk a Go AST and extract semantic comment markers.
|
|
694
|
+
|
|
695
|
+
Go has a single 'comment' node type for both // and /* */ comments.
|
|
696
|
+
For block comments, every line is scanned using _block_comment_lines.
|
|
697
|
+
"""
|
|
698
|
+
comments: list[Comment] = []
|
|
699
|
+
|
|
700
|
+
def _walk(node: Node) -> None:
|
|
701
|
+
if node.type == "comment":
|
|
702
|
+
raw = _text(node)
|
|
703
|
+
base_row = node.start_point[0] + 1
|
|
704
|
+
if raw.startswith("/*"):
|
|
705
|
+
for offset, body in _block_comment_lines(raw):
|
|
706
|
+
result = _match_marker(body)
|
|
707
|
+
if result is not None:
|
|
708
|
+
marker, text = result
|
|
709
|
+
comments.append(Comment(marker=marker, text=text, line=base_row + offset))
|
|
710
|
+
else:
|
|
711
|
+
body = raw.lstrip("/").strip()
|
|
712
|
+
result = _match_marker(body)
|
|
713
|
+
if result is not None:
|
|
714
|
+
marker, text = result
|
|
715
|
+
comments.append(Comment(marker=marker, text=text, line=base_row))
|
|
716
|
+
|
|
717
|
+
for child in node.children:
|
|
718
|
+
_walk(child)
|
|
719
|
+
|
|
720
|
+
for child in root.children:
|
|
721
|
+
_walk(child)
|
|
722
|
+
|
|
723
|
+
return comments
|