seam-code 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- seam/__init__.py +3 -0
- seam/_data/schema.sql +225 -0
- seam/_web/assets/index-BL_tqprR.js +216 -0
- seam/_web/assets/index-GTKUhVyD.css +1 -0
- seam/_web/index.html +13 -0
- seam/analysis/__init__.py +14 -0
- seam/analysis/affected.py +254 -0
- seam/analysis/builtins.py +966 -0
- seam/analysis/byte_budget.py +217 -0
- seam/analysis/changes.py +709 -0
- seam/analysis/cluster_naming.py +260 -0
- seam/analysis/clustering.py +216 -0
- seam/analysis/confidence.py +699 -0
- seam/analysis/embeddings.py +195 -0
- seam/analysis/flows.py +708 -0
- seam/analysis/impact.py +444 -0
- seam/analysis/imports.py +994 -0
- seam/analysis/imports_ext.py +780 -0
- seam/analysis/imports_resolve.py +176 -0
- seam/analysis/processes.py +453 -0
- seam/analysis/relevance.py +155 -0
- seam/analysis/rwr.py +129 -0
- seam/analysis/staleness.py +328 -0
- seam/analysis/steer.py +282 -0
- seam/analysis/synthesis.py +253 -0
- seam/analysis/synthesis_channels.py +433 -0
- seam/analysis/testpaths.py +103 -0
- seam/analysis/traversal.py +470 -0
- seam/cli/__init__.py +0 -0
- seam/cli/install.py +232 -0
- seam/cli/main.py +2602 -0
- seam/cli/output.py +137 -0
- seam/cli/read.py +244 -0
- seam/cli/serve.py +145 -0
- seam/config.py +551 -0
- seam/indexer/__init__.py +0 -0
- seam/indexer/cluster_index.py +425 -0
- seam/indexer/db.py +496 -0
- seam/indexer/embedding_index.py +183 -0
- seam/indexer/field_access.py +536 -0
- seam/indexer/field_access_c_cpp.py +643 -0
- seam/indexer/field_access_ext.py +708 -0
- seam/indexer/field_access_ext2.py +408 -0
- seam/indexer/field_access_go_rust.py +737 -0
- seam/indexer/field_access_php_swift.py +888 -0
- seam/indexer/field_access_ts.py +626 -0
- seam/indexer/graph.py +321 -0
- seam/indexer/graph_c.py +562 -0
- seam/indexer/graph_c_cpp.py +39 -0
- seam/indexer/graph_common.py +644 -0
- seam/indexer/graph_cpp.py +615 -0
- seam/indexer/graph_csharp.py +651 -0
- seam/indexer/graph_go.py +723 -0
- seam/indexer/graph_go_rust.py +39 -0
- seam/indexer/graph_java.py +689 -0
- seam/indexer/graph_java_csharp.py +38 -0
- seam/indexer/graph_php.py +914 -0
- seam/indexer/graph_python.py +628 -0
- seam/indexer/graph_ruby.py +748 -0
- seam/indexer/graph_rust.py +653 -0
- seam/indexer/graph_scope_infer.py +902 -0
- seam/indexer/graph_scope_infer_ext.py +723 -0
- seam/indexer/graph_scope_infer_ext2.py +992 -0
- seam/indexer/graph_swift.py +1014 -0
- seam/indexer/graph_swift_infer.py +515 -0
- seam/indexer/graph_typescript.py +663 -0
- seam/indexer/migrations.py +816 -0
- seam/indexer/parser.py +204 -0
- seam/indexer/pipeline.py +197 -0
- seam/indexer/signatures.py +634 -0
- seam/indexer/signatures_ext.py +780 -0
- seam/indexer/sync.py +287 -0
- seam/indexer/synthesis_index.py +291 -0
- seam/indexer/tokenize.py +79 -0
- seam/installer/__init__.py +67 -0
- seam/installer/claude.py +97 -0
- seam/installer/codex.py +94 -0
- seam/installer/core.py +127 -0
- seam/installer/cursor.py +61 -0
- seam/installer/guide.py +110 -0
- seam/installer/jsonfile.py +85 -0
- seam/installer/markdownfile.py +146 -0
- seam/installer/tomlfile.py +72 -0
- seam/query/__init__.py +0 -0
- seam/query/clusters.py +206 -0
- seam/query/comments.py +217 -0
- seam/query/context.py +293 -0
- seam/query/engine.py +940 -0
- seam/query/fts.py +328 -0
- seam/query/names.py +470 -0
- seam/query/pack.py +433 -0
- seam/query/semantic.py +339 -0
- seam/query/structure.py +727 -0
- seam/server/__init__.py +0 -0
- seam/server/graph_api.py +437 -0
- seam/server/handler_common.py +323 -0
- seam/server/impact_handler.py +615 -0
- seam/server/mcp.py +556 -0
- seam/server/tools.py +697 -0
- seam/server/trace_handler.py +184 -0
- seam/server/web.py +922 -0
- seam/watcher/__init__.py +0 -0
- seam/watcher/__main__.py +56 -0
- seam/watcher/daemon.py +237 -0
- seam_code-0.3.0.dist-info/METADATA +318 -0
- seam_code-0.3.0.dist-info/RECORD +109 -0
- seam_code-0.3.0.dist-info/WHEEL +4 -0
- seam_code-0.3.0.dist-info/entry_points.txt +2 -0
- seam_code-0.3.0.dist-info/licenses/LICENSE +21 -0
seam/indexer/graph_c.py
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
"""C symbol, edge, and comment extraction from tree-sitter ASTs.
|
|
2
|
+
|
|
3
|
+
LAYER: imports from graph_common (leaf) — never from graph.py.
|
|
4
|
+
|
|
5
|
+
LAYERING:
|
|
6
|
+
graph_common (leaf — no seam deps)
|
|
7
|
+
↑
|
|
8
|
+
graph_c (this file)
|
|
9
|
+
↑
|
|
10
|
+
graph_c_cpp (thin re-exporter; graph.py imports from there)
|
|
11
|
+
|
|
12
|
+
WHY split from graph_c_cpp.py: graph_c_cpp.py exceeded 1000 lines after Tier B additions.
|
|
13
|
+
C 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
|
+
GRAMMAR FACTS (verified by dumping AST from real fixtures):
|
|
21
|
+
function_definition: fields 'type', 'declarator' (function_declarator whose
|
|
22
|
+
'declarator' field is an identifier = function name), 'body'.
|
|
23
|
+
storage_class_specifier "static" → file-local → visibility='private', is_exported=False.
|
|
24
|
+
struct_specifier / union_specifier: field 'name' (type_identifier) → kind='class'.
|
|
25
|
+
enum_specifier: field 'name' → kind='type'.
|
|
26
|
+
type_definition: last type_identifier child is the typedef alias → kind='type'.
|
|
27
|
+
preproc_include: field 'path' — string_literal (local) or system_lib_string (system).
|
|
28
|
+
call_expression: field 'function' = identifier → bare call target.
|
|
29
|
+
comment: covers both // and /* */ comments.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import logging
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from tree_sitter import Node
|
|
36
|
+
|
|
37
|
+
import seam.config as config
|
|
38
|
+
from seam.analysis.builtins import is_builtin
|
|
39
|
+
from seam.indexer.field_access_ext import (
|
|
40
|
+
collect_field_symbols_c,
|
|
41
|
+
extract_field_accesses_c,
|
|
42
|
+
)
|
|
43
|
+
from seam.indexer.graph_common import (
|
|
44
|
+
Comment,
|
|
45
|
+
Edge,
|
|
46
|
+
Symbol,
|
|
47
|
+
_block_comment_lines,
|
|
48
|
+
_find_enclosing_function,
|
|
49
|
+
_make_symbol,
|
|
50
|
+
_match_marker,
|
|
51
|
+
_text,
|
|
52
|
+
)
|
|
53
|
+
from seam.indexer.signatures import extract_node_fields
|
|
54
|
+
|
|
55
|
+
logger = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ── Shared C/C++ helpers (also imported by graph_cpp.py) ──────────────────────
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _c_doc_comment(decl_node: Node) -> str | None:
|
|
62
|
+
"""Capture a C/C++ doc comment above a declaration.
|
|
63
|
+
|
|
64
|
+
Strategy:
|
|
65
|
+
1. If the immediately preceding sibling is a /* */ block comment that
|
|
66
|
+
starts with '/**', return its cleaned content (Javadoc-style).
|
|
67
|
+
2. Otherwise, collect contiguous // line comments immediately above the
|
|
68
|
+
declaration (row-adjacent, no blank lines).
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
prev = decl_node.prev_sibling
|
|
72
|
+
if prev is None:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
if prev.type == "comment":
|
|
76
|
+
raw = _text(prev)
|
|
77
|
+
if raw.startswith("/**"):
|
|
78
|
+
block_lines: list[str] = []
|
|
79
|
+
for line in raw.splitlines():
|
|
80
|
+
s = line.strip()
|
|
81
|
+
if s.startswith("/*"):
|
|
82
|
+
s = s[2:]
|
|
83
|
+
if s.endswith("*/"):
|
|
84
|
+
s = s[:-2]
|
|
85
|
+
s = s.strip().lstrip("*").strip()
|
|
86
|
+
if s:
|
|
87
|
+
block_lines.append(s)
|
|
88
|
+
return "\n".join(block_lines) if block_lines else None
|
|
89
|
+
|
|
90
|
+
if raw.startswith("//"):
|
|
91
|
+
lines: list[str] = []
|
|
92
|
+
current: Node | None = prev
|
|
93
|
+
while current is not None and current.type == "comment":
|
|
94
|
+
c_raw = _text(current)
|
|
95
|
+
if not c_raw.startswith("//"):
|
|
96
|
+
break
|
|
97
|
+
next_node = current.next_sibling
|
|
98
|
+
if next_node is not None:
|
|
99
|
+
end_row = current.end_point[0]
|
|
100
|
+
next_start = next_node.start_point[0]
|
|
101
|
+
if end_row + 1 != next_start:
|
|
102
|
+
break
|
|
103
|
+
body = c_raw[2:].strip()
|
|
104
|
+
lines.append(body)
|
|
105
|
+
current = current.prev_sibling
|
|
106
|
+
if not lines:
|
|
107
|
+
return None
|
|
108
|
+
return "\n".join(reversed(lines))
|
|
109
|
+
except Exception: # noqa: BLE001
|
|
110
|
+
pass
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _handle_c_include(node: Node, file_str: str, file_stem: str, edges: list[Edge]) -> None:
|
|
115
|
+
"""Emit an import edge from a C/C++ preproc_include node.
|
|
116
|
+
|
|
117
|
+
Both local (#include "x.h") and system (#include <x.h>) includes produce edges.
|
|
118
|
+
The target is the header filename stem (e.g. "utils" from "utils.h" or <stdio.h>).
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
line = node.start_point[0] + 1
|
|
122
|
+
path_node = node.child_by_field_name("path")
|
|
123
|
+
if path_node is None:
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
if path_node.type == "string_literal":
|
|
127
|
+
content = None
|
|
128
|
+
for child in path_node.children:
|
|
129
|
+
if child.type == "string_content":
|
|
130
|
+
content = _text(child)
|
|
131
|
+
break
|
|
132
|
+
if content is None:
|
|
133
|
+
content = _text(path_node).strip('"')
|
|
134
|
+
target = Path(content).stem if content else None
|
|
135
|
+
|
|
136
|
+
elif path_node.type == "system_lib_string":
|
|
137
|
+
raw = _text(path_node).strip("<>")
|
|
138
|
+
target = Path(raw).stem if raw else None
|
|
139
|
+
|
|
140
|
+
else:
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
if target:
|
|
144
|
+
edges.append(
|
|
145
|
+
Edge(
|
|
146
|
+
source=file_stem,
|
|
147
|
+
target=target,
|
|
148
|
+
kind="import",
|
|
149
|
+
file=file_str,
|
|
150
|
+
line=line,
|
|
151
|
+
confidence="INFERRED",
|
|
152
|
+
receiver=None,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
except Exception: # noqa: BLE001
|
|
156
|
+
logger.debug("_handle_c_include: failed for node at row %d", node.start_point[0])
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ── C extraction ───────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _c_function_name(func_def: Node) -> str | None:
|
|
163
|
+
"""Extract the function name from a C function_definition node.
|
|
164
|
+
|
|
165
|
+
The name lives inside: function_definition → function_declarator → identifier.
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
declarator = func_def.child_by_field_name("declarator")
|
|
169
|
+
if declarator is None:
|
|
170
|
+
return None
|
|
171
|
+
name_node = declarator.child_by_field_name("declarator")
|
|
172
|
+
if name_node is not None and name_node.type == "identifier":
|
|
173
|
+
return _text(name_node)
|
|
174
|
+
except Exception: # noqa: BLE001
|
|
175
|
+
pass
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _c_param_types(func_def: Node) -> list[tuple[str, int]]:
|
|
180
|
+
"""Return (plain_user_type, line) pairs from a C function_definition's parameters.
|
|
181
|
+
|
|
182
|
+
A C parameter_declaration's `type` field is a type_identifier (a typedef'd user type,
|
|
183
|
+
e.g. `Engine e`) or a struct_specifier (`struct Engine *e`). Primitive params
|
|
184
|
+
(int/char/…) parse as primitive_type and are skipped naturally. Pointer/array shapes
|
|
185
|
+
live in the declarator, so the type field is the pointed-to type regardless of `*`.
|
|
186
|
+
Builtins filtered via is_builtin. Deduped within the signature. Never raises.
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
declarator = func_def.child_by_field_name("declarator")
|
|
190
|
+
if declarator is None:
|
|
191
|
+
return []
|
|
192
|
+
params = declarator.child_by_field_name("parameters")
|
|
193
|
+
if params is None:
|
|
194
|
+
return []
|
|
195
|
+
line = func_def.start_point[0] + 1
|
|
196
|
+
seen: set[str] = set()
|
|
197
|
+
out: list[tuple[str, int]] = []
|
|
198
|
+
for p in params.named_children:
|
|
199
|
+
if p.type != "parameter_declaration":
|
|
200
|
+
continue
|
|
201
|
+
tnode = p.child_by_field_name("type")
|
|
202
|
+
if tnode is None:
|
|
203
|
+
continue
|
|
204
|
+
name: str | None = None
|
|
205
|
+
if tnode.type == "type_identifier":
|
|
206
|
+
name = _text(tnode)
|
|
207
|
+
elif tnode.type == "struct_specifier":
|
|
208
|
+
nm = tnode.child_by_field_name("name")
|
|
209
|
+
if nm is not None:
|
|
210
|
+
name = _text(nm)
|
|
211
|
+
if name and not is_builtin(name, "c") and name not in seen:
|
|
212
|
+
seen.add(name)
|
|
213
|
+
out.append((name, line))
|
|
214
|
+
return out
|
|
215
|
+
except Exception as exc: # noqa: BLE001
|
|
216
|
+
logger.debug("_c_param_types: failed: %r", exc)
|
|
217
|
+
return []
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _c_is_static(func_def: Node) -> bool:
|
|
221
|
+
"""Return True if a C function_definition has a storage_class_specifier 'static'."""
|
|
222
|
+
try:
|
|
223
|
+
for child in func_def.children:
|
|
224
|
+
if child.type == "storage_class_specifier":
|
|
225
|
+
if _text(child).strip() == "static":
|
|
226
|
+
return True
|
|
227
|
+
except Exception: # noqa: BLE001
|
|
228
|
+
pass
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _extract_symbols_c(root: Node, filepath: Path) -> list[Symbol]:
|
|
233
|
+
"""Walk a C AST and extract function, struct, union, enum, and typedef symbols.
|
|
234
|
+
|
|
235
|
+
Kind mapping (per Phase 9 spec):
|
|
236
|
+
function_definition → function
|
|
237
|
+
struct_specifier (named) → class
|
|
238
|
+
union_specifier (named) → class
|
|
239
|
+
enum_specifier (named) → type
|
|
240
|
+
type_definition (typedef) → type
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
symbols: list[Symbol] = []
|
|
244
|
+
file_str = str(filepath)
|
|
245
|
+
field_access_on = config.SEAM_FIELD_ACCESS_EDGES == "on"
|
|
246
|
+
|
|
247
|
+
def _walk(node: Node) -> None:
|
|
248
|
+
if node.type == "function_definition":
|
|
249
|
+
_handle_c_function(node, file_str, symbols)
|
|
250
|
+
|
|
251
|
+
elif node.type == "struct_specifier":
|
|
252
|
+
_handle_c_aggregate(node, "class", file_str, symbols)
|
|
253
|
+
# A3 Slice 4: emit field symbols for C struct fields.
|
|
254
|
+
if field_access_on:
|
|
255
|
+
_c_emit_field_symbols(node, file_str, symbols)
|
|
256
|
+
|
|
257
|
+
elif node.type == "union_specifier":
|
|
258
|
+
_handle_c_aggregate(node, "class", file_str, symbols)
|
|
259
|
+
|
|
260
|
+
elif node.type == "enum_specifier":
|
|
261
|
+
_handle_c_aggregate(node, "type", file_str, symbols)
|
|
262
|
+
|
|
263
|
+
elif node.type == "type_definition":
|
|
264
|
+
_handle_c_typedef(node, file_str, symbols)
|
|
265
|
+
|
|
266
|
+
else:
|
|
267
|
+
for child in node.children:
|
|
268
|
+
_walk(child)
|
|
269
|
+
|
|
270
|
+
for child in root.children:
|
|
271
|
+
_walk(child)
|
|
272
|
+
|
|
273
|
+
return symbols
|
|
274
|
+
except Exception: # noqa: BLE001
|
|
275
|
+
logger.debug(
|
|
276
|
+
"_extract_symbols_c: unhandled exception for file=%s",
|
|
277
|
+
filepath,
|
|
278
|
+
exc_info=True,
|
|
279
|
+
)
|
|
280
|
+
return []
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _handle_c_function(node: Node, file_str: str, symbols: list[Symbol]) -> None:
|
|
284
|
+
"""Emit a C function symbol from a function_definition node."""
|
|
285
|
+
try:
|
|
286
|
+
name = _c_function_name(node)
|
|
287
|
+
if not name:
|
|
288
|
+
return
|
|
289
|
+
doc = _c_doc_comment(node)
|
|
290
|
+
fields = extract_node_fields(
|
|
291
|
+
node,
|
|
292
|
+
"c",
|
|
293
|
+
qualified_name=name,
|
|
294
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
295
|
+
)
|
|
296
|
+
symbols.append(
|
|
297
|
+
_make_symbol(
|
|
298
|
+
name,
|
|
299
|
+
"function",
|
|
300
|
+
file_str,
|
|
301
|
+
node,
|
|
302
|
+
doc,
|
|
303
|
+
signature=fields["signature"],
|
|
304
|
+
decorators=fields["decorators"],
|
|
305
|
+
is_exported=fields["is_exported"],
|
|
306
|
+
visibility=fields["visibility"],
|
|
307
|
+
qualified_name=name,
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
except Exception: # noqa: BLE001
|
|
311
|
+
logger.debug("_handle_c_function: failed for node at row %d", node.start_point[0])
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _handle_c_aggregate(node: Node, kind: str, file_str: str, symbols: list[Symbol]) -> None:
|
|
315
|
+
"""Emit a C struct/union/enum symbol from a named specifier node."""
|
|
316
|
+
try:
|
|
317
|
+
name_node = node.child_by_field_name("name")
|
|
318
|
+
if name_node is None:
|
|
319
|
+
return
|
|
320
|
+
name = _text(name_node)
|
|
321
|
+
if not name:
|
|
322
|
+
return
|
|
323
|
+
doc = _c_doc_comment(node)
|
|
324
|
+
fields = extract_node_fields(
|
|
325
|
+
node,
|
|
326
|
+
"c",
|
|
327
|
+
qualified_name=name,
|
|
328
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
329
|
+
)
|
|
330
|
+
symbols.append(
|
|
331
|
+
_make_symbol(
|
|
332
|
+
name,
|
|
333
|
+
kind,
|
|
334
|
+
file_str,
|
|
335
|
+
node,
|
|
336
|
+
doc,
|
|
337
|
+
signature=fields["signature"],
|
|
338
|
+
decorators=fields["decorators"],
|
|
339
|
+
is_exported=fields["is_exported"],
|
|
340
|
+
visibility=fields["visibility"],
|
|
341
|
+
qualified_name=name,
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
except Exception: # noqa: BLE001
|
|
345
|
+
logger.debug("_handle_c_aggregate: failed for node at row %d", node.start_point[0])
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _handle_c_typedef(node: Node, file_str: str, symbols: list[Symbol]) -> None:
|
|
349
|
+
"""Emit a C typedef symbol from a type_definition node."""
|
|
350
|
+
try:
|
|
351
|
+
declarator = node.child_by_field_name("declarator")
|
|
352
|
+
if declarator is not None and declarator.type == "type_identifier":
|
|
353
|
+
name = _text(declarator)
|
|
354
|
+
else:
|
|
355
|
+
name = None
|
|
356
|
+
for child in reversed(node.children):
|
|
357
|
+
if child.type == "type_identifier":
|
|
358
|
+
name = _text(child)
|
|
359
|
+
break
|
|
360
|
+
if not name:
|
|
361
|
+
return
|
|
362
|
+
doc = _c_doc_comment(node)
|
|
363
|
+
fields = extract_node_fields(
|
|
364
|
+
node,
|
|
365
|
+
"c",
|
|
366
|
+
qualified_name=name,
|
|
367
|
+
max_signature_len=config.SEAM_MAX_SIGNATURE_LEN,
|
|
368
|
+
)
|
|
369
|
+
symbols.append(
|
|
370
|
+
_make_symbol(
|
|
371
|
+
name,
|
|
372
|
+
"type",
|
|
373
|
+
file_str,
|
|
374
|
+
node,
|
|
375
|
+
doc,
|
|
376
|
+
signature=fields["signature"],
|
|
377
|
+
decorators=fields["decorators"],
|
|
378
|
+
is_exported=fields["is_exported"],
|
|
379
|
+
visibility=fields["visibility"],
|
|
380
|
+
qualified_name=name,
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
except Exception: # noqa: BLE001
|
|
384
|
+
logger.debug("_handle_c_typedef: failed for node at row %d", node.start_point[0])
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _c_emit_field_symbols(
|
|
388
|
+
struct_node: Node,
|
|
389
|
+
file_str: str,
|
|
390
|
+
symbols: list[Symbol],
|
|
391
|
+
) -> None:
|
|
392
|
+
"""Emit kind='field' symbols for all fields in a C struct_specifier.
|
|
393
|
+
|
|
394
|
+
Called when SEAM_FIELD_ACCESS_EDGES='on'. Uses collect_field_symbols_c to get
|
|
395
|
+
(qualified_field, line) pairs and appends Symbol entries.
|
|
396
|
+
|
|
397
|
+
Never raises (backstop try/except).
|
|
398
|
+
"""
|
|
399
|
+
try:
|
|
400
|
+
name_node = struct_node.child_by_field_name("name")
|
|
401
|
+
if name_node is None:
|
|
402
|
+
return
|
|
403
|
+
struct_name = _text(name_node).strip()
|
|
404
|
+
if not struct_name:
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
for qualified_field, field_line in collect_field_symbols_c(struct_node, struct_name):
|
|
408
|
+
symbols.append(Symbol(
|
|
409
|
+
name=qualified_field,
|
|
410
|
+
kind="field",
|
|
411
|
+
file=file_str,
|
|
412
|
+
start_line=field_line,
|
|
413
|
+
end_line=field_line,
|
|
414
|
+
docstring=None,
|
|
415
|
+
signature=None,
|
|
416
|
+
decorators=[],
|
|
417
|
+
is_exported=None,
|
|
418
|
+
visibility=None,
|
|
419
|
+
qualified_name=qualified_field,
|
|
420
|
+
))
|
|
421
|
+
except Exception as exc: # noqa: BLE001
|
|
422
|
+
logger.debug("_c_emit_field_symbols: failed: %r", exc)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _extract_edges_c(root: Node, filepath: Path) -> list[Edge]:
|
|
426
|
+
"""Extract import and call edges from a C AST.
|
|
427
|
+
|
|
428
|
+
Import heuristic:
|
|
429
|
+
#include "x.h" → target = stem of "x.h" (e.g. 'x')
|
|
430
|
+
#include <x.h> → target = stem of 'x.h' (e.g. 'x') [system header]
|
|
431
|
+
|
|
432
|
+
Call heuristic (MVP — bare identifiers and field_expression):
|
|
433
|
+
call_expression where 'function' field is an identifier → kind="call".
|
|
434
|
+
call_expression where 'function' field is field_expression → obj->func_ptr() → capture receiver.
|
|
435
|
+
"""
|
|
436
|
+
try:
|
|
437
|
+
edges: list[Edge] = []
|
|
438
|
+
file_str = str(filepath)
|
|
439
|
+
file_stem = filepath.stem
|
|
440
|
+
field_access_on = config.SEAM_FIELD_ACCESS_EDGES == "on"
|
|
441
|
+
|
|
442
|
+
def _walk(node: Node) -> None:
|
|
443
|
+
if node.type == "preproc_include":
|
|
444
|
+
_handle_c_include(node, file_str, file_stem, edges)
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
elif node.type == "function_definition":
|
|
448
|
+
# 'uses' edges: function references plain user types as params. C functions
|
|
449
|
+
# are top-level → source is the bare function name.
|
|
450
|
+
if config.SEAM_PARAM_EDGES == "on":
|
|
451
|
+
_fn = _c_function_name(node)
|
|
452
|
+
if _fn:
|
|
453
|
+
for ptype, pline in _c_param_types(node):
|
|
454
|
+
edges.append(Edge(
|
|
455
|
+
source=_fn, target=ptype, kind="uses",
|
|
456
|
+
file=file_str, line=pline, confidence="INFERRED", receiver=None,
|
|
457
|
+
))
|
|
458
|
+
# A3 Slice 4: emit reads/writes field-access edges from C function bodies.
|
|
459
|
+
if field_access_on:
|
|
460
|
+
func_name = _c_function_name(node)
|
|
461
|
+
body = node.child_by_field_name("body")
|
|
462
|
+
if func_name and body is not None:
|
|
463
|
+
for src, tgt, mode, line in extract_field_accesses_c(
|
|
464
|
+
body, func_name, None, {}
|
|
465
|
+
):
|
|
466
|
+
edges.append(Edge(
|
|
467
|
+
source=src,
|
|
468
|
+
target=tgt,
|
|
469
|
+
kind=mode,
|
|
470
|
+
file=file_str,
|
|
471
|
+
line=line,
|
|
472
|
+
confidence="INFERRED",
|
|
473
|
+
receiver=None,
|
|
474
|
+
))
|
|
475
|
+
|
|
476
|
+
elif node.type == "call_expression":
|
|
477
|
+
func_child = node.child_by_field_name("function")
|
|
478
|
+
c_callee: str | None = None
|
|
479
|
+
c_recv: str | None = None
|
|
480
|
+
|
|
481
|
+
if func_child and func_child.type == "identifier":
|
|
482
|
+
c_callee = _text(func_child)
|
|
483
|
+
elif func_child and func_child.type == "field_expression":
|
|
484
|
+
arg_node = func_child.child_by_field_name("argument")
|
|
485
|
+
field_node = func_child.child_by_field_name("field")
|
|
486
|
+
if field_node is not None and field_node.type == "field_identifier":
|
|
487
|
+
c_callee = _text(field_node)
|
|
488
|
+
if arg_node is not None:
|
|
489
|
+
c_recv = _text(arg_node)
|
|
490
|
+
|
|
491
|
+
if c_callee:
|
|
492
|
+
source = _find_enclosing_function(node, "c")
|
|
493
|
+
if source is not None:
|
|
494
|
+
edges.append(
|
|
495
|
+
Edge(
|
|
496
|
+
source=source,
|
|
497
|
+
target=c_callee,
|
|
498
|
+
kind="call",
|
|
499
|
+
file=file_str,
|
|
500
|
+
line=node.start_point[0] + 1,
|
|
501
|
+
confidence="INFERRED",
|
|
502
|
+
receiver=c_recv,
|
|
503
|
+
)
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
for child in node.children:
|
|
507
|
+
_walk(child)
|
|
508
|
+
|
|
509
|
+
for child in root.children:
|
|
510
|
+
_walk(child)
|
|
511
|
+
|
|
512
|
+
return edges
|
|
513
|
+
except Exception: # noqa: BLE001
|
|
514
|
+
logger.debug(
|
|
515
|
+
"_extract_edges_c: unhandled exception for file=%s",
|
|
516
|
+
filepath,
|
|
517
|
+
exc_info=True,
|
|
518
|
+
)
|
|
519
|
+
return []
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _extract_comments_c(root: Node, filepath: Path) -> list[Comment]:
|
|
523
|
+
"""Walk a C AST and extract semantic comment markers (WHY/HACK/NOTE/TODO/FIXME).
|
|
524
|
+
|
|
525
|
+
C uses a single 'comment' node type for both // line and /* */ block comments.
|
|
526
|
+
"""
|
|
527
|
+
try:
|
|
528
|
+
comments: list[Comment] = []
|
|
529
|
+
|
|
530
|
+
def _walk(node: Node) -> None:
|
|
531
|
+
if node.type == "comment":
|
|
532
|
+
raw = _text(node)
|
|
533
|
+
base_row = node.start_point[0] + 1
|
|
534
|
+
if raw.startswith("/*"):
|
|
535
|
+
for offset, body in _block_comment_lines(raw):
|
|
536
|
+
result = _match_marker(body)
|
|
537
|
+
if result is not None:
|
|
538
|
+
marker, text = result
|
|
539
|
+
comments.append(
|
|
540
|
+
Comment(marker=marker, text=text, line=base_row + offset)
|
|
541
|
+
)
|
|
542
|
+
else:
|
|
543
|
+
body = raw.lstrip("/").strip()
|
|
544
|
+
result = _match_marker(body)
|
|
545
|
+
if result is not None:
|
|
546
|
+
marker, text = result
|
|
547
|
+
comments.append(Comment(marker=marker, text=text, line=base_row))
|
|
548
|
+
|
|
549
|
+
for child in node.children:
|
|
550
|
+
_walk(child)
|
|
551
|
+
|
|
552
|
+
for child in root.children:
|
|
553
|
+
_walk(child)
|
|
554
|
+
|
|
555
|
+
return comments
|
|
556
|
+
except Exception: # noqa: BLE001
|
|
557
|
+
logger.debug(
|
|
558
|
+
"_extract_comments_c: unhandled exception for file=%s",
|
|
559
|
+
filepath,
|
|
560
|
+
exc_info=True,
|
|
561
|
+
)
|
|
562
|
+
return []
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Thin re-exporter: C + C++ symbol/edge/comment extractors.
|
|
2
|
+
|
|
3
|
+
LAYER: imports from graph_c (leaf) and graph_cpp (leaf) — never from graph.py.
|
|
4
|
+
|
|
5
|
+
WHY this file exists: graph.py imports all family extractors through stable public names.
|
|
6
|
+
The C and C++ extractors were split into graph_c.py and graph_cpp.py when
|
|
7
|
+
graph_c_cpp.py exceeded 1000 lines (Tier B additions). This re-exporter keeps
|
|
8
|
+
graph.py's import stable.
|
|
9
|
+
|
|
10
|
+
graph.py continues to import from this module:
|
|
11
|
+
from seam.indexer.graph_c_cpp import (
|
|
12
|
+
_extract_comments_c, _extract_comments_cpp,
|
|
13
|
+
_extract_edges_c, _extract_edges_cpp,
|
|
14
|
+
_extract_symbols_c, _extract_symbols_cpp,
|
|
15
|
+
)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# Re-export C extractors — the actual code lives in graph_c.py.
|
|
19
|
+
from seam.indexer.graph_c import (
|
|
20
|
+
_extract_comments_c,
|
|
21
|
+
_extract_edges_c,
|
|
22
|
+
_extract_symbols_c,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Re-export C++ extractors — the actual code lives in graph_cpp.py.
|
|
26
|
+
from seam.indexer.graph_cpp import (
|
|
27
|
+
_extract_comments_cpp,
|
|
28
|
+
_extract_edges_cpp,
|
|
29
|
+
_extract_symbols_cpp,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"_extract_comments_c",
|
|
34
|
+
"_extract_edges_c",
|
|
35
|
+
"_extract_symbols_c",
|
|
36
|
+
"_extract_comments_cpp",
|
|
37
|
+
"_extract_edges_cpp",
|
|
38
|
+
"_extract_symbols_cpp",
|
|
39
|
+
]
|