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,615 @@
|
|
|
1
|
+
"""C++ symbol, edge, and comment extraction from tree-sitter ASTs.
|
|
2
|
+
|
|
3
|
+
LAYER: imports from graph_common (leaf), graph_scope_infer_ext[2] (leaf), graph_c (leaf) — never from graph.py.
|
|
4
|
+
|
|
5
|
+
LAYERING:
|
|
6
|
+
graph_common (leaf — no seam deps)
|
|
7
|
+
graph_c (leaf — C extractors + shared C/C++ helpers)
|
|
8
|
+
↑
|
|
9
|
+
graph_cpp (this file)
|
|
10
|
+
↑
|
|
11
|
+
graph_c_cpp (thin re-exporter; graph.py imports from there)
|
|
12
|
+
|
|
13
|
+
WHY split from graph_c_cpp.py: graph_c_cpp.py exceeded 1000 lines after Tier B additions.
|
|
14
|
+
C and C++ are now each large enough to stand alone. graph_cpp.py imports shared helpers
|
|
15
|
+
(_c_doc_comment, _handle_c_include) from graph_c to avoid duplication.
|
|
16
|
+
|
|
17
|
+
GRAMMAR FACTS (verified by dumping AST from real fixtures):
|
|
18
|
+
namespace_definition: traversed, NOT emitted as a symbol.
|
|
19
|
+
class_specifier / struct_specifier: field 'name' → kind='class'.
|
|
20
|
+
field_declaration_list body may contain:
|
|
21
|
+
access_specifier nodes (public/private/protected), field_declaration,
|
|
22
|
+
function_definition (in-class method).
|
|
23
|
+
In-class function_definition: declarator field is function_declarator whose
|
|
24
|
+
'declarator' field is a field_identifier (method name, NOT identifier).
|
|
25
|
+
Out-of-line method: function_declarator's 'declarator' field is a
|
|
26
|
+
qualified_identifier with 'scope' (class name) and 'name' (method name).
|
|
27
|
+
enum_specifier: field 'name' → kind='type'.
|
|
28
|
+
template_declaration: the inner class_specifier or function_definition is
|
|
29
|
+
extracted; the template itself is NOT emitted as a symbol.
|
|
30
|
+
call_expression: field 'function' = identifier → bare call target.
|
|
31
|
+
comment: same as C — covers // and /* */ comments.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import logging
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
from tree_sitter import Node
|
|
38
|
+
|
|
39
|
+
import seam.config as config
|
|
40
|
+
from seam.indexer.field_access_ext import (
|
|
41
|
+
collect_field_symbols_cpp,
|
|
42
|
+
extract_field_accesses_cpp,
|
|
43
|
+
)
|
|
44
|
+
from seam.indexer.graph_c import (
|
|
45
|
+
_c_doc_comment,
|
|
46
|
+
_extract_comments_c,
|
|
47
|
+
_handle_c_include,
|
|
48
|
+
)
|
|
49
|
+
from seam.indexer.graph_common import (
|
|
50
|
+
Comment,
|
|
51
|
+
Edge,
|
|
52
|
+
Symbol,
|
|
53
|
+
_find_enclosing_function,
|
|
54
|
+
_make_symbol,
|
|
55
|
+
_text,
|
|
56
|
+
)
|
|
57
|
+
from seam.indexer.graph_scope_infer_ext import param_types_via_recorder, resolve_receiver_type_ext
|
|
58
|
+
from seam.indexer.graph_scope_infer_ext2 import (
|
|
59
|
+
_CPP_SELF_NAMES,
|
|
60
|
+
collect_composition_types_cpp,
|
|
61
|
+
record_cpp_local_types,
|
|
62
|
+
record_cpp_param_types,
|
|
63
|
+
scan_class_fields_cpp,
|
|
64
|
+
)
|
|
65
|
+
from seam.indexer.signatures import extract_node_fields
|
|
66
|
+
|
|
67
|
+
logger = logging.getLogger(__name__)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ── C++ helpers ────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _cpp_function_name(func_def: Node) -> str | None:
|
|
74
|
+
"""Extract the function name from a C++ function_definition node.
|
|
75
|
+
|
|
76
|
+
Handles three patterns observed in the real grammar:
|
|
77
|
+
1. Free function: declarator → function_declarator → declarator=identifier
|
|
78
|
+
2. In-class method: declarator → function_declarator → declarator=field_identifier
|
|
79
|
+
3. Out-of-line method: declarator → function_declarator → declarator=qualified_identifier
|
|
80
|
+
|
|
81
|
+
Returns a plain name (for free functions and in-class methods) or a
|
|
82
|
+
'Class.method' qualified name (for out-of-line Class::method definitions).
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
declarator = func_def.child_by_field_name("declarator")
|
|
86
|
+
if declarator is None:
|
|
87
|
+
return None
|
|
88
|
+
if declarator.type != "function_declarator":
|
|
89
|
+
return None
|
|
90
|
+
inner = declarator.child_by_field_name("declarator")
|
|
91
|
+
if inner is None:
|
|
92
|
+
return None
|
|
93
|
+
if inner.type == "identifier":
|
|
94
|
+
return _text(inner)
|
|
95
|
+
if inner.type == "field_identifier":
|
|
96
|
+
return _text(inner)
|
|
97
|
+
if inner.type == "qualified_identifier":
|
|
98
|
+
scope = inner.child_by_field_name("scope")
|
|
99
|
+
name_node = inner.child_by_field_name("name")
|
|
100
|
+
if scope is not None and name_node is not None:
|
|
101
|
+
return f"{_text(scope)}.{_text(name_node)}"
|
|
102
|
+
except Exception: # noqa: BLE001
|
|
103
|
+
pass
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _cpp_enclosing_class_name(node: Node) -> str | None:
|
|
108
|
+
"""Walk up the parent chain to find the nearest enclosing C++ class/struct name."""
|
|
109
|
+
try:
|
|
110
|
+
current = node.parent
|
|
111
|
+
while current is not None:
|
|
112
|
+
if current.type in ("class_specifier", "struct_specifier"):
|
|
113
|
+
name_node = current.child_by_field_name("name")
|
|
114
|
+
if name_node is not None:
|
|
115
|
+
return _text(name_node)
|
|
116
|
+
current = current.parent
|
|
117
|
+
except Exception: # noqa: BLE001
|
|
118
|
+
pass
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _dedup_cpp_symbols(symbols: list[Symbol]) -> list[Symbol]:
|
|
123
|
+
"""De-duplicate C++ symbols by (name, kind), keeping one per (name, kind) pair.
|
|
124
|
+
|
|
125
|
+
WHY: a class method declared in-class AND defined out-of-line both produce a
|
|
126
|
+
symbol with the same qualified name and kind. The de-duplication rule: prefer
|
|
127
|
+
the entry with the larger line span (out-of-line definitions typically have a full body).
|
|
128
|
+
"""
|
|
129
|
+
seen: dict[tuple[str, str], Symbol] = {}
|
|
130
|
+
for sym in symbols:
|
|
131
|
+
key = (sym["name"], sym["kind"])
|
|
132
|
+
if key not in seen:
|
|
133
|
+
seen[key] = sym
|
|
134
|
+
else:
|
|
135
|
+
existing = seen[key]
|
|
136
|
+
existing_span = existing["end_line"] - existing["start_line"]
|
|
137
|
+
current_span = sym["end_line"] - sym["start_line"]
|
|
138
|
+
if current_span > existing_span:
|
|
139
|
+
seen[key] = sym
|
|
140
|
+
result: list[Symbol] = []
|
|
141
|
+
added: set[tuple[str, str]] = set()
|
|
142
|
+
for sym in symbols:
|
|
143
|
+
key = (sym["name"], sym["kind"])
|
|
144
|
+
if seen[key] is sym and key not in added:
|
|
145
|
+
result.append(sym)
|
|
146
|
+
added.add(key)
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ── C++ extraction ─────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _extract_symbols_cpp(root: Node, filepath: Path) -> list[Symbol]:
|
|
154
|
+
"""Walk a C++ AST and extract class, struct, union, enum, and function symbols.
|
|
155
|
+
|
|
156
|
+
Kind mapping (per Phase 9 spec):
|
|
157
|
+
namespace_definition → traversed, NOT emitted
|
|
158
|
+
class_specifier / struct_specifier → class
|
|
159
|
+
union_specifier → class
|
|
160
|
+
enum_specifier → type
|
|
161
|
+
function_definition (free) → function
|
|
162
|
+
function_definition (in-class) → method (Class.method)
|
|
163
|
+
function_definition (out-of-line) → method (Class.method via qualified_identifier)
|
|
164
|
+
template_declaration → inner class/function extracted
|
|
165
|
+
|
|
166
|
+
De-duplication is applied at the end: in-class + out-of-line definitions of the
|
|
167
|
+
same method both produce the same (name, kind) — only one is retained.
|
|
168
|
+
"""
|
|
169
|
+
try:
|
|
170
|
+
symbols: list[Symbol] = []
|
|
171
|
+
file_str = str(filepath)
|
|
172
|
+
|
|
173
|
+
def _walk(node: Node, class_name: str | None = None) -> None:
|
|
174
|
+
if node.type == "namespace_definition":
|
|
175
|
+
body = node.child_by_field_name("body")
|
|
176
|
+
if body is not None:
|
|
177
|
+
for child in body.children:
|
|
178
|
+
_walk(child, class_name)
|
|
179
|
+
|
|
180
|
+
elif node.type in ("class_specifier", "struct_specifier"):
|
|
181
|
+
_handle_cpp_class(node, file_str, symbols, _walk)
|
|
182
|
+
|
|
183
|
+
elif node.type == "union_specifier":
|
|
184
|
+
_handle_cpp_union(node, file_str, symbols, _walk)
|
|
185
|
+
|
|
186
|
+
elif node.type == "enum_specifier":
|
|
187
|
+
_handle_cpp_enum(node, file_str, symbols)
|
|
188
|
+
|
|
189
|
+
elif node.type == "function_definition":
|
|
190
|
+
_handle_cpp_function(node, file_str, symbols, class_name)
|
|
191
|
+
|
|
192
|
+
elif node.type == "template_declaration":
|
|
193
|
+
for child in node.children:
|
|
194
|
+
if child.type in (
|
|
195
|
+
"class_specifier",
|
|
196
|
+
"struct_specifier",
|
|
197
|
+
"function_definition",
|
|
198
|
+
):
|
|
199
|
+
_walk(child, class_name)
|
|
200
|
+
|
|
201
|
+
else:
|
|
202
|
+
for child in node.children:
|
|
203
|
+
_walk(child, class_name)
|
|
204
|
+
|
|
205
|
+
for child in root.children:
|
|
206
|
+
_walk(child, class_name=None)
|
|
207
|
+
|
|
208
|
+
return _dedup_cpp_symbols(symbols)
|
|
209
|
+
except Exception: # noqa: BLE001
|
|
210
|
+
logger.debug(
|
|
211
|
+
"_extract_symbols_cpp: unhandled exception for file=%s",
|
|
212
|
+
filepath,
|
|
213
|
+
exc_info=True,
|
|
214
|
+
)
|
|
215
|
+
return []
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _handle_cpp_class(
|
|
219
|
+
node: Node,
|
|
220
|
+
file_str: str,
|
|
221
|
+
symbols: list[Symbol],
|
|
222
|
+
walk_fn: object,
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Emit a C++ class/struct symbol and recurse into its body for methods."""
|
|
225
|
+
try:
|
|
226
|
+
name_node = node.child_by_field_name("name")
|
|
227
|
+
if name_node is None:
|
|
228
|
+
return
|
|
229
|
+
cls_name = _text(name_node)
|
|
230
|
+
if not cls_name:
|
|
231
|
+
return
|
|
232
|
+
doc = _c_doc_comment(node)
|
|
233
|
+
fields = extract_node_fields(
|
|
234
|
+
node,
|
|
235
|
+
"cpp",
|
|
236
|
+
qualified_name=cls_name,
|
|
237
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
238
|
+
)
|
|
239
|
+
symbols.append(
|
|
240
|
+
_make_symbol(
|
|
241
|
+
cls_name,
|
|
242
|
+
"class",
|
|
243
|
+
file_str,
|
|
244
|
+
node,
|
|
245
|
+
doc,
|
|
246
|
+
signature=fields["signature"],
|
|
247
|
+
decorators=fields["decorators"],
|
|
248
|
+
is_exported=fields["is_exported"],
|
|
249
|
+
visibility=fields["visibility"],
|
|
250
|
+
qualified_name=cls_name,
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
# A3 Slice 4: emit field symbols for C++ class/struct fields.
|
|
254
|
+
if config.SEAM_FIELD_ACCESS_EDGES == "on":
|
|
255
|
+
for qual_name, field_line in collect_field_symbols_cpp(node, cls_name):
|
|
256
|
+
symbols.append(Symbol(
|
|
257
|
+
name=qual_name,
|
|
258
|
+
kind="field",
|
|
259
|
+
file=file_str,
|
|
260
|
+
start_line=field_line,
|
|
261
|
+
end_line=field_line,
|
|
262
|
+
docstring=None,
|
|
263
|
+
signature=None,
|
|
264
|
+
decorators=[],
|
|
265
|
+
is_exported=None,
|
|
266
|
+
visibility=None,
|
|
267
|
+
qualified_name=qual_name,
|
|
268
|
+
))
|
|
269
|
+
body = node.child_by_field_name("body")
|
|
270
|
+
if body is not None:
|
|
271
|
+
for child in body.children:
|
|
272
|
+
if child.type == "function_definition":
|
|
273
|
+
_handle_cpp_function(child, file_str, symbols, cls_name)
|
|
274
|
+
except Exception: # noqa: BLE001
|
|
275
|
+
logger.debug("_handle_cpp_class: failed for node at row %d", node.start_point[0])
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _handle_cpp_union(
|
|
279
|
+
node: Node,
|
|
280
|
+
file_str: str,
|
|
281
|
+
symbols: list[Symbol],
|
|
282
|
+
walk_fn: object,
|
|
283
|
+
) -> None:
|
|
284
|
+
"""Emit a C++ union symbol from a union_specifier node."""
|
|
285
|
+
try:
|
|
286
|
+
name_node = node.child_by_field_name("name")
|
|
287
|
+
if name_node is None:
|
|
288
|
+
return
|
|
289
|
+
name = _text(name_node)
|
|
290
|
+
if not name:
|
|
291
|
+
return
|
|
292
|
+
doc = _c_doc_comment(node)
|
|
293
|
+
fields = extract_node_fields(
|
|
294
|
+
node,
|
|
295
|
+
"cpp",
|
|
296
|
+
qualified_name=name,
|
|
297
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
298
|
+
)
|
|
299
|
+
symbols.append(
|
|
300
|
+
_make_symbol(
|
|
301
|
+
name,
|
|
302
|
+
"class",
|
|
303
|
+
file_str,
|
|
304
|
+
node,
|
|
305
|
+
doc,
|
|
306
|
+
signature=fields["signature"],
|
|
307
|
+
decorators=fields["decorators"],
|
|
308
|
+
is_exported=fields["is_exported"],
|
|
309
|
+
visibility=fields["visibility"],
|
|
310
|
+
qualified_name=name,
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
except Exception: # noqa: BLE001
|
|
314
|
+
logger.debug("_handle_cpp_union: failed for node at row %d", node.start_point[0])
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _handle_cpp_enum(node: Node, file_str: str, symbols: list[Symbol]) -> None:
|
|
318
|
+
"""Emit a C++ enum symbol from an enum_specifier node."""
|
|
319
|
+
try:
|
|
320
|
+
name_node = node.child_by_field_name("name")
|
|
321
|
+
if name_node is None:
|
|
322
|
+
return
|
|
323
|
+
name = _text(name_node)
|
|
324
|
+
if not name:
|
|
325
|
+
return
|
|
326
|
+
doc = _c_doc_comment(node)
|
|
327
|
+
fields = extract_node_fields(
|
|
328
|
+
node,
|
|
329
|
+
"cpp",
|
|
330
|
+
qualified_name=name,
|
|
331
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
332
|
+
)
|
|
333
|
+
symbols.append(
|
|
334
|
+
_make_symbol(
|
|
335
|
+
name,
|
|
336
|
+
"type",
|
|
337
|
+
file_str,
|
|
338
|
+
node,
|
|
339
|
+
doc,
|
|
340
|
+
signature=fields["signature"],
|
|
341
|
+
decorators=fields["decorators"],
|
|
342
|
+
is_exported=fields["is_exported"],
|
|
343
|
+
visibility=fields["visibility"],
|
|
344
|
+
qualified_name=name,
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
except Exception: # noqa: BLE001
|
|
348
|
+
logger.debug("_handle_cpp_enum: failed for node at row %d", node.start_point[0])
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _handle_cpp_function(
|
|
352
|
+
node: Node,
|
|
353
|
+
file_str: str,
|
|
354
|
+
symbols: list[Symbol],
|
|
355
|
+
enclosing_class: str | None,
|
|
356
|
+
) -> None:
|
|
357
|
+
"""Emit a C++ function or method symbol from a function_definition node.
|
|
358
|
+
|
|
359
|
+
Qualification rules:
|
|
360
|
+
- Out-of-line method (qualified_identifier declarator): name = 'Class.method'
|
|
361
|
+
- In-class method (field_identifier declarator with enclosing class known):
|
|
362
|
+
name = 'EnclosingClass.method', kind='method'.
|
|
363
|
+
- Free function (identifier declarator, no enclosing class): name = plain name, kind='function'.
|
|
364
|
+
"""
|
|
365
|
+
try:
|
|
366
|
+
name = _cpp_function_name(node)
|
|
367
|
+
if not name:
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
if "." in name:
|
|
371
|
+
kind = "method"
|
|
372
|
+
qualified = name
|
|
373
|
+
elif enclosing_class:
|
|
374
|
+
kind = "method"
|
|
375
|
+
qualified = f"{enclosing_class}.{name}"
|
|
376
|
+
else:
|
|
377
|
+
kind = "function"
|
|
378
|
+
qualified = name
|
|
379
|
+
|
|
380
|
+
doc = _c_doc_comment(node)
|
|
381
|
+
fields = extract_node_fields(
|
|
382
|
+
node,
|
|
383
|
+
"cpp",
|
|
384
|
+
qualified_name=qualified,
|
|
385
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
386
|
+
)
|
|
387
|
+
symbols.append(
|
|
388
|
+
_make_symbol(
|
|
389
|
+
qualified,
|
|
390
|
+
kind,
|
|
391
|
+
file_str,
|
|
392
|
+
node,
|
|
393
|
+
doc,
|
|
394
|
+
signature=fields["signature"],
|
|
395
|
+
decorators=fields["decorators"],
|
|
396
|
+
is_exported=fields["is_exported"],
|
|
397
|
+
visibility=fields["visibility"],
|
|
398
|
+
qualified_name=qualified,
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
except Exception: # noqa: BLE001
|
|
402
|
+
logger.debug("_handle_cpp_function: failed for node at row %d", node.start_point[0])
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _extract_edges_cpp(root: Node, filepath: Path) -> list[Edge]:
|
|
406
|
+
"""Extract import and call edges from a C++ AST.
|
|
407
|
+
|
|
408
|
+
Import heuristic:
|
|
409
|
+
#include "x.h" → target = stem ('x')
|
|
410
|
+
#include <x> → target = stem ('x') [system/STL header]
|
|
411
|
+
|
|
412
|
+
Call heuristic:
|
|
413
|
+
call_expression where 'function' is an identifier → bare call.
|
|
414
|
+
call_expression where 'function' is field_expression → obj.m() or obj->m().
|
|
415
|
+
|
|
416
|
+
Tier B B5: when SEAM_TYPE_INFERENCE is on, field_expression calls are resolved to
|
|
417
|
+
'Type.method' qualified targets using per-function scope.
|
|
418
|
+
"""
|
|
419
|
+
try:
|
|
420
|
+
edges: list[Edge] = []
|
|
421
|
+
file_str = str(filepath)
|
|
422
|
+
file_stem = filepath.stem
|
|
423
|
+
infer = config.SEAM_TYPE_INFERENCE == "on"
|
|
424
|
+
composition_on = config.SEAM_COMPOSITION_EDGES == "on"
|
|
425
|
+
field_access_on = config.SEAM_FIELD_ACCESS_EDGES == "on"
|
|
426
|
+
param_edges_on = config.SEAM_PARAM_EDGES == "on"
|
|
427
|
+
|
|
428
|
+
def _walk(
|
|
429
|
+
node: Node,
|
|
430
|
+
class_name: str | None,
|
|
431
|
+
class_fields: dict[str, str],
|
|
432
|
+
var_types: dict[str, str],
|
|
433
|
+
) -> None:
|
|
434
|
+
ntype = node.type
|
|
435
|
+
if ntype == "preproc_include":
|
|
436
|
+
_handle_c_include(node, file_str, file_stem, edges)
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
if ntype in ("class_specifier", "struct_specifier"):
|
|
440
|
+
new_class: str | None = None
|
|
441
|
+
cn = node.child_by_field_name("name")
|
|
442
|
+
if cn is not None:
|
|
443
|
+
new_class = _text(cn).strip() or None
|
|
444
|
+
# Slice #79: emit holds edges for C++ class/struct fields.
|
|
445
|
+
if composition_on and new_class:
|
|
446
|
+
_handle_cpp_class_holds(node, new_class, file_str, edges)
|
|
447
|
+
new_fields = scan_class_fields_cpp(node) if infer else {}
|
|
448
|
+
for child in node.children:
|
|
449
|
+
_walk(child, new_class, new_fields, dict(new_fields))
|
|
450
|
+
return
|
|
451
|
+
|
|
452
|
+
if ntype == "function_definition":
|
|
453
|
+
new_types: dict[str, str] = dict(class_fields)
|
|
454
|
+
if infer:
|
|
455
|
+
record_cpp_param_types(node, new_types)
|
|
456
|
+
# 'uses' edges: function/method references plain user types as params.
|
|
457
|
+
if param_edges_on:
|
|
458
|
+
_fn = _cpp_function_name(node)
|
|
459
|
+
if _fn:
|
|
460
|
+
_src = (
|
|
461
|
+
f"{class_name}.{_fn}" if ("." not in _fn and class_name) else _fn
|
|
462
|
+
)
|
|
463
|
+
for ptype, pline in param_types_via_recorder(
|
|
464
|
+
node, record_cpp_param_types, "cpp"
|
|
465
|
+
):
|
|
466
|
+
edges.append(Edge(
|
|
467
|
+
source=_src, target=ptype, kind="uses",
|
|
468
|
+
file=file_str, line=pline, confidence="INFERRED", receiver=None,
|
|
469
|
+
))
|
|
470
|
+
body = node.child_by_field_name("body")
|
|
471
|
+
if body is not None:
|
|
472
|
+
# A3 Slice 4: emit reads/writes field-access edges for C++ methods.
|
|
473
|
+
if field_access_on:
|
|
474
|
+
func_name = _cpp_function_name(node)
|
|
475
|
+
if func_name:
|
|
476
|
+
# For in-class methods (field_identifier declarator),
|
|
477
|
+
# qualify with enclosing class.
|
|
478
|
+
if "." not in func_name and class_name:
|
|
479
|
+
source_fn = f"{class_name}.{func_name}"
|
|
480
|
+
else:
|
|
481
|
+
source_fn = func_name
|
|
482
|
+
# Determine class context: use dotted prefix if out-of-line.
|
|
483
|
+
ctx_class = class_name
|
|
484
|
+
if "." in source_fn:
|
|
485
|
+
ctx_class = source_fn.split(".")[0]
|
|
486
|
+
for src, tgt, mode, ln in extract_field_accesses_cpp(
|
|
487
|
+
body, source_fn, ctx_class, new_types
|
|
488
|
+
):
|
|
489
|
+
edges.append(Edge(
|
|
490
|
+
source=src,
|
|
491
|
+
target=tgt,
|
|
492
|
+
kind=mode,
|
|
493
|
+
file=file_str,
|
|
494
|
+
line=ln,
|
|
495
|
+
confidence="INFERRED",
|
|
496
|
+
receiver=None,
|
|
497
|
+
))
|
|
498
|
+
for child in body.children:
|
|
499
|
+
_walk(child, class_name, class_fields, new_types)
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
if infer and ntype == "declaration":
|
|
503
|
+
record_cpp_local_types(node, var_types)
|
|
504
|
+
|
|
505
|
+
# Tier B B6: new_expression (new Foo(...)) → instantiates edge.
|
|
506
|
+
elif ntype == "new_expression":
|
|
507
|
+
type_node = next(
|
|
508
|
+
(c for c in node.children if c.type == "type_identifier"), None
|
|
509
|
+
)
|
|
510
|
+
if type_node is not None:
|
|
511
|
+
type_name = _text(type_node)
|
|
512
|
+
if type_name:
|
|
513
|
+
source = _find_enclosing_function(node, "cpp")
|
|
514
|
+
if source is not None:
|
|
515
|
+
edges.append(
|
|
516
|
+
Edge(
|
|
517
|
+
source=source,
|
|
518
|
+
target=type_name,
|
|
519
|
+
kind="instantiates",
|
|
520
|
+
file=file_str,
|
|
521
|
+
line=node.start_point[0] + 1,
|
|
522
|
+
confidence="INFERRED",
|
|
523
|
+
receiver=None,
|
|
524
|
+
)
|
|
525
|
+
)
|
|
526
|
+
for child in node.children:
|
|
527
|
+
_walk(child, class_name, class_fields, var_types)
|
|
528
|
+
return
|
|
529
|
+
|
|
530
|
+
elif ntype == "call_expression":
|
|
531
|
+
func_child = node.child_by_field_name("function")
|
|
532
|
+
cpp_callee: str | None = None
|
|
533
|
+
cpp_recv: str | None = None
|
|
534
|
+
|
|
535
|
+
if func_child and func_child.type == "identifier":
|
|
536
|
+
cpp_callee = _text(func_child)
|
|
537
|
+
elif func_child and func_child.type == "field_expression":
|
|
538
|
+
arg_node = func_child.child_by_field_name("argument")
|
|
539
|
+
field_node = func_child.child_by_field_name("field")
|
|
540
|
+
if field_node is not None and field_node.type == "field_identifier":
|
|
541
|
+
cpp_callee = _text(field_node)
|
|
542
|
+
if arg_node is not None:
|
|
543
|
+
cpp_recv = _text(arg_node)
|
|
544
|
+
|
|
545
|
+
if cpp_callee:
|
|
546
|
+
final_target = cpp_callee
|
|
547
|
+
if infer and cpp_recv is not None:
|
|
548
|
+
recv_lookup = cpp_recv.lstrip("*").strip()
|
|
549
|
+
resolved_type = resolve_receiver_type_ext(
|
|
550
|
+
recv_lookup, class_name, var_types, _CPP_SELF_NAMES
|
|
551
|
+
)
|
|
552
|
+
if resolved_type:
|
|
553
|
+
final_target = f"{resolved_type}.{cpp_callee}"
|
|
554
|
+
|
|
555
|
+
source = _find_enclosing_function(node, "cpp")
|
|
556
|
+
if source is not None:
|
|
557
|
+
edges.append(
|
|
558
|
+
Edge(
|
|
559
|
+
source=source,
|
|
560
|
+
target=final_target,
|
|
561
|
+
kind="call",
|
|
562
|
+
file=file_str,
|
|
563
|
+
line=node.start_point[0] + 1,
|
|
564
|
+
confidence="INFERRED",
|
|
565
|
+
receiver=cpp_recv,
|
|
566
|
+
)
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
for child in node.children:
|
|
570
|
+
_walk(child, class_name, class_fields, var_types)
|
|
571
|
+
|
|
572
|
+
for child in root.children:
|
|
573
|
+
_walk(child, None, {}, {})
|
|
574
|
+
|
|
575
|
+
return edges
|
|
576
|
+
except Exception: # noqa: BLE001
|
|
577
|
+
logger.debug(
|
|
578
|
+
"_extract_edges_cpp: unhandled exception for file=%s",
|
|
579
|
+
filepath,
|
|
580
|
+
exc_info=True,
|
|
581
|
+
)
|
|
582
|
+
return []
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _handle_cpp_class_holds(
|
|
586
|
+
class_node: Node, class_name: str, file_str: str, edges: list[Edge]
|
|
587
|
+
) -> None:
|
|
588
|
+
"""Emit holds edges for each plain user-type field in a C++ class/struct.
|
|
589
|
+
|
|
590
|
+
Delegates to collect_composition_types_cpp for (held_type, line) pairs and
|
|
591
|
+
emits one Edge per unique pair. Never raises (backstop try/except).
|
|
592
|
+
"""
|
|
593
|
+
try:
|
|
594
|
+
for held_type, held_line in collect_composition_types_cpp(class_node):
|
|
595
|
+
edges.append(
|
|
596
|
+
Edge(
|
|
597
|
+
source=class_name,
|
|
598
|
+
target=held_type,
|
|
599
|
+
kind="holds",
|
|
600
|
+
file=file_str,
|
|
601
|
+
line=held_line,
|
|
602
|
+
confidence="INFERRED",
|
|
603
|
+
receiver=None,
|
|
604
|
+
)
|
|
605
|
+
)
|
|
606
|
+
except Exception as exc: # noqa: BLE001
|
|
607
|
+
logger.debug("_handle_cpp_class_holds: failed: %r", exc)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _extract_comments_cpp(root: Node, filepath: Path) -> list[Comment]:
|
|
611
|
+
"""Walk a C++ AST and extract semantic comment markers (WHY/HACK/NOTE/TODO/FIXME).
|
|
612
|
+
|
|
613
|
+
C++ shares the same 'comment' node type as C — delegate to the C implementation.
|
|
614
|
+
"""
|
|
615
|
+
return _extract_comments_c(root, filepath)
|