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