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,780 @@
|
|
|
1
|
+
"""Phase 9 node-field extractors — Java, C#, Ruby, C, C++, PHP.
|
|
2
|
+
|
|
3
|
+
LAYER: pure leaf module — imports only stdlib + tree_sitter.
|
|
4
|
+
Must NOT import from any other seam.indexer module (same contract as signatures.py).
|
|
5
|
+
|
|
6
|
+
LAYERING:
|
|
7
|
+
signatures_ext (this file — leaf, no seam deps)
|
|
8
|
+
↑
|
|
9
|
+
signatures.py (dispatch entry point — imports this at top level)
|
|
10
|
+
|
|
11
|
+
Entry points (called from signatures.extract_node_fields):
|
|
12
|
+
_extract_java / _extract_csharp / _extract_ruby
|
|
13
|
+
_extract_c / _extract_cpp / _extract_php
|
|
14
|
+
|
|
15
|
+
Each function signature: (node, qualified_name, max_sig_len) -> NodeFields
|
|
16
|
+
NEVER raises. On any failure, returns _safe_defaults(qualified_name).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from typing import TypedDict
|
|
21
|
+
|
|
22
|
+
from tree_sitter import Node
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NodeFields(TypedDict):
|
|
28
|
+
"""Five enrichment fields extracted per symbol node (all nullable).
|
|
29
|
+
|
|
30
|
+
WHY re-declared here rather than imported from signatures.py:
|
|
31
|
+
signatures_ext must stay a leaf (no seam deps). Importing NodeFields from
|
|
32
|
+
signatures.py would create a cycle (signatures.py imports signatures_ext).
|
|
33
|
+
The TypedDict shape is identical — this is intentional duplication to
|
|
34
|
+
preserve the leaf property of both modules.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
signature: str | None
|
|
38
|
+
decorators: list[str]
|
|
39
|
+
is_exported: bool | None
|
|
40
|
+
visibility: str | None
|
|
41
|
+
qualified_name: str | None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _safe_defaults(qualified_name: str | None = None) -> NodeFields:
|
|
45
|
+
"""Return a safe NodeFields with all nulls/empty. Used on any extraction failure."""
|
|
46
|
+
return NodeFields(
|
|
47
|
+
signature=None,
|
|
48
|
+
decorators=[],
|
|
49
|
+
is_exported=None,
|
|
50
|
+
visibility=None,
|
|
51
|
+
qualified_name=qualified_name,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _text(node: Node | None) -> str:
|
|
56
|
+
"""Safely decode a tree-sitter node's text bytes to str."""
|
|
57
|
+
if node is None:
|
|
58
|
+
return ""
|
|
59
|
+
raw = node.text
|
|
60
|
+
if raw is None:
|
|
61
|
+
return ""
|
|
62
|
+
return raw.decode("utf-8", errors="replace")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _normalize_to_one_line(text: str) -> str:
|
|
66
|
+
"""Collapse embedded newlines and multiple spaces into a single space."""
|
|
67
|
+
return " ".join(text.split())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _truncate(text: str, max_len: int) -> str:
|
|
71
|
+
"""Truncate text to max_len characters, appending '...' if needed."""
|
|
72
|
+
if len(text) <= max_len:
|
|
73
|
+
return text
|
|
74
|
+
return text[: max_len - 3] + "..."
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ── Java ──────────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
_JAVA_VISIBILITY_KEYWORDS: frozenset[str] = frozenset({"public", "private", "protected"})
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _java_modifiers(node: Node) -> Node | None:
|
|
84
|
+
"""Find the 'modifiers' child of a Java declaration node, or None."""
|
|
85
|
+
for child in node.children:
|
|
86
|
+
if child.type == "modifiers":
|
|
87
|
+
return child
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _java_visibility(node: Node) -> str | None:
|
|
92
|
+
"""Extract visibility from a Java declaration's modifiers child.
|
|
93
|
+
|
|
94
|
+
Scans modifiers children for public/private/protected keyword nodes.
|
|
95
|
+
Returns None for package-private (no access modifier present).
|
|
96
|
+
"""
|
|
97
|
+
mods = _java_modifiers(node)
|
|
98
|
+
if mods is None:
|
|
99
|
+
return None
|
|
100
|
+
try:
|
|
101
|
+
for child in mods.children:
|
|
102
|
+
if child.type in _JAVA_VISIBILITY_KEYWORDS:
|
|
103
|
+
return child.type
|
|
104
|
+
except Exception: # noqa: BLE001
|
|
105
|
+
pass
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _java_annotations(node: Node) -> list[str]:
|
|
110
|
+
"""Extract Java annotation texts (@Service, @Override, etc.) from a declaration node.
|
|
111
|
+
|
|
112
|
+
Looks inside the modifiers child for marker_annotation and annotation nodes.
|
|
113
|
+
Returns verbatim text (e.g. '@Service', '@SuppressWarnings("all")').
|
|
114
|
+
"""
|
|
115
|
+
result: list[str] = []
|
|
116
|
+
mods = _java_modifiers(node)
|
|
117
|
+
if mods is None:
|
|
118
|
+
return result
|
|
119
|
+
try:
|
|
120
|
+
for child in mods.children:
|
|
121
|
+
if child.type in ("marker_annotation", "annotation"):
|
|
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
|
+
def _java_signature(node: Node) -> str | None:
|
|
131
|
+
"""Build a one-line Java signature from a declaration node.
|
|
132
|
+
|
|
133
|
+
Covers: class_declaration, interface_declaration, enum_declaration,
|
|
134
|
+
record_declaration, method_declaration, constructor_declaration.
|
|
135
|
+
|
|
136
|
+
Strategy: collect text from all children BEFORE the body (class_body, block,
|
|
137
|
+
constructor_body, enum_body, interface_body), join, and normalize to one line.
|
|
138
|
+
"""
|
|
139
|
+
try:
|
|
140
|
+
body_types = frozenset(
|
|
141
|
+
{
|
|
142
|
+
"class_body",
|
|
143
|
+
"block",
|
|
144
|
+
"constructor_body",
|
|
145
|
+
"enum_body",
|
|
146
|
+
"interface_body",
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
parts: list[str] = []
|
|
150
|
+
for child in node.children:
|
|
151
|
+
if child.type in body_types:
|
|
152
|
+
break
|
|
153
|
+
text = _text(child).strip()
|
|
154
|
+
if text:
|
|
155
|
+
parts.append(text)
|
|
156
|
+
if not parts:
|
|
157
|
+
return None
|
|
158
|
+
return _normalize_to_one_line(" ".join(parts))
|
|
159
|
+
except Exception: # noqa: BLE001
|
|
160
|
+
pass
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _extract_java(
|
|
165
|
+
node: Node,
|
|
166
|
+
qualified_name: str | None,
|
|
167
|
+
max_sig_len: int,
|
|
168
|
+
) -> NodeFields:
|
|
169
|
+
"""Extract all five enrichment fields for a Java symbol node.
|
|
170
|
+
|
|
171
|
+
signature : declaration header up to (but not including) the body, one line.
|
|
172
|
+
decorators : Java annotations verbatim (@Override, @Service, etc.).
|
|
173
|
+
is_exported : True when 'public' modifier is present.
|
|
174
|
+
visibility : 'public' | 'private' | 'protected' | None (package-private).
|
|
175
|
+
qualified_name : passed through from the caller.
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
vis = _java_visibility(node)
|
|
179
|
+
is_exported = vis == "public"
|
|
180
|
+
annotations = _java_annotations(node)
|
|
181
|
+
|
|
182
|
+
sig = _java_signature(node)
|
|
183
|
+
if sig is not None:
|
|
184
|
+
sig = _truncate(sig, max_sig_len)
|
|
185
|
+
|
|
186
|
+
return NodeFields(
|
|
187
|
+
signature=sig,
|
|
188
|
+
decorators=annotations,
|
|
189
|
+
is_exported=is_exported,
|
|
190
|
+
visibility=vis,
|
|
191
|
+
qualified_name=qualified_name,
|
|
192
|
+
)
|
|
193
|
+
except Exception: # noqa: BLE001
|
|
194
|
+
return _safe_defaults(qualified_name)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ── C# ────────────────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
_CSHARP_VISIBILITY_KEYWORDS: frozenset[str] = frozenset(
|
|
201
|
+
{
|
|
202
|
+
"public",
|
|
203
|
+
"private",
|
|
204
|
+
"protected",
|
|
205
|
+
"internal",
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _csharp_modifier_text(node: Node) -> str | None:
|
|
211
|
+
"""Find the first access modifier from a C# declaration's 'modifier' children.
|
|
212
|
+
|
|
213
|
+
Returns 'public', 'private', 'protected', or 'internal' if found, else None.
|
|
214
|
+
"""
|
|
215
|
+
try:
|
|
216
|
+
for child in node.children:
|
|
217
|
+
if child.type == "modifier":
|
|
218
|
+
text = _text(child).strip()
|
|
219
|
+
if text in _CSHARP_VISIBILITY_KEYWORDS:
|
|
220
|
+
return text
|
|
221
|
+
except Exception: # noqa: BLE001
|
|
222
|
+
pass
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _csharp_attributes_texts(node: Node) -> list[str]:
|
|
227
|
+
"""Extract C# attribute lists ([Serializable], [HttpGet], etc.) verbatim.
|
|
228
|
+
|
|
229
|
+
Attribute lists appear as 'attribute_list' children before the modifier.
|
|
230
|
+
Returns each attribute_list as a verbatim string.
|
|
231
|
+
"""
|
|
232
|
+
result: list[str] = []
|
|
233
|
+
try:
|
|
234
|
+
for child in node.children:
|
|
235
|
+
if child.type == "attribute_list":
|
|
236
|
+
text = _text(child).strip()
|
|
237
|
+
if text:
|
|
238
|
+
result.append(text)
|
|
239
|
+
except Exception: # noqa: BLE001
|
|
240
|
+
pass
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _csharp_signature(node: Node) -> str | None:
|
|
245
|
+
"""Build a one-line C# signature from a declaration node.
|
|
246
|
+
|
|
247
|
+
Covers: class_declaration, struct_declaration, record_declaration,
|
|
248
|
+
interface_declaration, enum_declaration, delegate_declaration,
|
|
249
|
+
method_declaration, constructor_declaration.
|
|
250
|
+
|
|
251
|
+
Strategy: collect text from all children BEFORE the body (block, declaration_list,
|
|
252
|
+
enum_member_declaration_list), join, normalize. Skip attribute_list children
|
|
253
|
+
(those are decorators, not part of the signature header).
|
|
254
|
+
"""
|
|
255
|
+
try:
|
|
256
|
+
body_types = frozenset(
|
|
257
|
+
{
|
|
258
|
+
"block",
|
|
259
|
+
"declaration_list",
|
|
260
|
+
"enum_member_declaration_list",
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
skip_types = frozenset({"attribute_list"})
|
|
264
|
+
parts: list[str] = []
|
|
265
|
+
for child in node.children:
|
|
266
|
+
if child.type in body_types:
|
|
267
|
+
break
|
|
268
|
+
if child.type in skip_types:
|
|
269
|
+
continue
|
|
270
|
+
text = _text(child).strip()
|
|
271
|
+
if text:
|
|
272
|
+
parts.append(text)
|
|
273
|
+
if not parts:
|
|
274
|
+
return None
|
|
275
|
+
return _normalize_to_one_line(" ".join(parts))
|
|
276
|
+
except Exception: # noqa: BLE001
|
|
277
|
+
pass
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _extract_csharp(
|
|
282
|
+
node: Node,
|
|
283
|
+
qualified_name: str | None,
|
|
284
|
+
max_sig_len: int,
|
|
285
|
+
) -> NodeFields:
|
|
286
|
+
"""Extract all five enrichment fields for a C# symbol node.
|
|
287
|
+
|
|
288
|
+
signature : declaration header up to (but not including) the body, one line.
|
|
289
|
+
decorators : C# attributes verbatim ([Serializable], [HttpGet], etc.).
|
|
290
|
+
is_exported : True when 'public' modifier is present.
|
|
291
|
+
visibility : 'public' | 'private' | 'protected' | 'internal' | None.
|
|
292
|
+
qualified_name : passed through from the caller.
|
|
293
|
+
"""
|
|
294
|
+
try:
|
|
295
|
+
vis = _csharp_modifier_text(node)
|
|
296
|
+
is_exported = vis == "public"
|
|
297
|
+
attributes = _csharp_attributes_texts(node)
|
|
298
|
+
|
|
299
|
+
sig = _csharp_signature(node)
|
|
300
|
+
if sig is not None:
|
|
301
|
+
sig = _truncate(sig, max_sig_len)
|
|
302
|
+
|
|
303
|
+
return NodeFields(
|
|
304
|
+
signature=sig,
|
|
305
|
+
decorators=attributes,
|
|
306
|
+
is_exported=is_exported,
|
|
307
|
+
visibility=vis,
|
|
308
|
+
qualified_name=qualified_name,
|
|
309
|
+
)
|
|
310
|
+
except Exception: # noqa: BLE001
|
|
311
|
+
return _safe_defaults(qualified_name)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ── Ruby ──────────────────────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _ruby_signature(node: Node) -> str | None:
|
|
318
|
+
"""Build a one-line Ruby signature from a method, class, or module node.
|
|
319
|
+
|
|
320
|
+
For method nodes: capture everything from 'def' up to (but not including)
|
|
321
|
+
the body_statement. For class/module nodes: capture 'class Name' / 'module Name'.
|
|
322
|
+
|
|
323
|
+
Covers: method, singleton_method, class, module nodes.
|
|
324
|
+
Strategy: collect text from all children BEFORE body_statement, join, normalize.
|
|
325
|
+
SKIP 'comment' child nodes — inline comments like `module Utils # NOTE: ...`
|
|
326
|
+
can appear between the name (constant) and body_statement and must not be
|
|
327
|
+
included in the signature text.
|
|
328
|
+
"""
|
|
329
|
+
try:
|
|
330
|
+
body_stop_types = frozenset({"body_statement", "end"})
|
|
331
|
+
# WHY skip comment: class/module nodes may have inline comment children
|
|
332
|
+
# between the class/module name and the body_statement. Including them
|
|
333
|
+
# would corrupt the signature (e.g. 'module Utils # NOTE: ...').
|
|
334
|
+
skip_types = frozenset({"comment"})
|
|
335
|
+
parts: list[str] = []
|
|
336
|
+
for child in node.children:
|
|
337
|
+
if child.type in body_stop_types:
|
|
338
|
+
break
|
|
339
|
+
if child.type in skip_types:
|
|
340
|
+
continue # skip inline comments
|
|
341
|
+
text = _text(child).strip()
|
|
342
|
+
if text:
|
|
343
|
+
parts.append(text)
|
|
344
|
+
if not parts:
|
|
345
|
+
return None
|
|
346
|
+
return _normalize_to_one_line(" ".join(parts))
|
|
347
|
+
except Exception: # noqa: BLE001
|
|
348
|
+
pass
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _extract_ruby(
|
|
353
|
+
node: Node,
|
|
354
|
+
qualified_name: str | None,
|
|
355
|
+
max_sig_len: int,
|
|
356
|
+
) -> NodeFields:
|
|
357
|
+
"""Extract enrichment fields for a Ruby symbol node.
|
|
358
|
+
|
|
359
|
+
signature : method/class declaration header (best-effort, one line).
|
|
360
|
+
decorators : [] (Ruby has no decorator/annotation syntax).
|
|
361
|
+
is_exported : None (Ruby visibility is dynamic — private/protected not tracked at
|
|
362
|
+
extraction time; tracking would require stateful AST traversal).
|
|
363
|
+
visibility : None (same reason — deferred to a future enhancement).
|
|
364
|
+
qualified_name : passed through from the caller.
|
|
365
|
+
|
|
366
|
+
WHY None for visibility: Ruby's visibility model (private/protected/public) is
|
|
367
|
+
set by method calls (private :foo) rather than keywords on the declaration itself.
|
|
368
|
+
Statically extracting it correctly would require tracking call context — out of
|
|
369
|
+
scope for this MVP (same limitation noted in the spec).
|
|
370
|
+
"""
|
|
371
|
+
try:
|
|
372
|
+
sig = _ruby_signature(node)
|
|
373
|
+
if sig is not None:
|
|
374
|
+
sig = _truncate(sig, max_sig_len)
|
|
375
|
+
|
|
376
|
+
return NodeFields(
|
|
377
|
+
signature=sig,
|
|
378
|
+
decorators=[], # Ruby has no annotation/decorator syntax
|
|
379
|
+
is_exported=None, # Dynamic visibility — not tracked statically
|
|
380
|
+
visibility=None, # Dynamic visibility — not tracked statically
|
|
381
|
+
qualified_name=qualified_name,
|
|
382
|
+
)
|
|
383
|
+
except Exception: # noqa: BLE001
|
|
384
|
+
return _safe_defaults(qualified_name)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
# ── C ─────────────────────────────────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _c_is_static(node: Node) -> bool:
|
|
391
|
+
"""Return True if a C function_definition has a 'static' storage_class_specifier."""
|
|
392
|
+
try:
|
|
393
|
+
for child in node.children:
|
|
394
|
+
if child.type == "storage_class_specifier":
|
|
395
|
+
if _text(child).strip() == "static":
|
|
396
|
+
return True
|
|
397
|
+
except Exception: # noqa: BLE001
|
|
398
|
+
pass
|
|
399
|
+
return False
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _c_signature(node: Node) -> str | None:
|
|
403
|
+
"""Build a one-line C signature from a declaration node.
|
|
404
|
+
|
|
405
|
+
Strategy: collect text from all children BEFORE the body (compound_statement,
|
|
406
|
+
field_declaration_list, enumerator_list), join, and normalize to one line.
|
|
407
|
+
|
|
408
|
+
Covers: function_definition, struct_specifier, union_specifier, enum_specifier,
|
|
409
|
+
type_definition.
|
|
410
|
+
"""
|
|
411
|
+
try:
|
|
412
|
+
body_types = frozenset(
|
|
413
|
+
{
|
|
414
|
+
"compound_statement",
|
|
415
|
+
"field_declaration_list",
|
|
416
|
+
"enumerator_list",
|
|
417
|
+
}
|
|
418
|
+
)
|
|
419
|
+
parts: list[str] = []
|
|
420
|
+
for child in node.children:
|
|
421
|
+
if child.type in body_types:
|
|
422
|
+
break
|
|
423
|
+
text = _text(child).strip()
|
|
424
|
+
if text:
|
|
425
|
+
parts.append(text)
|
|
426
|
+
if not parts:
|
|
427
|
+
return None
|
|
428
|
+
return _normalize_to_one_line(" ".join(parts))
|
|
429
|
+
except Exception: # noqa: BLE001
|
|
430
|
+
pass
|
|
431
|
+
return None
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _extract_c(
|
|
435
|
+
node: Node,
|
|
436
|
+
qualified_name: str | None,
|
|
437
|
+
max_sig_len: int,
|
|
438
|
+
) -> NodeFields:
|
|
439
|
+
"""Extract all five enrichment fields for a C symbol node.
|
|
440
|
+
|
|
441
|
+
signature : declaration header up to (but not including) the body, one line.
|
|
442
|
+
decorators : [] (C has no decorator syntax).
|
|
443
|
+
is_exported : False when 'static' storage class is present (file-local); True otherwise.
|
|
444
|
+
visibility : 'private' for static functions (file-local); 'public' otherwise.
|
|
445
|
+
qualified_name : passed through from the caller.
|
|
446
|
+
|
|
447
|
+
WHY static→private: C 'static' at file scope means the function/variable is not
|
|
448
|
+
visible outside the translation unit — the closest equivalent to 'private'.
|
|
449
|
+
"""
|
|
450
|
+
try:
|
|
451
|
+
is_static = _c_is_static(node)
|
|
452
|
+
vis = "private" if is_static else "public"
|
|
453
|
+
is_exported = not is_static
|
|
454
|
+
|
|
455
|
+
sig = _c_signature(node)
|
|
456
|
+
if sig is not None:
|
|
457
|
+
sig = _truncate(sig, max_sig_len)
|
|
458
|
+
|
|
459
|
+
return NodeFields(
|
|
460
|
+
signature=sig,
|
|
461
|
+
decorators=[],
|
|
462
|
+
is_exported=is_exported,
|
|
463
|
+
visibility=vis,
|
|
464
|
+
qualified_name=qualified_name,
|
|
465
|
+
)
|
|
466
|
+
except Exception: # noqa: BLE001
|
|
467
|
+
return _safe_defaults(qualified_name)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# ── C++ ───────────────────────────────────────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _cpp_signature(node: Node) -> str | None:
|
|
474
|
+
"""Build a one-line C++ signature from a declaration node.
|
|
475
|
+
|
|
476
|
+
Strategy: collect text from all children BEFORE the body (compound_statement,
|
|
477
|
+
field_declaration_list, enumerator_list, declaration_list), join, normalize.
|
|
478
|
+
|
|
479
|
+
Covers: class_specifier, struct_specifier, union_specifier, enum_specifier,
|
|
480
|
+
function_definition.
|
|
481
|
+
"""
|
|
482
|
+
try:
|
|
483
|
+
body_types = frozenset(
|
|
484
|
+
{
|
|
485
|
+
"compound_statement",
|
|
486
|
+
"field_declaration_list",
|
|
487
|
+
"enumerator_list",
|
|
488
|
+
"declaration_list",
|
|
489
|
+
}
|
|
490
|
+
)
|
|
491
|
+
parts: list[str] = []
|
|
492
|
+
for child in node.children:
|
|
493
|
+
if child.type in body_types:
|
|
494
|
+
break
|
|
495
|
+
# Skip field_initializer_list (constructor initializer : x(v)) from signature
|
|
496
|
+
if child.type == "field_initializer_list":
|
|
497
|
+
break
|
|
498
|
+
text = _text(child).strip()
|
|
499
|
+
if text:
|
|
500
|
+
parts.append(text)
|
|
501
|
+
if not parts:
|
|
502
|
+
return None
|
|
503
|
+
return _normalize_to_one_line(" ".join(parts))
|
|
504
|
+
except Exception: # noqa: BLE001
|
|
505
|
+
pass
|
|
506
|
+
return None
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _extract_cpp(
|
|
510
|
+
node: Node,
|
|
511
|
+
qualified_name: str | None,
|
|
512
|
+
max_sig_len: int,
|
|
513
|
+
) -> NodeFields:
|
|
514
|
+
"""Extract all five enrichment fields for a C++ symbol node.
|
|
515
|
+
|
|
516
|
+
signature : declaration header up to (but not including) the body, one line.
|
|
517
|
+
decorators : [] (C++ [[...]] attributes are out of scope per PRD).
|
|
518
|
+
is_exported : True for public class members; True for free functions; None for
|
|
519
|
+
non-class context where visibility is undecidable at extraction time.
|
|
520
|
+
visibility : Not tracked per-member in this MVP (would require tracking
|
|
521
|
+
access_specifier state across the class body); returns None.
|
|
522
|
+
qualified_name : passed through from the caller.
|
|
523
|
+
|
|
524
|
+
WHY None for visibility: C++ visibility depends on the current access_specifier
|
|
525
|
+
(public/private/protected) within the class body, which would require stateful
|
|
526
|
+
tracking during AST traversal. This is deferred to a future enhancement.
|
|
527
|
+
"""
|
|
528
|
+
try:
|
|
529
|
+
sig = _cpp_signature(node)
|
|
530
|
+
if sig is not None:
|
|
531
|
+
sig = _truncate(sig, max_sig_len)
|
|
532
|
+
|
|
533
|
+
return NodeFields(
|
|
534
|
+
signature=sig,
|
|
535
|
+
decorators=[],
|
|
536
|
+
is_exported=True, # Conservative: assume exportable at extraction time
|
|
537
|
+
visibility=None, # Stateful tracking required; out of scope for MVP
|
|
538
|
+
qualified_name=qualified_name,
|
|
539
|
+
)
|
|
540
|
+
except Exception: # noqa: BLE001
|
|
541
|
+
return _safe_defaults(qualified_name)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
# ── PHP ───────────────────────────────────────────────────────────────────────
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
_PHP_VISIBILITY_KEYWORDS: frozenset[str] = frozenset({"public", "private", "protected"})
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _php_visibility(node: Node) -> str | None:
|
|
551
|
+
"""Extract visibility from a PHP method_declaration or property_declaration node.
|
|
552
|
+
|
|
553
|
+
Looks for a visibility_modifier child and reads its keyword child.
|
|
554
|
+
Returns 'public' | 'private' | 'protected' | None.
|
|
555
|
+
"""
|
|
556
|
+
try:
|
|
557
|
+
for child in node.children:
|
|
558
|
+
if child.type == "visibility_modifier":
|
|
559
|
+
for kw in child.children:
|
|
560
|
+
if kw.type in _PHP_VISIBILITY_KEYWORDS:
|
|
561
|
+
return kw.type
|
|
562
|
+
except Exception: # noqa: BLE001
|
|
563
|
+
pass
|
|
564
|
+
return None
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _php_attributes(node: Node) -> list[str]:
|
|
568
|
+
"""Extract PHP 8 attribute lists (#[...]) verbatim from a declaration node.
|
|
569
|
+
|
|
570
|
+
PHP attributes appear as attribute_list children before visibility_modifier.
|
|
571
|
+
Each attribute_list (which may contain one or more attribute_group entries)
|
|
572
|
+
is returned verbatim (e.g. "#[Route('/users')]").
|
|
573
|
+
"""
|
|
574
|
+
result: list[str] = []
|
|
575
|
+
try:
|
|
576
|
+
for child in node.children:
|
|
577
|
+
if child.type == "attribute_list":
|
|
578
|
+
text = _text(child).strip()
|
|
579
|
+
if text:
|
|
580
|
+
result.append(text)
|
|
581
|
+
except Exception: # noqa: BLE001
|
|
582
|
+
pass
|
|
583
|
+
return result
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _php_signature(node: Node) -> str | None:
|
|
587
|
+
"""Build a one-line PHP signature from a declaration node.
|
|
588
|
+
|
|
589
|
+
Covers: class_declaration, interface_declaration, trait_declaration,
|
|
590
|
+
enum_declaration, function_definition, method_declaration.
|
|
591
|
+
|
|
592
|
+
Strategy: collect text from all children BEFORE the body (compound_statement,
|
|
593
|
+
declaration_list, enum_declaration_list), join, normalize. Skip attribute_list
|
|
594
|
+
children (those are decorators, not signature header).
|
|
595
|
+
"""
|
|
596
|
+
try:
|
|
597
|
+
body_stop_types = frozenset(
|
|
598
|
+
{
|
|
599
|
+
"compound_statement",
|
|
600
|
+
"declaration_list",
|
|
601
|
+
"enum_declaration_list",
|
|
602
|
+
}
|
|
603
|
+
)
|
|
604
|
+
skip_types = frozenset({"attribute_list"})
|
|
605
|
+
parts: list[str] = []
|
|
606
|
+
for child in node.children:
|
|
607
|
+
if child.type in body_stop_types:
|
|
608
|
+
break
|
|
609
|
+
if child.type in skip_types:
|
|
610
|
+
continue # skip attributes — those are decorators
|
|
611
|
+
text = _text(child).strip()
|
|
612
|
+
if text:
|
|
613
|
+
parts.append(text)
|
|
614
|
+
if not parts:
|
|
615
|
+
return None
|
|
616
|
+
return _normalize_to_one_line(" ".join(parts))
|
|
617
|
+
except Exception: # noqa: BLE001
|
|
618
|
+
pass
|
|
619
|
+
return None
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _extract_php(
|
|
623
|
+
node: Node,
|
|
624
|
+
qualified_name: str | None,
|
|
625
|
+
max_sig_len: int,
|
|
626
|
+
) -> NodeFields:
|
|
627
|
+
"""Extract all five enrichment fields for a PHP symbol node.
|
|
628
|
+
|
|
629
|
+
signature : declaration header up to (but not including) the body, one line.
|
|
630
|
+
decorators : PHP 8 attributes (#[Route(...)]) verbatim from attribute_list children.
|
|
631
|
+
is_exported : True when 'public' modifier is present.
|
|
632
|
+
visibility : 'public' | 'private' | 'protected' | None (no modifier).
|
|
633
|
+
qualified_name : passed through from the caller.
|
|
634
|
+
"""
|
|
635
|
+
try:
|
|
636
|
+
vis = _php_visibility(node)
|
|
637
|
+
is_exported = vis == "public"
|
|
638
|
+
attributes = _php_attributes(node)
|
|
639
|
+
|
|
640
|
+
sig = _php_signature(node)
|
|
641
|
+
if sig is not None:
|
|
642
|
+
sig = _truncate(sig, max_sig_len)
|
|
643
|
+
|
|
644
|
+
return NodeFields(
|
|
645
|
+
signature=sig,
|
|
646
|
+
decorators=attributes,
|
|
647
|
+
is_exported=is_exported,
|
|
648
|
+
visibility=vis,
|
|
649
|
+
qualified_name=qualified_name,
|
|
650
|
+
)
|
|
651
|
+
except Exception: # noqa: BLE001
|
|
652
|
+
return _safe_defaults(qualified_name)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
# ── Swift ─────────────────────────────────────────────────────────────────────
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def _swift_ext_modifiers(node: Node) -> Node | None:
|
|
659
|
+
"""Find the 'modifiers' child of a Swift declaration node, or None."""
|
|
660
|
+
for child in node.children:
|
|
661
|
+
if child.type == "modifiers":
|
|
662
|
+
return child
|
|
663
|
+
return None
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _swift_ext_visibility(node: Node) -> tuple[str, bool]:
|
|
667
|
+
"""Extract (visibility_str, is_exported) from a Swift declaration modifiers.
|
|
668
|
+
|
|
669
|
+
public / open → ('public', True)
|
|
670
|
+
private / fileprivate → ('private', False)
|
|
671
|
+
internal (explicit or absent) → ('internal', False)
|
|
672
|
+
"""
|
|
673
|
+
mods = _swift_ext_modifiers(node)
|
|
674
|
+
if mods is None:
|
|
675
|
+
return ("internal", False)
|
|
676
|
+
try:
|
|
677
|
+
for child in mods.children:
|
|
678
|
+
if child.type == "visibility_modifier":
|
|
679
|
+
vis_text = _text(child).strip()
|
|
680
|
+
if vis_text in ("public", "open"):
|
|
681
|
+
return ("public", True)
|
|
682
|
+
if vis_text in ("private", "fileprivate"):
|
|
683
|
+
return ("private", False)
|
|
684
|
+
return ("internal", False)
|
|
685
|
+
except Exception: # noqa: BLE001
|
|
686
|
+
pass
|
|
687
|
+
return ("internal", False)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _swift_ext_attributes(node: Node) -> list[str]:
|
|
691
|
+
"""Extract Swift @attribute decorator texts from a declaration node's modifiers.
|
|
692
|
+
|
|
693
|
+
Returns each attribute verbatim (e.g. '@objc', '@available(iOS 13.0, *)').
|
|
694
|
+
"""
|
|
695
|
+
result: list[str] = []
|
|
696
|
+
mods = _swift_ext_modifiers(node)
|
|
697
|
+
if mods is None:
|
|
698
|
+
return result
|
|
699
|
+
try:
|
|
700
|
+
for child in mods.children:
|
|
701
|
+
if child.type == "attribute":
|
|
702
|
+
text = _text(child).strip()
|
|
703
|
+
if text:
|
|
704
|
+
result.append(text)
|
|
705
|
+
except Exception: # noqa: BLE001
|
|
706
|
+
pass
|
|
707
|
+
return result
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _swift_ext_signature(node: Node) -> str | None:
|
|
711
|
+
"""Build a one-line Swift signature from a declaration node.
|
|
712
|
+
|
|
713
|
+
Collects children before the body (function_body / class_body / enum_class_body /
|
|
714
|
+
protocol_body). Includes visibility_modifier from modifiers but skips @attributes.
|
|
715
|
+
"""
|
|
716
|
+
try:
|
|
717
|
+
body_stop_types = frozenset({
|
|
718
|
+
"function_body",
|
|
719
|
+
"class_body",
|
|
720
|
+
"enum_class_body",
|
|
721
|
+
"protocol_body",
|
|
722
|
+
})
|
|
723
|
+
parts: list[str] = []
|
|
724
|
+
for child in node.children:
|
|
725
|
+
if child.type in body_stop_types:
|
|
726
|
+
break
|
|
727
|
+
if child.type == "modifiers":
|
|
728
|
+
# Include only visibility_modifier in signature text.
|
|
729
|
+
for mc in child.children:
|
|
730
|
+
if mc.type == "visibility_modifier":
|
|
731
|
+
vis = _text(mc).strip()
|
|
732
|
+
if vis:
|
|
733
|
+
parts.append(vis)
|
|
734
|
+
continue
|
|
735
|
+
text = _text(child).strip()
|
|
736
|
+
if text:
|
|
737
|
+
parts.append(text)
|
|
738
|
+
if not parts:
|
|
739
|
+
return None
|
|
740
|
+
return " ".join(" ".join(parts).split())
|
|
741
|
+
except Exception: # noqa: BLE001
|
|
742
|
+
pass
|
|
743
|
+
return None
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _extract_swift(
|
|
747
|
+
node: Node,
|
|
748
|
+
qualified_name: str | None,
|
|
749
|
+
max_sig_len: int,
|
|
750
|
+
) -> NodeFields:
|
|
751
|
+
"""Extract all five enrichment fields for a Swift symbol node.
|
|
752
|
+
|
|
753
|
+
signature : declaration header up to (but not including) the body, one line.
|
|
754
|
+
decorators : Swift @attributes verbatim (@objc, @available(...), etc.).
|
|
755
|
+
is_exported : True when 'public' or 'open' modifier is present.
|
|
756
|
+
visibility : 'public' (public/open) | 'private' (private/fileprivate) |
|
|
757
|
+
'internal' (explicit internal or absent — Swift default).
|
|
758
|
+
qualified_name : passed through from the caller.
|
|
759
|
+
|
|
760
|
+
WHY internal for absent modifier: Swift's default access level is 'internal'
|
|
761
|
+
(visible within the module but not exported). Only public/open cross the module
|
|
762
|
+
boundary (is_exported=True). This mirrors the PRD spec decision.
|
|
763
|
+
"""
|
|
764
|
+
try:
|
|
765
|
+
vis, is_exported = _swift_ext_visibility(node)
|
|
766
|
+
attributes = _swift_ext_attributes(node)
|
|
767
|
+
|
|
768
|
+
sig = _swift_ext_signature(node)
|
|
769
|
+
if sig is not None:
|
|
770
|
+
sig = _truncate(sig, max_sig_len)
|
|
771
|
+
|
|
772
|
+
return NodeFields(
|
|
773
|
+
signature=sig,
|
|
774
|
+
decorators=attributes,
|
|
775
|
+
is_exported=is_exported,
|
|
776
|
+
visibility=vis,
|
|
777
|
+
qualified_name=qualified_name,
|
|
778
|
+
)
|
|
779
|
+
except Exception: # noqa: BLE001
|
|
780
|
+
return _safe_defaults(qualified_name)
|