seam-code 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. seam/__init__.py +3 -0
  2. seam/_data/schema.sql +225 -0
  3. seam/_web/assets/index-BL_tqprR.js +216 -0
  4. seam/_web/assets/index-GTKUhVyD.css +1 -0
  5. seam/_web/index.html +13 -0
  6. seam/analysis/__init__.py +14 -0
  7. seam/analysis/affected.py +254 -0
  8. seam/analysis/builtins.py +966 -0
  9. seam/analysis/byte_budget.py +217 -0
  10. seam/analysis/changes.py +709 -0
  11. seam/analysis/cluster_naming.py +260 -0
  12. seam/analysis/clustering.py +216 -0
  13. seam/analysis/confidence.py +699 -0
  14. seam/analysis/embeddings.py +195 -0
  15. seam/analysis/flows.py +708 -0
  16. seam/analysis/impact.py +444 -0
  17. seam/analysis/imports.py +994 -0
  18. seam/analysis/imports_ext.py +780 -0
  19. seam/analysis/imports_resolve.py +176 -0
  20. seam/analysis/processes.py +453 -0
  21. seam/analysis/relevance.py +155 -0
  22. seam/analysis/rwr.py +129 -0
  23. seam/analysis/staleness.py +328 -0
  24. seam/analysis/steer.py +282 -0
  25. seam/analysis/synthesis.py +253 -0
  26. seam/analysis/synthesis_channels.py +433 -0
  27. seam/analysis/testpaths.py +103 -0
  28. seam/analysis/traversal.py +470 -0
  29. seam/cli/__init__.py +0 -0
  30. seam/cli/install.py +232 -0
  31. seam/cli/main.py +2602 -0
  32. seam/cli/output.py +137 -0
  33. seam/cli/read.py +244 -0
  34. seam/cli/serve.py +145 -0
  35. seam/config.py +551 -0
  36. seam/indexer/__init__.py +0 -0
  37. seam/indexer/cluster_index.py +425 -0
  38. seam/indexer/db.py +496 -0
  39. seam/indexer/embedding_index.py +183 -0
  40. seam/indexer/field_access.py +536 -0
  41. seam/indexer/field_access_c_cpp.py +643 -0
  42. seam/indexer/field_access_ext.py +708 -0
  43. seam/indexer/field_access_ext2.py +408 -0
  44. seam/indexer/field_access_go_rust.py +737 -0
  45. seam/indexer/field_access_php_swift.py +888 -0
  46. seam/indexer/field_access_ts.py +626 -0
  47. seam/indexer/graph.py +321 -0
  48. seam/indexer/graph_c.py +562 -0
  49. seam/indexer/graph_c_cpp.py +39 -0
  50. seam/indexer/graph_common.py +644 -0
  51. seam/indexer/graph_cpp.py +615 -0
  52. seam/indexer/graph_csharp.py +651 -0
  53. seam/indexer/graph_go.py +723 -0
  54. seam/indexer/graph_go_rust.py +39 -0
  55. seam/indexer/graph_java.py +689 -0
  56. seam/indexer/graph_java_csharp.py +38 -0
  57. seam/indexer/graph_php.py +914 -0
  58. seam/indexer/graph_python.py +628 -0
  59. seam/indexer/graph_ruby.py +748 -0
  60. seam/indexer/graph_rust.py +653 -0
  61. seam/indexer/graph_scope_infer.py +902 -0
  62. seam/indexer/graph_scope_infer_ext.py +723 -0
  63. seam/indexer/graph_scope_infer_ext2.py +992 -0
  64. seam/indexer/graph_swift.py +1014 -0
  65. seam/indexer/graph_swift_infer.py +515 -0
  66. seam/indexer/graph_typescript.py +663 -0
  67. seam/indexer/migrations.py +816 -0
  68. seam/indexer/parser.py +204 -0
  69. seam/indexer/pipeline.py +197 -0
  70. seam/indexer/signatures.py +634 -0
  71. seam/indexer/signatures_ext.py +780 -0
  72. seam/indexer/sync.py +287 -0
  73. seam/indexer/synthesis_index.py +291 -0
  74. seam/indexer/tokenize.py +79 -0
  75. seam/installer/__init__.py +67 -0
  76. seam/installer/claude.py +97 -0
  77. seam/installer/codex.py +94 -0
  78. seam/installer/core.py +127 -0
  79. seam/installer/cursor.py +61 -0
  80. seam/installer/guide.py +110 -0
  81. seam/installer/jsonfile.py +85 -0
  82. seam/installer/markdownfile.py +146 -0
  83. seam/installer/tomlfile.py +72 -0
  84. seam/query/__init__.py +0 -0
  85. seam/query/clusters.py +206 -0
  86. seam/query/comments.py +217 -0
  87. seam/query/context.py +293 -0
  88. seam/query/engine.py +940 -0
  89. seam/query/fts.py +328 -0
  90. seam/query/names.py +470 -0
  91. seam/query/pack.py +433 -0
  92. seam/query/semantic.py +339 -0
  93. seam/query/structure.py +727 -0
  94. seam/server/__init__.py +0 -0
  95. seam/server/graph_api.py +437 -0
  96. seam/server/handler_common.py +323 -0
  97. seam/server/impact_handler.py +615 -0
  98. seam/server/mcp.py +556 -0
  99. seam/server/tools.py +697 -0
  100. seam/server/trace_handler.py +184 -0
  101. seam/server/web.py +922 -0
  102. seam/watcher/__init__.py +0 -0
  103. seam/watcher/__main__.py +56 -0
  104. seam/watcher/daemon.py +237 -0
  105. seam_code-0.3.0.dist-info/METADATA +318 -0
  106. seam_code-0.3.0.dist-info/RECORD +109 -0
  107. seam_code-0.3.0.dist-info/WHEEL +4 -0
  108. seam_code-0.3.0.dist-info/entry_points.txt +2 -0
  109. seam_code-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,723 @@
1
+ """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