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,634 @@
|
|
|
1
|
+
"""Node-field extractor for Phase 4 — signature, decorators, is_exported, visibility, qualified_name.
|
|
2
|
+
|
|
3
|
+
LAYER: pure leaf module — imports only stdlib + tree_sitter.
|
|
4
|
+
Must NOT import from any other seam.indexer module (same contract as graph_common.py).
|
|
5
|
+
|
|
6
|
+
Entry point:
|
|
7
|
+
extract_node_fields(node, language, qualified_name=None) -> NodeFields
|
|
8
|
+
|
|
9
|
+
NEVER raises. On any extraction failure, returns the field as None / empty list
|
|
10
|
+
so the caller (graph.py / graph_go_rust.py) can still emit the symbol.
|
|
11
|
+
|
|
12
|
+
Per-language rules (PRD §Implementation Decisions):
|
|
13
|
+
signature : declaration header normalized to one line, truncated to max len.
|
|
14
|
+
decorators : Python/TS verbatim decorator nodes; Go/Rust → always [].
|
|
15
|
+
is_exported : TS/JS: export keyword; Go: uppercase first letter; Rust: pub/pub(crate);
|
|
16
|
+
Python: no single-underscore prefix (heuristic; __all__ is out of scope).
|
|
17
|
+
visibility : Rust: pub/pub(crate)/none → public/crate/private;
|
|
18
|
+
TS/JS: public/private/protected modifiers;
|
|
19
|
+
Python: private if underscore-prefix, else public;
|
|
20
|
+
Go: derived from capitalization.
|
|
21
|
+
qualified_name : passed in from the caller (already resolved by graph.py scope-walking).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
from typing import Any, TypedDict
|
|
26
|
+
|
|
27
|
+
from tree_sitter import Node
|
|
28
|
+
|
|
29
|
+
# Phase 9: import the new-language stub extractors from the companion leaf module.
|
|
30
|
+
# signatures_ext is a leaf (no seam deps) so importing it here does not create a cycle.
|
|
31
|
+
from seam.indexer.signatures_ext import (
|
|
32
|
+
_extract_c,
|
|
33
|
+
_extract_cpp,
|
|
34
|
+
_extract_csharp,
|
|
35
|
+
_extract_java,
|
|
36
|
+
_extract_php,
|
|
37
|
+
_extract_ruby,
|
|
38
|
+
_extract_swift,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
# Fallback when callers omit max_signature_len (e.g. unit tests calling extract_node_fields
|
|
44
|
+
# directly). The real limit comes from seam.config.SEAM_MAX_SIGNATURE_LEN, which callers
|
|
45
|
+
# (graph.py / graph_go_rust.py) read and pass in as a parameter. Keeping config reading
|
|
46
|
+
# in the caller rather than here preserves this module's leaf property: a leaf must not
|
|
47
|
+
# import from any other seam module, including seam.config. Parameter threading is the
|
|
48
|
+
# only way to honour both the config-centralisation rule and the leaf-purity rule.
|
|
49
|
+
# 300 must match the seam.config default so tests and prod see the same truncation point.
|
|
50
|
+
_DEFAULT_MAX_SIG_LEN = 300
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class NodeFields(TypedDict):
|
|
54
|
+
"""Five enrichment fields extracted per symbol node (all nullable)."""
|
|
55
|
+
|
|
56
|
+
signature: str | None # declaration header, single line, truncated
|
|
57
|
+
decorators: list[str] # verbatim decorator strings (Python/TS); [] otherwise
|
|
58
|
+
is_exported: bool | None # True = public API; None = unknown (unsupported language)
|
|
59
|
+
visibility: str | None # "public" | "private" | "protected" | "crate" | None
|
|
60
|
+
qualified_name: str | None # "ClassName.method" or plain name; None for top-level unknown
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ── Safe text helper (mirrors graph_common._text without importing it) ────────
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _text(node: Node | None) -> str:
|
|
67
|
+
"""Safely decode a tree-sitter node's text bytes to str."""
|
|
68
|
+
if node is None:
|
|
69
|
+
return ""
|
|
70
|
+
raw = node.text
|
|
71
|
+
if raw is None:
|
|
72
|
+
return ""
|
|
73
|
+
return raw.decode("utf-8", errors="replace")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _normalize_to_one_line(text: str) -> str:
|
|
77
|
+
"""Collapse any embedded newlines and multiple spaces into a single space."""
|
|
78
|
+
return " ".join(text.split())
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _truncate(text: str, max_len: int) -> str:
|
|
82
|
+
"""Truncate text to max_len characters, appending '...' if truncated."""
|
|
83
|
+
if len(text) <= max_len:
|
|
84
|
+
return text
|
|
85
|
+
return text[: max_len - 3] + "..."
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ── Python extraction ─────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _py_is_exported(name: str) -> bool:
|
|
92
|
+
"""Python export heuristic: name NOT starting with underscore → exported."""
|
|
93
|
+
return not name.startswith("_")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _py_visibility(name: str) -> str:
|
|
97
|
+
"""Python visibility: underscore prefix → private; else public."""
|
|
98
|
+
return "private" if name.startswith("_") else "public"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _py_signature(node: Node) -> str | None:
|
|
102
|
+
"""Extract Python function/class declaration header as a one-line string.
|
|
103
|
+
|
|
104
|
+
For function_definition: "def name(params) -> return_type"
|
|
105
|
+
For class_definition: "class Name(Bases)"
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
node_type = node.type
|
|
109
|
+
|
|
110
|
+
# decorated_definition wraps the inner function_definition; recurse to extract
|
|
111
|
+
# the signature from the actual declaration, not the decorator wrapper node.
|
|
112
|
+
if node_type == "decorated_definition":
|
|
113
|
+
inner = node.child_by_field_name("definition")
|
|
114
|
+
if inner is not None:
|
|
115
|
+
return _py_signature(inner)
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
if node_type == "function_definition":
|
|
119
|
+
name_node = node.child_by_field_name("name")
|
|
120
|
+
params_node = node.child_by_field_name("parameters")
|
|
121
|
+
return_node = node.child_by_field_name("return_type")
|
|
122
|
+
|
|
123
|
+
name = _text(name_node)
|
|
124
|
+
params = _text(params_node)
|
|
125
|
+
parts = [f"def {name}{params}"]
|
|
126
|
+
if return_node is not None:
|
|
127
|
+
# WHY removeprefix not the strip-char-set method:
|
|
128
|
+
# The strip-charset variant treats the argument as a SET of characters to
|
|
129
|
+
# strip, not a prefix string. removeprefix("->") strips only the exact
|
|
130
|
+
# two-char prefix once — correct for return-type nodes whose text starts
|
|
131
|
+
# with "->" (the arrow is part of the node's text in tree-sitter Python).
|
|
132
|
+
ret = _text(return_node).removeprefix("->").strip()
|
|
133
|
+
parts.append(f"-> {ret}")
|
|
134
|
+
return _normalize_to_one_line(" ".join(parts))
|
|
135
|
+
|
|
136
|
+
if node_type == "class_definition":
|
|
137
|
+
name_node = node.child_by_field_name("name")
|
|
138
|
+
superclasses_node = node.child_by_field_name("superclasses")
|
|
139
|
+
name = _text(name_node)
|
|
140
|
+
if superclasses_node:
|
|
141
|
+
bases = _text(superclasses_node)
|
|
142
|
+
return _normalize_to_one_line(f"class {name}{bases}")
|
|
143
|
+
return _normalize_to_one_line(f"class {name}")
|
|
144
|
+
|
|
145
|
+
except Exception as exc: # noqa: BLE001
|
|
146
|
+
# Log at DEBUG so extraction failures surface during development without
|
|
147
|
+
# polluting production logs — callers emit the symbol with signature=None.
|
|
148
|
+
logger.debug("_py_signature extraction failed for node.type=%r: %s", node.type, exc)
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _py_decorators(node: Node) -> list[str]:
|
|
153
|
+
"""Extract Python decorator texts from a decorated_definition node."""
|
|
154
|
+
if node.type != "decorated_definition":
|
|
155
|
+
return []
|
|
156
|
+
decorators: list[str] = []
|
|
157
|
+
try:
|
|
158
|
+
for child in node.children:
|
|
159
|
+
if child.type == "decorator":
|
|
160
|
+
text = _text(child).strip()
|
|
161
|
+
if text:
|
|
162
|
+
decorators.append(text)
|
|
163
|
+
except Exception as exc: # noqa: BLE001
|
|
164
|
+
logger.debug("_py_decorators extraction failed: %s", exc)
|
|
165
|
+
return decorators
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _extract_python(node: Node, qualified_name: str | None, max_sig_len: int) -> NodeFields:
|
|
169
|
+
"""Extract all five fields for a Python node."""
|
|
170
|
+
try:
|
|
171
|
+
# Determine the effective name for export/visibility check
|
|
172
|
+
effective_node = node
|
|
173
|
+
if node.type == "decorated_definition":
|
|
174
|
+
inner = node.child_by_field_name("definition")
|
|
175
|
+
if inner is not None:
|
|
176
|
+
effective_node = inner
|
|
177
|
+
|
|
178
|
+
name_node = effective_node.child_by_field_name("name")
|
|
179
|
+
name = _text(name_node) if name_node else ""
|
|
180
|
+
|
|
181
|
+
sig = _py_signature(node)
|
|
182
|
+
if sig is not None:
|
|
183
|
+
sig = _truncate(sig, max_sig_len)
|
|
184
|
+
|
|
185
|
+
return NodeFields(
|
|
186
|
+
signature=sig,
|
|
187
|
+
decorators=_py_decorators(node),
|
|
188
|
+
is_exported=_py_is_exported(name) if name else None,
|
|
189
|
+
visibility=_py_visibility(name) if name else None,
|
|
190
|
+
qualified_name=qualified_name,
|
|
191
|
+
)
|
|
192
|
+
except Exception: # noqa: BLE001
|
|
193
|
+
return _safe_defaults(qualified_name)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ── TypeScript / JavaScript extraction ───────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
_TS_VISIBILITY_KEYWORDS: frozenset[str] = frozenset({"public", "private", "protected"})
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _ts_is_exported(node: Node) -> bool:
|
|
203
|
+
"""TypeScript/JS export detection: check if parent is export_statement.
|
|
204
|
+
|
|
205
|
+
Covers:
|
|
206
|
+
export function foo() {}
|
|
207
|
+
export class Bar {}
|
|
208
|
+
export interface X {}
|
|
209
|
+
export default function foo() {}
|
|
210
|
+
export default class Foo {}
|
|
211
|
+
|
|
212
|
+
All of these produce an export_statement parent in the tree-sitter grammar.
|
|
213
|
+
The previous code had a duplicate dead branch (two identical checks) — removed.
|
|
214
|
+
"""
|
|
215
|
+
try:
|
|
216
|
+
parent = node.parent
|
|
217
|
+
if parent is None:
|
|
218
|
+
return False
|
|
219
|
+
# export function/class/interface/default: parent is export_statement.
|
|
220
|
+
if parent.type == "export_statement":
|
|
221
|
+
return True
|
|
222
|
+
return False
|
|
223
|
+
except Exception: # noqa: BLE001
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _ts_visibility(node: Node) -> str | None:
|
|
228
|
+
"""TypeScript visibility from access modifier keywords on a method/property."""
|
|
229
|
+
try:
|
|
230
|
+
for child in node.children:
|
|
231
|
+
if child.type in ("public", "private", "protected"):
|
|
232
|
+
return child.type
|
|
233
|
+
# Some tree-sitter grammars store access modifiers as accessibility_modifier
|
|
234
|
+
if child.type == "accessibility_modifier":
|
|
235
|
+
text = _text(child).strip()
|
|
236
|
+
if text in _TS_VISIBILITY_KEYWORDS:
|
|
237
|
+
return text
|
|
238
|
+
except Exception: # noqa: BLE001
|
|
239
|
+
pass
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _ts_decorators(node: Node) -> list[str]:
|
|
244
|
+
"""Extract TypeScript/JS decorator nodes from the node's parent context.
|
|
245
|
+
|
|
246
|
+
In the tree-sitter TypeScript grammar, decorators appear as sibling nodes
|
|
247
|
+
before the class/method declaration, or as decorator nodes attached directly.
|
|
248
|
+
This walks prev_sibling for decorator nodes.
|
|
249
|
+
"""
|
|
250
|
+
decorators: list[str] = []
|
|
251
|
+
try:
|
|
252
|
+
current = node.prev_sibling
|
|
253
|
+
while current is not None and current.type == "decorator":
|
|
254
|
+
text = _text(current).strip()
|
|
255
|
+
if text:
|
|
256
|
+
decorators.insert(0, text) # prepend to preserve order
|
|
257
|
+
current = current.prev_sibling
|
|
258
|
+
except Exception: # noqa: BLE001
|
|
259
|
+
pass
|
|
260
|
+
return decorators
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _ts_signature(node: Node) -> str | None:
|
|
264
|
+
"""Extract TypeScript/JS declaration header as one-line string."""
|
|
265
|
+
try:
|
|
266
|
+
ntype = node.type
|
|
267
|
+
|
|
268
|
+
if ntype == "function_declaration":
|
|
269
|
+
name_node = node.child_by_field_name("name")
|
|
270
|
+
params_node = node.child_by_field_name("parameters")
|
|
271
|
+
ret_node = node.child_by_field_name("return_type")
|
|
272
|
+
name = _text(name_node)
|
|
273
|
+
params = _text(params_node)
|
|
274
|
+
parts = [f"function {name}{params}"]
|
|
275
|
+
if ret_node:
|
|
276
|
+
parts.append(f": {_text(ret_node).lstrip(':').strip()}")
|
|
277
|
+
return _normalize_to_one_line("".join(parts))
|
|
278
|
+
|
|
279
|
+
if ntype == "method_definition":
|
|
280
|
+
name_node = node.child_by_field_name("name")
|
|
281
|
+
params_node = node.child_by_field_name("parameters")
|
|
282
|
+
ret_node = node.child_by_field_name("return_type")
|
|
283
|
+
name = _text(name_node)
|
|
284
|
+
params = _text(params_node)
|
|
285
|
+
parts = [f"{name}{params}"]
|
|
286
|
+
if ret_node:
|
|
287
|
+
parts.append(f": {_text(ret_node).lstrip(':').strip()}")
|
|
288
|
+
return _normalize_to_one_line("".join(parts))
|
|
289
|
+
|
|
290
|
+
if ntype == "class_declaration":
|
|
291
|
+
name_node = node.child_by_field_name("name")
|
|
292
|
+
# type_parameters and class_heritage (implements/extends)
|
|
293
|
+
type_params = node.child_by_field_name("type_parameters")
|
|
294
|
+
heritage = None
|
|
295
|
+
for child in node.children:
|
|
296
|
+
if child.type == "class_heritage":
|
|
297
|
+
heritage = child
|
|
298
|
+
break
|
|
299
|
+
name = _text(name_node)
|
|
300
|
+
sig = f"class {name}"
|
|
301
|
+
if type_params:
|
|
302
|
+
sig += _text(type_params)
|
|
303
|
+
if heritage:
|
|
304
|
+
sig += f" {_text(heritage)}"
|
|
305
|
+
return _normalize_to_one_line(sig)
|
|
306
|
+
|
|
307
|
+
if ntype == "interface_declaration":
|
|
308
|
+
name_node = node.child_by_field_name("name")
|
|
309
|
+
type_params = node.child_by_field_name("type_parameters")
|
|
310
|
+
name = _text(name_node)
|
|
311
|
+
sig = f"interface {name}"
|
|
312
|
+
if type_params:
|
|
313
|
+
sig += _text(type_params)
|
|
314
|
+
return _normalize_to_one_line(sig)
|
|
315
|
+
|
|
316
|
+
if ntype == "type_alias_declaration":
|
|
317
|
+
name_node = node.child_by_field_name("name")
|
|
318
|
+
name = _text(name_node)
|
|
319
|
+
return _normalize_to_one_line(f"type {name}")
|
|
320
|
+
|
|
321
|
+
except Exception: # noqa: BLE001
|
|
322
|
+
pass
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _extract_typescript(node: Node, qualified_name: str | None, max_sig_len: int) -> NodeFields:
|
|
327
|
+
"""Extract all five fields for a TypeScript/JavaScript node."""
|
|
328
|
+
try:
|
|
329
|
+
sig = _ts_signature(node)
|
|
330
|
+
if sig is not None:
|
|
331
|
+
sig = _truncate(sig, max_sig_len)
|
|
332
|
+
|
|
333
|
+
is_exported = _ts_is_exported(node)
|
|
334
|
+
visibility = _ts_visibility(node)
|
|
335
|
+
|
|
336
|
+
return NodeFields(
|
|
337
|
+
signature=sig,
|
|
338
|
+
decorators=_ts_decorators(node),
|
|
339
|
+
is_exported=is_exported,
|
|
340
|
+
visibility=visibility,
|
|
341
|
+
qualified_name=qualified_name,
|
|
342
|
+
)
|
|
343
|
+
except Exception: # noqa: BLE001
|
|
344
|
+
return _safe_defaults(qualified_name)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# ── Go extraction ─────────────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _go_is_exported(name: str) -> bool:
|
|
351
|
+
"""Go export rule: capitalized first letter → exported."""
|
|
352
|
+
return bool(name) and name[0].isupper()
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _go_visibility(name: str) -> str:
|
|
356
|
+
"""Go visibility from capitalization."""
|
|
357
|
+
return "public" if _go_is_exported(name) else "private"
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _go_signature(node: Node) -> str | None:
|
|
361
|
+
"""Extract Go declaration header as one-line string."""
|
|
362
|
+
try:
|
|
363
|
+
ntype = node.type
|
|
364
|
+
|
|
365
|
+
if ntype == "function_declaration":
|
|
366
|
+
name_node = node.child_by_field_name("name")
|
|
367
|
+
params_node = node.child_by_field_name("parameters")
|
|
368
|
+
result_node = node.child_by_field_name("result")
|
|
369
|
+
name = _text(name_node)
|
|
370
|
+
params = _text(params_node)
|
|
371
|
+
sig = f"func {name}{params}"
|
|
372
|
+
if result_node:
|
|
373
|
+
sig += f" {_text(result_node)}"
|
|
374
|
+
return _normalize_to_one_line(sig)
|
|
375
|
+
|
|
376
|
+
if ntype == "method_declaration":
|
|
377
|
+
# func (recv RecvType) MethodName(params) result
|
|
378
|
+
recv_node = node.child_by_field_name("receiver")
|
|
379
|
+
name_node = node.child_by_field_name("name")
|
|
380
|
+
params_node = node.child_by_field_name("parameters")
|
|
381
|
+
result_node = node.child_by_field_name("result")
|
|
382
|
+
recv = _text(recv_node) if recv_node else ""
|
|
383
|
+
name = _text(name_node)
|
|
384
|
+
params = _text(params_node)
|
|
385
|
+
sig = f"func {recv} {name}{params}"
|
|
386
|
+
if result_node:
|
|
387
|
+
sig += f" {_text(result_node)}"
|
|
388
|
+
return _normalize_to_one_line(sig)
|
|
389
|
+
|
|
390
|
+
if ntype == "type_declaration":
|
|
391
|
+
# For type declarations we extract the first type_spec or type_alias
|
|
392
|
+
for child in node.named_children:
|
|
393
|
+
if child.type in ("type_spec", "type_alias"):
|
|
394
|
+
name_node = child.child_by_field_name("name")
|
|
395
|
+
type_node = child.child_by_field_name("type")
|
|
396
|
+
if name_node:
|
|
397
|
+
name = _text(name_node)
|
|
398
|
+
if type_node:
|
|
399
|
+
type_kind = type_node.type.replace("_type", "").replace("_", " ")
|
|
400
|
+
return _normalize_to_one_line(f"type {name} {type_kind}")
|
|
401
|
+
return _normalize_to_one_line(f"type {name}")
|
|
402
|
+
|
|
403
|
+
except Exception: # noqa: BLE001
|
|
404
|
+
pass
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _extract_go(node: Node, qualified_name: str | None, max_sig_len: int) -> NodeFields:
|
|
409
|
+
"""Extract all five fields for a Go node."""
|
|
410
|
+
try:
|
|
411
|
+
# Determine name for export check
|
|
412
|
+
name_node = node.child_by_field_name("name")
|
|
413
|
+
name = _text(name_node) if name_node else ""
|
|
414
|
+
|
|
415
|
+
# For type_declaration, name is inside the type_spec
|
|
416
|
+
if node.type == "type_declaration":
|
|
417
|
+
for child in node.named_children:
|
|
418
|
+
if child.type in ("type_spec", "type_alias"):
|
|
419
|
+
inner_name = child.child_by_field_name("name")
|
|
420
|
+
name = _text(inner_name) if inner_name else ""
|
|
421
|
+
break
|
|
422
|
+
|
|
423
|
+
sig = _go_signature(node)
|
|
424
|
+
if sig is not None:
|
|
425
|
+
sig = _truncate(sig, max_sig_len)
|
|
426
|
+
|
|
427
|
+
return NodeFields(
|
|
428
|
+
signature=sig,
|
|
429
|
+
# Go has no decorator syntax; unlike Python @decorator or TS @Decorator,
|
|
430
|
+
# struct tags and //go:generate directives are not captured here.
|
|
431
|
+
decorators=[],
|
|
432
|
+
is_exported=_go_is_exported(name) if name else None,
|
|
433
|
+
visibility=_go_visibility(name) if name else None,
|
|
434
|
+
qualified_name=qualified_name,
|
|
435
|
+
)
|
|
436
|
+
except Exception: # noqa: BLE001
|
|
437
|
+
return _safe_defaults(qualified_name)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# ── Rust extraction ───────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _rust_has_pub(node: Node) -> bool:
|
|
444
|
+
"""Check if a Rust node has a `pub` or `pub(...)` visibility modifier."""
|
|
445
|
+
try:
|
|
446
|
+
for child in node.children:
|
|
447
|
+
if child.type in ("visibility_modifier",):
|
|
448
|
+
return True
|
|
449
|
+
except Exception: # noqa: BLE001
|
|
450
|
+
pass
|
|
451
|
+
return False
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _rust_visibility(node: Node) -> str:
|
|
455
|
+
"""Rust visibility from pub modifier.
|
|
456
|
+
|
|
457
|
+
pub → 'public'
|
|
458
|
+
pub(crate) → 'crate'
|
|
459
|
+
(none) → 'private'
|
|
460
|
+
"""
|
|
461
|
+
try:
|
|
462
|
+
for child in node.children:
|
|
463
|
+
if child.type == "visibility_modifier":
|
|
464
|
+
text = _text(child).strip()
|
|
465
|
+
if "crate" in text:
|
|
466
|
+
return "crate"
|
|
467
|
+
return "public"
|
|
468
|
+
except Exception: # noqa: BLE001
|
|
469
|
+
pass
|
|
470
|
+
return "private"
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _rust_signature(node: Node) -> str | None:
|
|
474
|
+
"""Extract Rust declaration header as one-line string."""
|
|
475
|
+
try:
|
|
476
|
+
ntype = node.type
|
|
477
|
+
|
|
478
|
+
if ntype in ("function_item", "function_signature_item"):
|
|
479
|
+
# Build: [pub] fn name(params) -> return_type
|
|
480
|
+
parts: list[str] = []
|
|
481
|
+
for child in node.children:
|
|
482
|
+
# Stop at the body block (declaration_list or block)
|
|
483
|
+
if child.type in ("block", "declaration_list"):
|
|
484
|
+
break
|
|
485
|
+
text = _text(child).strip()
|
|
486
|
+
if text:
|
|
487
|
+
parts.append(text)
|
|
488
|
+
return _normalize_to_one_line(" ".join(parts))
|
|
489
|
+
|
|
490
|
+
if ntype == "struct_item":
|
|
491
|
+
parts = []
|
|
492
|
+
for child in node.children:
|
|
493
|
+
if child.type in ("field_declaration_list", "ordered_field_declaration_list"):
|
|
494
|
+
break
|
|
495
|
+
text = _text(child).strip()
|
|
496
|
+
if text:
|
|
497
|
+
parts.append(text)
|
|
498
|
+
return _normalize_to_one_line(" ".join(parts))
|
|
499
|
+
|
|
500
|
+
if ntype == "enum_item":
|
|
501
|
+
parts = []
|
|
502
|
+
for child in node.children:
|
|
503
|
+
if child.type == "enum_variant_list":
|
|
504
|
+
break
|
|
505
|
+
text = _text(child).strip()
|
|
506
|
+
if text:
|
|
507
|
+
parts.append(text)
|
|
508
|
+
return _normalize_to_one_line(" ".join(parts))
|
|
509
|
+
|
|
510
|
+
if ntype == "trait_item":
|
|
511
|
+
parts = []
|
|
512
|
+
for child in node.children:
|
|
513
|
+
if child.type == "declaration_list":
|
|
514
|
+
break
|
|
515
|
+
text = _text(child).strip()
|
|
516
|
+
if text:
|
|
517
|
+
parts.append(text)
|
|
518
|
+
return _normalize_to_one_line(" ".join(parts))
|
|
519
|
+
|
|
520
|
+
except Exception: # noqa: BLE001
|
|
521
|
+
pass
|
|
522
|
+
return None
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _extract_rust(node: Node, qualified_name: str | None, max_sig_len: int) -> NodeFields:
|
|
526
|
+
"""Extract all five fields for a Rust node."""
|
|
527
|
+
try:
|
|
528
|
+
vis = _rust_visibility(node)
|
|
529
|
+
is_exported = vis in ("public", "crate")
|
|
530
|
+
|
|
531
|
+
sig = _rust_signature(node)
|
|
532
|
+
if sig is not None:
|
|
533
|
+
sig = _truncate(sig, max_sig_len)
|
|
534
|
+
|
|
535
|
+
return NodeFields(
|
|
536
|
+
signature=sig,
|
|
537
|
+
# Rust #[...] attributes are not decorators in the Python/TS sense and are
|
|
538
|
+
# intentionally excluded per PRD: they appear before the item, would require
|
|
539
|
+
# separate attribute_item walking, and rarely appear in symbol-level queries.
|
|
540
|
+
decorators=[],
|
|
541
|
+
is_exported=is_exported,
|
|
542
|
+
visibility=vis,
|
|
543
|
+
qualified_name=qualified_name,
|
|
544
|
+
)
|
|
545
|
+
except Exception: # noqa: BLE001
|
|
546
|
+
return _safe_defaults(qualified_name)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# ── Safe defaults ─────────────────────────────────────────────────────────────
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _safe_defaults(qualified_name: str | None = None) -> NodeFields:
|
|
553
|
+
"""Return a safe NodeFields with all nulls/empty. Used on any extraction failure."""
|
|
554
|
+
return NodeFields(
|
|
555
|
+
signature=None,
|
|
556
|
+
decorators=[],
|
|
557
|
+
is_exported=None,
|
|
558
|
+
visibility=None,
|
|
559
|
+
qualified_name=qualified_name,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
# ── Public entry point ────────────────────────────────────────────────────────
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def extract_node_fields(
|
|
567
|
+
node: Any,
|
|
568
|
+
language: str,
|
|
569
|
+
qualified_name: str | None = None,
|
|
570
|
+
max_signature_len: int = _DEFAULT_MAX_SIG_LEN,
|
|
571
|
+
) -> NodeFields:
|
|
572
|
+
"""Extract the five enrichment fields from a tree-sitter symbol node.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
node: A tree-sitter Node (or None/invalid — never raises).
|
|
576
|
+
language: 'python' | 'typescript' | 'javascript' | 'go' | 'rust'.
|
|
577
|
+
qualified_name: Already-resolved qualified name from the caller's scope-walker.
|
|
578
|
+
Passed through as-is into NodeFields.qualified_name.
|
|
579
|
+
max_signature_len: Maximum signature length in characters. Signatures longer
|
|
580
|
+
than this are truncated with '...'. Callers (graph.py /
|
|
581
|
+
graph_go_rust.py) read this from seam.config.SEAM_MAX_SIGNATURE_LEN
|
|
582
|
+
so there is a single config source of truth. The default here
|
|
583
|
+
(300) matches the seam.config default.
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
NodeFields TypedDict. ALL fields are safe: signature may be None,
|
|
587
|
+
decorators is always a list (possibly empty), is_exported/visibility may be None.
|
|
588
|
+
|
|
589
|
+
NEVER raises: any extraction error returns _safe_defaults().
|
|
590
|
+
"""
|
|
591
|
+
# Guard: None or non-Node input → safe defaults
|
|
592
|
+
if node is None:
|
|
593
|
+
return _safe_defaults(qualified_name)
|
|
594
|
+
|
|
595
|
+
if not isinstance(node, Node):
|
|
596
|
+
return _safe_defaults(qualified_name)
|
|
597
|
+
|
|
598
|
+
try:
|
|
599
|
+
if language == "python":
|
|
600
|
+
return _extract_python(node, qualified_name, max_signature_len)
|
|
601
|
+
elif language in ("typescript", "javascript"):
|
|
602
|
+
return _extract_typescript(node, qualified_name, max_signature_len)
|
|
603
|
+
elif language == "go":
|
|
604
|
+
return _extract_go(node, qualified_name, max_signature_len)
|
|
605
|
+
elif language == "rust":
|
|
606
|
+
return _extract_rust(node, qualified_name, max_signature_len)
|
|
607
|
+
# Phase 9 — new languages routed to signatures_ext (stubs return safe defaults)
|
|
608
|
+
elif language == "java":
|
|
609
|
+
return _extract_java(node, qualified_name, max_signature_len)
|
|
610
|
+
elif language == "csharp":
|
|
611
|
+
return _extract_csharp(node, qualified_name, max_signature_len)
|
|
612
|
+
elif language == "ruby":
|
|
613
|
+
return _extract_ruby(node, qualified_name, max_signature_len)
|
|
614
|
+
elif language == "c":
|
|
615
|
+
return _extract_c(node, qualified_name, max_signature_len)
|
|
616
|
+
elif language == "cpp":
|
|
617
|
+
return _extract_cpp(node, qualified_name, max_signature_len)
|
|
618
|
+
elif language == "php":
|
|
619
|
+
return _extract_php(node, qualified_name, max_signature_len)
|
|
620
|
+
# Phase 10 — Swift
|
|
621
|
+
elif language == "swift":
|
|
622
|
+
return _extract_swift(node, qualified_name, max_signature_len)
|
|
623
|
+
else:
|
|
624
|
+
# Unsupported language — safe defaults, not an error; callers may index
|
|
625
|
+
# languages not yet wired to an extractor.
|
|
626
|
+
return _safe_defaults(qualified_name)
|
|
627
|
+
except Exception: # noqa: BLE001
|
|
628
|
+
# Belt-and-suspenders: any unhandled exception → safe defaults
|
|
629
|
+
logger.debug(
|
|
630
|
+
"extract_node_fields: unhandled exception for language=%r node.type=%r",
|
|
631
|
+
language,
|
|
632
|
+
getattr(node, "type", "?"),
|
|
633
|
+
)
|
|
634
|
+
return _safe_defaults(qualified_name)
|