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,651 @@
1
+ """C# symbol, edge, and comment extraction from tree-sitter ASTs.
2
+
3
+ LAYER: imports from graph_common (leaf) and graph_scope_infer_ext[2] (leaf) — never from graph.py.
4
+
5
+ LAYERING:
6
+ graph_common (leaf — no seam deps)
7
+
8
+ graph_csharp (this file)
9
+
10
+ graph_java_csharp (thin re-exporter; graph.py imports from there)
11
+
12
+ WHY split from graph_java_csharp.py: graph_java_csharp.py exceeded 1000 lines after Tier B
13
+ additions. Java and C# are now each large enough to stand alone.
14
+
15
+ All extractor functions follow the same contract:
16
+ - Accept a tree-sitter Node + filepath.
17
+ - Return a list — never raise, never return None.
18
+ - Edges carry confidence='INFERRED' by default.
19
+ """
20
+
21
+ import logging
22
+ from pathlib import Path
23
+
24
+ from tree_sitter import Node
25
+
26
+ import seam.config as config
27
+ from seam.indexer.field_access_ext import (
28
+ collect_field_symbols_csharp,
29
+ extract_field_accesses_csharp,
30
+ )
31
+ from seam.indexer.graph_common import (
32
+ Comment,
33
+ Edge,
34
+ Symbol,
35
+ _base_type_name,
36
+ _block_comment_lines,
37
+ _find_enclosing_function,
38
+ _make_symbol,
39
+ _match_marker,
40
+ _text,
41
+ )
42
+ from seam.indexer.graph_scope_infer_ext import param_types_via_recorder, resolve_receiver_type_ext
43
+ from seam.indexer.graph_scope_infer_ext2 import (
44
+ _CS_SELF_NAMES,
45
+ collect_composition_types_cs,
46
+ record_cs_local_types,
47
+ record_cs_param_types,
48
+ scan_class_fields_cs,
49
+ )
50
+ from seam.indexer.signatures import extract_node_fields
51
+
52
+ logger = logging.getLogger(__name__)
53
+
54
+
55
+ # ── C# helpers ─────────────────────────────────────────────────────────────────
56
+
57
+
58
+ def _node_name_cs(node: Node) -> str | None:
59
+ """Return text of the 'name' field child, or None if absent."""
60
+ name_node = node.child_by_field_name("name")
61
+ if name_node is None:
62
+ return None
63
+ return _text(name_node)
64
+
65
+
66
+ def _csharp_doc_comment(decl_node: Node) -> str | None:
67
+ """Capture C# XML doc-comment: the nearest contiguous block of /// lines above the declaration.
68
+
69
+ In the C# grammar all comments (///, //, /* */) appear as 'comment' nodes.
70
+ Walk prev_sibling backwards:
71
+ - Skip non-/// 'comment' nodes (e.g. // HACK: ...) to find the nearest
72
+ contiguous block of /// lines.
73
+ - Collect contiguous /// lines into the docstring.
74
+ - Stop at non-comment nodes or when a gap (non-/// comment) breaks the block.
75
+ """
76
+ try:
77
+ lines: list[str] = []
78
+ current = decl_node.prev_sibling
79
+
80
+ # Phase 1: skip any leading non-/// comments to find the tail of the /// block.
81
+ while current is not None and current.type == "comment":
82
+ raw = _text(current)
83
+ if raw.startswith("///"):
84
+ break
85
+ current = current.prev_sibling
86
+
87
+ # Phase 2: collect contiguous /// lines.
88
+ while current is not None and current.type == "comment":
89
+ raw = _text(current)
90
+ if not raw.startswith("///"):
91
+ break
92
+ body = raw[3:].strip()
93
+ lines.append(body)
94
+ current = current.prev_sibling
95
+
96
+ if not lines:
97
+ return None
98
+ return "\n".join(reversed(lines))
99
+ except Exception: # noqa: BLE001
100
+ return None
101
+
102
+
103
+ def _csharp_modifier(decl_node: Node, modifier_type: str = "modifier") -> str | None:
104
+ """Find a modifier string ('public'/'private'/'protected'/'internal') from a C# declaration."""
105
+ try:
106
+ for child in decl_node.children:
107
+ if child.type == modifier_type:
108
+ text = _text(child).strip()
109
+ if text in ("public", "private", "protected", "internal"):
110
+ return text
111
+ except Exception: # noqa: BLE001
112
+ pass
113
+ return None
114
+
115
+
116
+ def _csharp_attributes(decl_node: Node) -> list[str]:
117
+ """Extract C# attribute list texts ([Serializable], [HttpGet], etc.) from a declaration."""
118
+ result: list[str] = []
119
+ try:
120
+ for child in decl_node.children:
121
+ if child.type == "attribute_list":
122
+ text = _text(child).strip()
123
+ if text:
124
+ result.append(text)
125
+ except Exception: # noqa: BLE001
126
+ pass
127
+ return result
128
+
129
+
130
+ # ── C# extraction ──────────────────────────────────────────────────────────────
131
+
132
+
133
+ def _extract_symbols_csharp(root: Node, filepath: Path) -> list[Symbol]:
134
+ """Walk a C# AST and extract class, struct, record, interface, enum, delegate, and method symbols.
135
+
136
+ Kind mapping (Phase 9 spec, closed vocabulary):
137
+ class_declaration / struct_declaration / record_declaration → class
138
+ interface_declaration → interface
139
+ enum_declaration / delegate_declaration → type
140
+ method_declaration / constructor_declaration (inside type) → method (qualified)
141
+
142
+ namespace_declaration and file_scoped_namespace_declaration are TRAVERSED but
143
+ NOT emitted as symbols (per spec — namespace is a container, not a symbol).
144
+ """
145
+ symbols: list[Symbol] = []
146
+ file_str = str(filepath)
147
+ field_access_on = config.SEAM_FIELD_ACCESS_EDGES == "on"
148
+
149
+ def _walk_body(body_node: Node, class_name: str | None = None) -> None:
150
+ """Walk declaration_list children, tracking class context."""
151
+ for child in body_node.named_children:
152
+ _walk(child, class_name)
153
+
154
+ def _walk(node: Node, class_name: str | None = None) -> None:
155
+ """Recursively walk the AST; class_name tracks the enclosing type."""
156
+ try:
157
+ ntype = node.type
158
+
159
+ if ntype in ("namespace_declaration", "file_scoped_namespace_declaration"):
160
+ body = node.child_by_field_name("body")
161
+ if body is not None:
162
+ _walk_body(body)
163
+
164
+ elif ntype in ("class_declaration", "struct_declaration", "record_declaration"):
165
+ name = _node_name_cs(node)
166
+ if name:
167
+ doc = _csharp_doc_comment(node)
168
+ fields = extract_node_fields(
169
+ node,
170
+ "csharp",
171
+ qualified_name=name,
172
+ max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
173
+ )
174
+ symbols.append(
175
+ _make_symbol(
176
+ name,
177
+ "class",
178
+ file_str,
179
+ node,
180
+ doc,
181
+ signature=fields["signature"],
182
+ decorators=fields["decorators"],
183
+ is_exported=fields["is_exported"],
184
+ visibility=fields["visibility"],
185
+ qualified_name=name,
186
+ )
187
+ )
188
+ # A3 Slice 4: emit field symbols for C# class/struct fields and properties.
189
+ if field_access_on:
190
+ for qual_name, field_line in collect_field_symbols_csharp(node, name):
191
+ symbols.append(Symbol(
192
+ name=qual_name,
193
+ kind="field",
194
+ file=file_str,
195
+ start_line=field_line,
196
+ end_line=field_line,
197
+ docstring=None,
198
+ signature=None,
199
+ decorators=[],
200
+ is_exported=None,
201
+ visibility=None,
202
+ qualified_name=qual_name,
203
+ ))
204
+ body = node.child_by_field_name("body")
205
+ if body is not None:
206
+ _walk_body(body, class_name=name)
207
+
208
+ elif ntype == "interface_declaration":
209
+ name = _node_name_cs(node)
210
+ if name:
211
+ doc = _csharp_doc_comment(node)
212
+ fields = extract_node_fields(
213
+ node,
214
+ "csharp",
215
+ qualified_name=name,
216
+ max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
217
+ )
218
+ symbols.append(
219
+ _make_symbol(
220
+ name,
221
+ "interface",
222
+ file_str,
223
+ node,
224
+ doc,
225
+ signature=fields["signature"],
226
+ decorators=fields["decorators"],
227
+ is_exported=fields["is_exported"],
228
+ visibility=fields["visibility"],
229
+ qualified_name=name,
230
+ )
231
+ )
232
+ body = node.child_by_field_name("body")
233
+ if body is not None:
234
+ _walk_body(body, class_name=name)
235
+
236
+ elif ntype in ("enum_declaration", "delegate_declaration"):
237
+ name = _node_name_cs(node)
238
+ if name:
239
+ doc = _csharp_doc_comment(node)
240
+ fields = extract_node_fields(
241
+ node,
242
+ "csharp",
243
+ qualified_name=name,
244
+ max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
245
+ )
246
+ symbols.append(
247
+ _make_symbol(
248
+ name,
249
+ "type",
250
+ file_str,
251
+ node,
252
+ doc,
253
+ signature=fields["signature"],
254
+ decorators=fields["decorators"],
255
+ is_exported=fields["is_exported"],
256
+ visibility=fields["visibility"],
257
+ qualified_name=name,
258
+ )
259
+ )
260
+
261
+ elif (
262
+ ntype in ("method_declaration", "constructor_declaration")
263
+ and class_name is not None
264
+ ):
265
+ name = _node_name_cs(node)
266
+ if name:
267
+ qualified = f"{class_name}.{name}"
268
+ doc = _csharp_doc_comment(node)
269
+ fields = extract_node_fields(
270
+ node,
271
+ "csharp",
272
+ qualified_name=qualified,
273
+ max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
274
+ )
275
+ symbols.append(
276
+ _make_symbol(
277
+ qualified,
278
+ "method",
279
+ file_str,
280
+ node,
281
+ doc,
282
+ signature=fields["signature"],
283
+ decorators=fields["decorators"],
284
+ is_exported=fields["is_exported"],
285
+ visibility=fields["visibility"],
286
+ qualified_name=qualified,
287
+ )
288
+ )
289
+
290
+ else:
291
+ for child in node.named_children:
292
+ _walk(child, class_name)
293
+
294
+ except Exception: # noqa: BLE001
295
+ logger.debug(
296
+ "_extract_symbols_csharp: unhandled exception for node.type=%r file=%s",
297
+ node.type,
298
+ file_str,
299
+ )
300
+
301
+ for child in root.named_children:
302
+ _walk(child)
303
+
304
+ return symbols
305
+
306
+
307
+ def _extract_edges_csharp(root: Node, filepath: Path) -> list[Edge]:
308
+ """Extract import and call edges from a C# AST.
309
+
310
+ Import heuristic:
311
+ using System; → target = 'System' (identifier)
312
+ using System.Collections.Generic → target = 'Generic' (qualified_name.name)
313
+
314
+ Call heuristic:
315
+ invocation_expression where 'function' is an identifier → bare call.
316
+ invocation_expression where 'function' is member_access_expression → obj.Method().
317
+
318
+ Tier B B5: when SEAM_TYPE_INFERENCE is on, member-access calls are resolved to
319
+ 'Type.method' qualified targets using per-function scope (class fields + params +
320
+ local declaration statements).
321
+ """
322
+ edges: list[Edge] = []
323
+ file_str = str(filepath)
324
+ file_stem = filepath.stem
325
+ emit_inheritance = config.SEAM_INHERITANCE_EDGES == "on"
326
+ infer = config.SEAM_TYPE_INFERENCE == "on"
327
+ composition_on = config.SEAM_COMPOSITION_EDGES == "on"
328
+ field_access_on = config.SEAM_FIELD_ACCESS_EDGES == "on"
329
+ param_edges_on = config.SEAM_PARAM_EDGES == "on"
330
+
331
+ def _walk(
332
+ node: Node,
333
+ class_name: str | None,
334
+ class_fields: dict[str, str],
335
+ var_types: dict[str, str],
336
+ ) -> None:
337
+ try:
338
+ ntype = node.type
339
+
340
+ if emit_inheritance and ntype in (
341
+ "class_declaration",
342
+ "struct_declaration",
343
+ "interface_declaration",
344
+ "record_declaration",
345
+ ):
346
+ _handle_csharp_inheritance(node, file_str, edges)
347
+
348
+ if ntype == "using_directive":
349
+ _handle_csharp_using(node, file_str, file_stem, edges)
350
+ return
351
+
352
+ if ntype in (
353
+ "class_declaration",
354
+ "struct_declaration",
355
+ "record_declaration",
356
+ ):
357
+ new_class_name: str | None = None
358
+ cn = node.child_by_field_name("name")
359
+ if cn is not None:
360
+ new_class_name = _text(cn).strip() or None
361
+ # Slice #79: emit holds edges for C# class fields + ctor params.
362
+ if composition_on and new_class_name:
363
+ _handle_cs_class_holds(node, new_class_name, file_str, edges)
364
+ new_fields: dict[str, str] = scan_class_fields_cs(node) if infer else {}
365
+ body = node.child_by_field_name("body")
366
+ if body is not None:
367
+ for child in body.named_children:
368
+ _walk(child, new_class_name, new_fields, dict(new_fields))
369
+ return
370
+
371
+ if ntype in ("method_declaration", "constructor_declaration"):
372
+ new_types: dict[str, str] = dict(class_fields)
373
+ if infer:
374
+ record_cs_param_types(node, new_types)
375
+ # 'uses' edges: method references plain user types as params.
376
+ if param_edges_on and class_name is not None:
377
+ _m = _node_name_cs(node)
378
+ if _m:
379
+ _src = f"{class_name}.{_m}"
380
+ for ptype, pline in param_types_via_recorder(
381
+ node, record_cs_param_types, "csharp"
382
+ ):
383
+ edges.append(Edge(
384
+ source=_src, target=ptype, kind="uses",
385
+ file=file_str, line=pline, confidence="INFERRED", receiver=None,
386
+ ))
387
+ body = node.child_by_field_name("body")
388
+ if body is not None:
389
+ # A3 Slice 4: emit reads/writes field-access edges.
390
+ if field_access_on and class_name is not None:
391
+ method_name = _node_name_cs(node)
392
+ if method_name:
393
+ source_fn = f"{class_name}.{method_name}"
394
+ for src, tgt, mode, line in extract_field_accesses_csharp(
395
+ body, source_fn, class_name, new_types
396
+ ):
397
+ edges.append(Edge(
398
+ source=src,
399
+ target=tgt,
400
+ kind=mode,
401
+ file=file_str,
402
+ line=line,
403
+ confidence="INFERRED",
404
+ receiver=None,
405
+ ))
406
+ for child in body.named_children:
407
+ _walk(child, class_name, class_fields, new_types)
408
+ return
409
+
410
+ if infer and ntype == "local_declaration_statement":
411
+ record_cs_local_types(node, var_types)
412
+
413
+ # Tier B B6: object_creation_expression (new Foo()) → instantiates edge.
414
+ elif ntype == "object_creation_expression":
415
+ type_node = next(
416
+ (c for c in node.children if c.type == "identifier"), None
417
+ )
418
+ if type_node is not None:
419
+ type_name = _text(type_node)
420
+ if type_name:
421
+ source = _find_enclosing_function(node, "csharp")
422
+ if source is not None:
423
+ edges.append(
424
+ Edge(
425
+ source=source,
426
+ target=type_name,
427
+ kind="instantiates",
428
+ file=file_str,
429
+ line=node.start_point[0] + 1,
430
+ confidence="INFERRED",
431
+ receiver=None,
432
+ )
433
+ )
434
+ for child in node.children:
435
+ _walk(child, class_name, class_fields, var_types)
436
+ return
437
+
438
+ elif ntype == "invocation_expression":
439
+ func_node = node.child_by_field_name("function")
440
+ cs_callee: str | None = None
441
+ cs_recv: str | None = None
442
+
443
+ if func_node is not None and func_node.type == "identifier":
444
+ cs_callee = _text(func_node)
445
+ elif func_node is not None and func_node.type == "member_access_expression":
446
+ expr_node = func_node.child_by_field_name("expression")
447
+ name_node = func_node.child_by_field_name("name")
448
+ if name_node is not None and name_node.type == "identifier":
449
+ cs_callee = _text(name_node)
450
+ if expr_node is not None:
451
+ cs_recv = _text(expr_node)
452
+
453
+ if cs_callee is not None:
454
+ final_target = cs_callee
455
+ if infer and cs_recv is not None:
456
+ resolved_type = resolve_receiver_type_ext(
457
+ cs_recv, class_name, var_types, _CS_SELF_NAMES
458
+ )
459
+ if resolved_type:
460
+ final_target = f"{resolved_type}.{cs_callee}"
461
+ source = _find_enclosing_function(node, "csharp")
462
+ if source is not None:
463
+ edges.append(
464
+ Edge(
465
+ source=source,
466
+ target=final_target,
467
+ kind="call",
468
+ file=file_str,
469
+ line=node.start_point[0] + 1,
470
+ confidence="INFERRED",
471
+ receiver=cs_recv,
472
+ )
473
+ )
474
+
475
+ for child in node.children:
476
+ _walk(child, class_name, class_fields, var_types)
477
+
478
+ except Exception: # noqa: BLE001
479
+ logger.debug(
480
+ "_extract_edges_csharp: unhandled exception for node.type=%r file=%s",
481
+ node.type,
482
+ file_str,
483
+ )
484
+
485
+ for child in root.children:
486
+ _walk(child, None, {}, {})
487
+
488
+ return edges
489
+
490
+
491
+ def _handle_cs_class_holds(
492
+ class_node: Node, class_name: str, file_str: str, edges: list[Edge]
493
+ ) -> None:
494
+ """Emit holds edges for each plain user-type field/ctor-param in a C# class.
495
+
496
+ Delegates to collect_composition_types_cs for (held_type, line) pairs and
497
+ emits one Edge per unique pair. Never raises (backstop try/except).
498
+ """
499
+ try:
500
+ for held_type, held_line in collect_composition_types_cs(class_node):
501
+ edges.append(
502
+ Edge(
503
+ source=class_name,
504
+ target=held_type,
505
+ kind="holds",
506
+ file=file_str,
507
+ line=held_line,
508
+ confidence="INFERRED",
509
+ receiver=None,
510
+ )
511
+ )
512
+ except Exception as exc: # noqa: BLE001
513
+ logger.debug("_handle_cs_class_holds: failed: %r", exc)
514
+
515
+
516
+ def _handle_csharp_inheritance(decl_node: Node, file_str: str, edges: list[Edge]) -> None:
517
+ """Emit extends edges from a C# type declaration's base_list.
518
+
519
+ C# does NOT syntactically distinguish a base class from implemented interfaces
520
+ (both live in `base_list` after the colon), so all base entries are emitted as
521
+ 'extends' edges. Never raises.
522
+ """
523
+ name_node = decl_node.child_by_field_name("name")
524
+ if name_node is None:
525
+ return
526
+ src_name = _text(name_node)
527
+ line = decl_node.start_point[0] + 1
528
+
529
+ for child in decl_node.children:
530
+ if child.type == "base_list":
531
+ for base in child.named_children:
532
+ target = _base_type_name(base)
533
+ if target:
534
+ edges.append(
535
+ Edge(
536
+ source=src_name,
537
+ target=target,
538
+ kind="extends",
539
+ file=file_str,
540
+ line=line,
541
+ confidence="INFERRED",
542
+ receiver=None,
543
+ )
544
+ )
545
+
546
+
547
+ def _handle_csharp_using(
548
+ using_node: Node, file_str: str, file_stem: str, edges: list[Edge]
549
+ ) -> None:
550
+ """Emit an import edge from a C# using_directive node.
551
+
552
+ Extracts the last segment of the namespace:
553
+ using System → 'System' (identifier)
554
+ using System.Collections.Generic → 'Generic' (qualified_name.name)
555
+ using Foo = System.Collections.Generic → 'Generic' (alias form, skip alias identifier)
556
+ """
557
+ line = using_node.start_point[0] + 1
558
+ named = using_node.named_children
559
+ has_identifier = any(c.type == "identifier" for c in named)
560
+ has_qualified = any(c.type == "qualified_name" for c in named)
561
+ if has_identifier and has_qualified:
562
+ # Alias form: emit only the qualified_name segment, not the alias.
563
+ for child in named:
564
+ if child.type == "qualified_name":
565
+ target = _csharp_using_last_segment(child)
566
+ if target:
567
+ edges.append(
568
+ Edge(
569
+ source=file_stem,
570
+ target=target,
571
+ kind="import",
572
+ file=file_str,
573
+ line=line,
574
+ confidence="INFERRED",
575
+ receiver=None,
576
+ )
577
+ )
578
+ return
579
+
580
+ for child in named:
581
+ target = _csharp_using_last_segment(child)
582
+ if target:
583
+ edges.append(
584
+ Edge(
585
+ source=file_stem,
586
+ target=target,
587
+ kind="import",
588
+ file=file_str,
589
+ line=line,
590
+ confidence="INFERRED",
591
+ receiver=None,
592
+ )
593
+ )
594
+
595
+
596
+ def _csharp_using_last_segment(node: Node) -> str | None:
597
+ """Extract the rightmost segment from a C# using_directive's name node."""
598
+ if node.type == "identifier":
599
+ return _text(node)
600
+ if node.type == "qualified_name":
601
+ name_node = node.child_by_field_name("name")
602
+ if name_node is not None:
603
+ return _text(name_node)
604
+ return None
605
+
606
+
607
+ def _extract_comments_csharp(root: Node, filepath: Path) -> list[Comment]:
608
+ """Walk a C# AST and extract semantic comment markers (WHY/HACK/NOTE/TODO/FIXME).
609
+
610
+ C# comment node types:
611
+ 'comment' — covers //, ///, and /* */ comments (all under the same node type).
612
+ """
613
+ comments: list[Comment] = []
614
+
615
+ def _walk(node: Node) -> None:
616
+ try:
617
+ if node.type == "comment":
618
+ raw = _text(node)
619
+ base_row = node.start_point[0] + 1
620
+
621
+ if raw.startswith("/*"):
622
+ for offset, body in _block_comment_lines(raw):
623
+ result = _match_marker(body)
624
+ if result is not None:
625
+ marker, text = result
626
+ comments.append(
627
+ Comment(marker=marker, text=text, line=base_row + offset)
628
+ )
629
+ elif raw.startswith("///"):
630
+ body = raw[3:].strip()
631
+ result = _match_marker(body)
632
+ if result is not None:
633
+ marker, text = result
634
+ comments.append(Comment(marker=marker, text=text, line=base_row))
635
+ elif raw.startswith("//"):
636
+ body = raw[2:].strip()
637
+ result = _match_marker(body)
638
+ if result is not None:
639
+ marker, text = result
640
+ comments.append(Comment(marker=marker, text=text, line=base_row))
641
+
642
+ for child in node.children:
643
+ _walk(child)
644
+
645
+ except Exception: # noqa: BLE001
646
+ pass
647
+
648
+ for child in root.children:
649
+ _walk(child)
650
+
651
+ return comments