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,644 @@
|
|
|
1
|
+
"""Shared primitive types, constants, and helpers for the Seam indexer graph layer.
|
|
2
|
+
|
|
3
|
+
LAYER: leaf — imports only stdlib and tree_sitter. This module must never import
|
|
4
|
+
from any other seam.indexer module.
|
|
5
|
+
|
|
6
|
+
LAYERING (import direction):
|
|
7
|
+
graph_common (this file — leaf, no seam deps)
|
|
8
|
+
↑ ↑
|
|
9
|
+
graph.py graph_go_rust.py
|
|
10
|
+
|
|
11
|
+
Both graph.py and graph_go_rust.py import from here at their top levels.
|
|
12
|
+
Neither imports from the other, so no circular dependency exists.
|
|
13
|
+
|
|
14
|
+
WHY this split was necessary: before this module existed, graph_go_rust.py imported
|
|
15
|
+
helpers (Symbol, Edge, _text, etc.) from graph.py at its top level, and graph.py in
|
|
16
|
+
turn tried to import the Go/Rust extractors from graph_go_rust.py — a circular init.
|
|
17
|
+
The only escape at the time was deferred in-function imports, which violated the
|
|
18
|
+
project rule that all imports must be at the top of every file. Moving the shared
|
|
19
|
+
primitives here (a leaf with no seam deps) cuts the cycle completely, restores
|
|
20
|
+
top-level-only imports in all three files, and is the authoritative home for the
|
|
21
|
+
public TypedDicts so callers can do:
|
|
22
|
+
from seam.indexer.graph import Symbol, Edge # still works — graph re-exports these
|
|
23
|
+
|
|
24
|
+
Contents:
|
|
25
|
+
- TypedDicts: Confidence, Symbol, Edge, Comment
|
|
26
|
+
- Constants: SEMANTIC_MARKERS, _MARKER_RE
|
|
27
|
+
- Helpers: _text, _node_name, _make_symbol, _match_marker,
|
|
28
|
+
_block_comment_lines, _arrow_function_name, _find_enclosing_function
|
|
29
|
+
- Go/Rust receiver helpers: _go_recv_type_name, _rust_impl_type_name
|
|
30
|
+
(kept here so _find_enclosing_function can call them without importing
|
|
31
|
+
from graph_go_rust — this is what keeps the leaf property intact)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import logging
|
|
35
|
+
import re
|
|
36
|
+
from typing import Literal, NotRequired, TypedDict
|
|
37
|
+
|
|
38
|
+
from tree_sitter import Node
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
# ── Semantic comment markers (WHY-extraction feature, Phase 1b) ───────────────
|
|
43
|
+
|
|
44
|
+
# Fixed set of marker keywords matched case-insensitively at the START of the
|
|
45
|
+
# comment body (after stripping the delimiter and leading whitespace).
|
|
46
|
+
SEMANTIC_MARKERS: frozenset[str] = frozenset({"WHY", "HACK", "NOTE", "TODO", "FIXME"})
|
|
47
|
+
|
|
48
|
+
# Pre-compiled regex: group 1=marker keyword, group 3=remainder text.
|
|
49
|
+
# Marker must be followed by ':', whitespace, or end-of-string — blocks prefix
|
|
50
|
+
# matches like 'whyever'.
|
|
51
|
+
_MARKER_RE = re.compile(
|
|
52
|
+
r"^(WHY|HACK|NOTE|TODO|FIXME)(?::|((?=\s)|$))(.*)",
|
|
53
|
+
re.IGNORECASE | re.DOTALL,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Confidence level for an edge — persisted in the DB, exposed via MCP.
|
|
57
|
+
Confidence = Literal["EXTRACTED", "INFERRED", "AMBIGUOUS"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Symbol(TypedDict):
|
|
61
|
+
name: str
|
|
62
|
+
kind: str # 'function' | 'class' | 'method' | 'interface' | 'type'
|
|
63
|
+
file: str # str(path) — resolved at call time
|
|
64
|
+
start_line: int
|
|
65
|
+
end_line: int
|
|
66
|
+
docstring: str | None
|
|
67
|
+
# Phase 4 — Node-field enrichment. All nullable: None means "not yet extracted"
|
|
68
|
+
# (e.g. pre-v5 rows) rather than "has no value". Callers must not treat None as
|
|
69
|
+
# equivalent to empty string or False.
|
|
70
|
+
signature: str | None # declaration header, single line, truncated at SEAM_MAX_SIGNATURE_LEN
|
|
71
|
+
decorators: list[str] # verbatim decorator text (Python/TS only); [] for Go/Rust
|
|
72
|
+
is_exported: bool | None # export status; None when language has no uniform export concept
|
|
73
|
+
visibility: str | None # "public"|"private"|"protected"|"crate"; Python uses heuristic
|
|
74
|
+
qualified_name: str | None # "ClassName.method" for methods; None for anonymous/top-level unknown
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Edge(TypedDict):
|
|
78
|
+
source: str # Symbol name of caller / importer
|
|
79
|
+
target: str # Symbol name of callee / importee
|
|
80
|
+
# Edge kind vocabulary:
|
|
81
|
+
# 'import' — module/symbol import statement
|
|
82
|
+
# 'call' — function/method call edge
|
|
83
|
+
# 'extends' — class inheritance (base class)
|
|
84
|
+
# 'implements' — interface implementation
|
|
85
|
+
# 'instantiates' — object construction (new Foo(), Foo(), Foo::new(), Foo{}) [Tier B B6]
|
|
86
|
+
# 'holds' — composition: a class stores a plain user type as a typed field/property
|
|
87
|
+
# OR receives one as a typed constructor/init parameter [Slice #77]
|
|
88
|
+
# 'uses' — a function/method references a plain user type as a PARAMETER in its
|
|
89
|
+
# signature (e.g. f(x: T) → f uses T). Complements 'holds' (stored
|
|
90
|
+
# composition) with signature-level coupling. Gated by SEAM_PARAM_EDGES.
|
|
91
|
+
kind: str
|
|
92
|
+
file: str
|
|
93
|
+
line: int
|
|
94
|
+
confidence: Confidence # EXTRACTED | INFERRED | AMBIGUOUS
|
|
95
|
+
# Tier B B1 (v10): raw receiver expression text for attribute calls (e.g. 'self', 'obj').
|
|
96
|
+
# None for import edges, bare-identifier call edges, and pre-v10 rows.
|
|
97
|
+
#
|
|
98
|
+
# WHY NotRequired (not a plain required field): the Edge TypedDict is constructed in
|
|
99
|
+
# many call sites across 12 language extractors. Making receiver Required would force
|
|
100
|
+
# all existing Edge() instantiations to be updated simultaneously. NotRequired lets
|
|
101
|
+
# us add the field incrementally — old callers remain valid, new callers opt in.
|
|
102
|
+
# upsert_file uses edge.get("receiver") which defaults to None, so absent ≡ None
|
|
103
|
+
# at the DB layer. This is the same null-contract as Phase 4/5 enrichment fields.
|
|
104
|
+
#
|
|
105
|
+
# WHY stored even when target is already qualified (e.g. 'Client.method'):
|
|
106
|
+
# Preserves the raw receiver text for debugging, future re-inference passes, and
|
|
107
|
+
# any tooling that needs to re-derive qualification without a full re-index.
|
|
108
|
+
# Edges remain string-name-keyed (source/target are names, not node IDs) as required
|
|
109
|
+
# for independent re-indexing.
|
|
110
|
+
receiver: NotRequired[str | None]
|
|
111
|
+
# v12: synthesis channel that produced this edge. None for parser-extracted edges;
|
|
112
|
+
# a channel name string (e.g. 'interface-override') for edges emitted by the
|
|
113
|
+
# post-pass synthesis engine (seam/analysis/synthesis.py).
|
|
114
|
+
#
|
|
115
|
+
# WHY NotRequired: same rationale as receiver — all 12 extractors construct Edge()
|
|
116
|
+
# dicts without this field, and synthesis adds it only on its own output.
|
|
117
|
+
# upsert_file uses edge.get("synthesized_by") defaulting to None, so absent ≡ NULL.
|
|
118
|
+
# Provenance is DERIVED: synthesized_by IS NOT NULL ⟹ heuristic edge. This avoids
|
|
119
|
+
# a separate boolean column and keeps the schema additive.
|
|
120
|
+
synthesized_by: NotRequired[str | None]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class Comment(TypedDict):
|
|
124
|
+
"""A semantic comment extracted from source code during indexing.
|
|
125
|
+
|
|
126
|
+
Only WHY/HACK/NOTE/TODO/FIXME-tagged comments are stored — plain
|
|
127
|
+
comments are ignored. Marker is normalized to UPPERCASE.
|
|
128
|
+
"""
|
|
129
|
+
marker: str # Normalized: WHY | HACK | NOTE | TODO | FIXME
|
|
130
|
+
text: str # Body after the marker (and optional colon), stripped
|
|
131
|
+
line: int # 1-based line number in the source file
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ── Low-level AST helpers ──────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _text(node: Node) -> str:
|
|
138
|
+
"""Safely decode a tree-sitter node's text bytes to str.
|
|
139
|
+
|
|
140
|
+
node.text is typed as bytes | None in the stubs; guard against None.
|
|
141
|
+
"""
|
|
142
|
+
raw = node.text
|
|
143
|
+
if raw is None:
|
|
144
|
+
return ""
|
|
145
|
+
return raw.decode("utf-8", errors="replace")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _node_name(node: Node) -> str | None:
|
|
149
|
+
"""Return the text of the 'name' field child, or None if absent."""
|
|
150
|
+
name_node = node.child_by_field_name("name")
|
|
151
|
+
if name_node is None:
|
|
152
|
+
return None
|
|
153
|
+
return _text(name_node)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _make_symbol(
|
|
157
|
+
name: str,
|
|
158
|
+
kind: str,
|
|
159
|
+
file: str,
|
|
160
|
+
node: Node,
|
|
161
|
+
docstring: str | None,
|
|
162
|
+
signature: str | None = None,
|
|
163
|
+
decorators: list[str] | None = None,
|
|
164
|
+
is_exported: bool | None = None,
|
|
165
|
+
visibility: str | None = None,
|
|
166
|
+
qualified_name: str | None = None,
|
|
167
|
+
) -> Symbol:
|
|
168
|
+
"""Construct a Symbol TypedDict from a tree-sitter node.
|
|
169
|
+
|
|
170
|
+
Phase 4 enrichment fields default to None/[] so callers that don't yet call
|
|
171
|
+
extract_node_fields continue to work without changes — backward-compatible extension
|
|
172
|
+
rather than requiring every call site to be updated simultaneously.
|
|
173
|
+
"""
|
|
174
|
+
return Symbol(
|
|
175
|
+
name=name,
|
|
176
|
+
kind=kind,
|
|
177
|
+
file=file,
|
|
178
|
+
start_line=node.start_point[0] + 1, # tree-sitter rows are 0-based
|
|
179
|
+
end_line=node.end_point[0] + 1,
|
|
180
|
+
docstring=docstring,
|
|
181
|
+
signature=signature,
|
|
182
|
+
decorators=decorators if decorators is not None else [],
|
|
183
|
+
is_exported=is_exported,
|
|
184
|
+
visibility=visibility,
|
|
185
|
+
qualified_name=qualified_name,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _match_marker(body: str) -> tuple[str, str] | None:
|
|
190
|
+
"""Try to match a semantic marker at the start of a stripped comment body.
|
|
191
|
+
|
|
192
|
+
Returns (marker_upper, text) if matched, or None.
|
|
193
|
+
The regex requires the marker to be followed by ':', whitespace, or end-of-string.
|
|
194
|
+
"""
|
|
195
|
+
m = _MARKER_RE.match(body)
|
|
196
|
+
if not m:
|
|
197
|
+
return None
|
|
198
|
+
marker = m.group(1).upper()
|
|
199
|
+
text = (m.group(3) or "").strip()
|
|
200
|
+
return marker, text
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _block_comment_lines(raw: str) -> list[tuple[int, str]]:
|
|
204
|
+
"""Return (line_offset, cleaned_body) for each non-empty line of a /* */ block.
|
|
205
|
+
|
|
206
|
+
line_offset is the 0-based line index from the block's first line.
|
|
207
|
+
Each line has /*, */ and leading '*' decorations stripped. Empty lines omitted.
|
|
208
|
+
"""
|
|
209
|
+
out: list[tuple[int, str]] = []
|
|
210
|
+
for i, line in enumerate(raw.splitlines()):
|
|
211
|
+
s = line.strip()
|
|
212
|
+
if s.startswith("/*"):
|
|
213
|
+
s = s[2:]
|
|
214
|
+
if s.endswith("*/"):
|
|
215
|
+
s = s[:-2]
|
|
216
|
+
s = s.strip().lstrip("*").strip()
|
|
217
|
+
if s:
|
|
218
|
+
out.append((i, s))
|
|
219
|
+
return out
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ── Go/Rust receiver helpers (leaf — no circular dep) ─────────────────────────
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _go_recv_type_name(method_node: Node) -> str | None:
|
|
226
|
+
"""Extract the receiver type name from a Go method_declaration.
|
|
227
|
+
|
|
228
|
+
Go method receivers come in four forms; all are normalized to a plain type name:
|
|
229
|
+
- value receiver: func (r Foo) M() → 'Foo' (type_identifier)
|
|
230
|
+
- pointer receiver: func (r *Foo) M() → 'Foo' (pointer_type → type_identifier)
|
|
231
|
+
- generic value receiver: func (r Foo[T]) M() → 'Foo' (generic_type → type_identifier)
|
|
232
|
+
- generic pointer recv: func (r *Repo[T]) M() → 'Repo' (pointer_type → generic_type → type_identifier)
|
|
233
|
+
|
|
234
|
+
Normalization rule: always extract the base type_identifier, discarding `*` and `[T]`.
|
|
235
|
+
Returns None if the receiver list is absent or the type node has an unexpected shape.
|
|
236
|
+
"""
|
|
237
|
+
recv = method_node.child_by_field_name("receiver")
|
|
238
|
+
if recv is None:
|
|
239
|
+
return None
|
|
240
|
+
for pd in recv.named_children:
|
|
241
|
+
if pd.type == "parameter_declaration":
|
|
242
|
+
typ = pd.child_by_field_name("type")
|
|
243
|
+
if typ is None:
|
|
244
|
+
continue
|
|
245
|
+
if typ.type == "pointer_type":
|
|
246
|
+
# *T or *T[K]: inspect the single named child of pointer_type.
|
|
247
|
+
inner = next(iter(typ.named_children), None)
|
|
248
|
+
if inner is None:
|
|
249
|
+
continue
|
|
250
|
+
if inner.type == "type_identifier":
|
|
251
|
+
# Simple pointer receiver: *Foo → 'Foo'
|
|
252
|
+
return _text(inner)
|
|
253
|
+
if inner.type == "generic_type":
|
|
254
|
+
# Generic pointer receiver: *Repo[T] → extract 'Repo' from generic_type
|
|
255
|
+
base = next(
|
|
256
|
+
(c for c in inner.named_children if c.type == "type_identifier"),
|
|
257
|
+
None,
|
|
258
|
+
)
|
|
259
|
+
if base is not None:
|
|
260
|
+
return _text(base)
|
|
261
|
+
elif typ.type == "type_identifier":
|
|
262
|
+
# Plain value receiver: Foo → 'Foo'
|
|
263
|
+
return _text(typ)
|
|
264
|
+
elif typ.type == "generic_type":
|
|
265
|
+
# Generic value receiver: Repo[T] → extract 'Repo' from generic_type
|
|
266
|
+
base = next(
|
|
267
|
+
(c for c in typ.named_children if c.type == "type_identifier"),
|
|
268
|
+
None,
|
|
269
|
+
)
|
|
270
|
+
if base is not None:
|
|
271
|
+
return _text(base)
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _rust_impl_type_name(impl_node: Node) -> str | None:
|
|
276
|
+
"""Extract the base type name from a Rust impl_item node.
|
|
277
|
+
|
|
278
|
+
Handles:
|
|
279
|
+
- Plain impl: impl Foo { ... } → 'Foo' (type field is type_identifier)
|
|
280
|
+
- Generic impl: impl<T> Foo<T> { ... } → 'Foo' (type field is generic_type;
|
|
281
|
+
extract the first type_identifier child)
|
|
282
|
+
|
|
283
|
+
Returns the base type_identifier string (e.g. 'Foo', not 'Foo<T>'), or None if
|
|
284
|
+
the type field is absent or has an unexpected shape.
|
|
285
|
+
"""
|
|
286
|
+
type_node = impl_node.child_by_field_name("type")
|
|
287
|
+
if type_node is None:
|
|
288
|
+
return None
|
|
289
|
+
if type_node.type == "type_identifier":
|
|
290
|
+
return _text(type_node)
|
|
291
|
+
if type_node.type == "generic_type":
|
|
292
|
+
base = next(
|
|
293
|
+
(c for c in type_node.named_children if c.type == "type_identifier"),
|
|
294
|
+
None,
|
|
295
|
+
)
|
|
296
|
+
if base is not None:
|
|
297
|
+
return _text(base)
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ── Inheritance base-name normalizer (P6a) ───────────────────────────────────
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _base_type_name(node: Node) -> str | None:
|
|
305
|
+
"""Normalize a base-class / interface type node to its bare type NAME.
|
|
306
|
+
|
|
307
|
+
The edge graph is name-keyed and homonym-collapsed, so generic arguments and
|
|
308
|
+
namespace/package qualifiers are stripped to the rightmost simple type name —
|
|
309
|
+
matching how call/import targets are stored. Handles the shapes that appear in
|
|
310
|
+
base-class / interface clauses across Python, TypeScript, Java, and C#:
|
|
311
|
+
|
|
312
|
+
- identifier / type_identifier → the text directly (Base)
|
|
313
|
+
- generic_type (Java/TS: Base<T>) → first type_identifier child (Base)
|
|
314
|
+
- generic_name (C#: IFace<T>) → first identifier child (IFace)
|
|
315
|
+
- qualified_name (C#: Ns.Base) → 'name' field, else last identifier child
|
|
316
|
+
- scoped_type_identifier (Java: a.B) → 'name' field, else last type_identifier
|
|
317
|
+
- member_expression (TS: ns.Base) → 'property' field, else last child
|
|
318
|
+
|
|
319
|
+
Returns the bare name string, or None when the node shape is unrecognized
|
|
320
|
+
(the caller then skips emitting an edge — never raises).
|
|
321
|
+
"""
|
|
322
|
+
t = node.type
|
|
323
|
+
if t in ("identifier", "type_identifier"):
|
|
324
|
+
return _text(node)
|
|
325
|
+
if t in ("generic_type", "generic_name"):
|
|
326
|
+
for c in node.named_children:
|
|
327
|
+
if c.type in ("type_identifier", "identifier"):
|
|
328
|
+
return _text(c)
|
|
329
|
+
return None
|
|
330
|
+
if t in ("qualified_name", "scoped_type_identifier"):
|
|
331
|
+
name_node = node.child_by_field_name("name")
|
|
332
|
+
if name_node is not None:
|
|
333
|
+
return _text(name_node)
|
|
334
|
+
# Fallback: rightmost identifier-like child.
|
|
335
|
+
for c in reversed(node.named_children):
|
|
336
|
+
if c.type in ("identifier", "type_identifier"):
|
|
337
|
+
return _text(c)
|
|
338
|
+
return None
|
|
339
|
+
if t == "member_expression":
|
|
340
|
+
prop = node.child_by_field_name("property")
|
|
341
|
+
if prop is not None:
|
|
342
|
+
return _text(prop)
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ── Arrow-function name resolver (TypeScript/JS only) ─────────────────────────
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _arrow_function_name(arrow_node: Node) -> str | None:
|
|
350
|
+
"""Resolve the name of an arrow function from its assignment context.
|
|
351
|
+
|
|
352
|
+
Returns the variable name if `const X = () => {...}`, else None.
|
|
353
|
+
"""
|
|
354
|
+
parent = arrow_node.parent
|
|
355
|
+
if parent is None:
|
|
356
|
+
return None
|
|
357
|
+
if parent.type == "variable_declarator":
|
|
358
|
+
name_node = parent.child_by_field_name("name")
|
|
359
|
+
if name_node is not None and name_node.type == "identifier":
|
|
360
|
+
return _text(name_node)
|
|
361
|
+
return None
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# ── Enclosing-scope resolver ───────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _c_cpp_function_name_from_def(func_def: Node) -> str | None:
|
|
368
|
+
"""Extract the function name from a C or C++ function_definition node.
|
|
369
|
+
|
|
370
|
+
C/C++ function_definition has NO 'name' field directly — the name is nested:
|
|
371
|
+
function_definition → declarator (function_declarator) → declarator (identifier
|
|
372
|
+
| field_identifier | qualified_identifier).
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
- Plain name (str) for free functions and in-class method declarations.
|
|
376
|
+
- 'Class.method' qualified name for out-of-line definitions (Class::method).
|
|
377
|
+
- None if the name cannot be resolved.
|
|
378
|
+
|
|
379
|
+
WHY this helper is in graph_common (leaf): _find_enclosing_function needs it to
|
|
380
|
+
resolve enclosing scope names for C/C++ call edges. Putting it in graph_c_cpp
|
|
381
|
+
would create a cycle (graph_c_cpp → graph_common → graph_c_cpp). Keeping it here
|
|
382
|
+
(alongside the other language helpers like _go_recv_type_name) preserves the leaf.
|
|
383
|
+
"""
|
|
384
|
+
try:
|
|
385
|
+
declarator = func_def.child_by_field_name("declarator")
|
|
386
|
+
if declarator is None or declarator.type != "function_declarator":
|
|
387
|
+
return None
|
|
388
|
+
inner = declarator.child_by_field_name("declarator")
|
|
389
|
+
if inner is None:
|
|
390
|
+
return None
|
|
391
|
+
if inner.type in ("identifier", "field_identifier"):
|
|
392
|
+
return _text(inner)
|
|
393
|
+
if inner.type == "qualified_identifier":
|
|
394
|
+
# Out-of-line C++ method: Class::method → 'Class.method'
|
|
395
|
+
scope = inner.child_by_field_name("scope")
|
|
396
|
+
name_node = inner.child_by_field_name("name")
|
|
397
|
+
if scope and name_node:
|
|
398
|
+
return f"{_text(scope)}.{_text(name_node)}"
|
|
399
|
+
except Exception: # noqa: BLE001
|
|
400
|
+
pass
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _find_enclosing_function(node: Node, language: str) -> str | None:
|
|
405
|
+
"""Walk up the parent chain to find the nearest enclosing function/method name.
|
|
406
|
+
|
|
407
|
+
Returns 'ClassName.methodName' for methods, plain name for functions, or None
|
|
408
|
+
when no enclosing function exists (e.g. top-level module code). None causes the
|
|
409
|
+
caller to drop the edge — only named scopes produce edge sources.
|
|
410
|
+
|
|
411
|
+
Supports all 12 Seam languages: python, typescript, javascript, go, rust,
|
|
412
|
+
java, csharp, ruby, c, cpp, php, swift. Per-language function/method node types
|
|
413
|
+
and class-context types are defined in the body below.
|
|
414
|
+
|
|
415
|
+
WHY in this leaf module: both graph.py and the family modules (graph_go_rust,
|
|
416
|
+
graph_java_csharp, graph_c_cpp, graph_ruby_php) call this for call-edge source
|
|
417
|
+
resolution. Keeping it here avoids circular imports between family modules.
|
|
418
|
+
"""
|
|
419
|
+
func_types_py = {"function_definition"}
|
|
420
|
+
func_types_ts = {"function_declaration", "method_definition", "arrow_function"}
|
|
421
|
+
func_types_go = {"function_declaration", "method_declaration"}
|
|
422
|
+
func_types_rust = {"function_item"}
|
|
423
|
+
# Phase 9 function/method node types per language
|
|
424
|
+
func_types_java = {"method_declaration", "constructor_declaration"}
|
|
425
|
+
func_types_csharp = {
|
|
426
|
+
"method_declaration",
|
|
427
|
+
"constructor_declaration",
|
|
428
|
+
"local_function_statement",
|
|
429
|
+
}
|
|
430
|
+
func_types_ruby = {"method", "singleton_method"}
|
|
431
|
+
func_types_c = {"function_definition"}
|
|
432
|
+
func_types_cpp = {"function_definition"}
|
|
433
|
+
func_types_php = {"method_declaration", "function_definition"}
|
|
434
|
+
# Phase 10 Swift: function_declaration is the sole function/method node type.
|
|
435
|
+
func_types_swift = {"function_declaration", "protocol_function_declaration"}
|
|
436
|
+
|
|
437
|
+
# Phase 9 class-context node types for qualified 'Class.method' names
|
|
438
|
+
class_types_java = {
|
|
439
|
+
"class_declaration",
|
|
440
|
+
"interface_declaration",
|
|
441
|
+
"enum_declaration",
|
|
442
|
+
"record_declaration",
|
|
443
|
+
}
|
|
444
|
+
class_types_csharp = {
|
|
445
|
+
"class_declaration",
|
|
446
|
+
"struct_declaration",
|
|
447
|
+
"record_declaration",
|
|
448
|
+
"interface_declaration",
|
|
449
|
+
}
|
|
450
|
+
class_types_ruby = {"class", "module"}
|
|
451
|
+
class_types_cpp = {"class_specifier", "struct_specifier"}
|
|
452
|
+
class_types_php = {
|
|
453
|
+
"class_declaration",
|
|
454
|
+
"interface_declaration",
|
|
455
|
+
"trait_declaration",
|
|
456
|
+
}
|
|
457
|
+
# Phase 10 Swift: class_declaration covers class/struct/actor/extension;
|
|
458
|
+
# protocol_declaration covers protocol methods.
|
|
459
|
+
class_types_swift = {"class_declaration", "protocol_declaration"}
|
|
460
|
+
|
|
461
|
+
if language == "go":
|
|
462
|
+
func_types = func_types_go
|
|
463
|
+
elif language == "rust":
|
|
464
|
+
func_types = func_types_rust
|
|
465
|
+
elif language == "java":
|
|
466
|
+
func_types = func_types_java
|
|
467
|
+
elif language == "csharp":
|
|
468
|
+
func_types = func_types_csharp
|
|
469
|
+
elif language == "ruby":
|
|
470
|
+
func_types = func_types_ruby
|
|
471
|
+
elif language == "c":
|
|
472
|
+
func_types = func_types_c
|
|
473
|
+
elif language == "cpp":
|
|
474
|
+
func_types = func_types_cpp
|
|
475
|
+
elif language == "php":
|
|
476
|
+
func_types = func_types_php
|
|
477
|
+
elif language == "swift":
|
|
478
|
+
func_types = func_types_swift
|
|
479
|
+
else:
|
|
480
|
+
func_types = func_types_py if language == "python" else func_types_ts
|
|
481
|
+
|
|
482
|
+
# Fallback: name of the innermost const-assigned arrow found while walking up.
|
|
483
|
+
fallback_arrow_name: str | None = None
|
|
484
|
+
|
|
485
|
+
current = node.parent
|
|
486
|
+
while current is not None:
|
|
487
|
+
if current.type in func_types:
|
|
488
|
+
if current.type == "arrow_function":
|
|
489
|
+
if fallback_arrow_name is None:
|
|
490
|
+
fallback_arrow_name = _arrow_function_name(current)
|
|
491
|
+
current = current.parent
|
|
492
|
+
continue
|
|
493
|
+
|
|
494
|
+
# Go method_declaration: build qualified 'Recv.Name' using shared helper.
|
|
495
|
+
if language == "go" and current.type == "method_declaration":
|
|
496
|
+
# FIX 3: if the method has no name, skip — do NOT fall back to full node text.
|
|
497
|
+
func_name_node = current.child_by_field_name("name")
|
|
498
|
+
if func_name_node is None:
|
|
499
|
+
current = current.parent
|
|
500
|
+
continue
|
|
501
|
+
func_name = _text(func_name_node)
|
|
502
|
+
recv_name = _go_recv_type_name(current)
|
|
503
|
+
if recv_name:
|
|
504
|
+
return f"{recv_name}.{func_name}"
|
|
505
|
+
return func_name
|
|
506
|
+
|
|
507
|
+
# Rust function_item inside impl_item: build 'Type.fn' qualified name.
|
|
508
|
+
if language == "rust" and current.type == "function_item":
|
|
509
|
+
func_name_node = current.child_by_field_name("name")
|
|
510
|
+
if func_name_node is None:
|
|
511
|
+
current = current.parent
|
|
512
|
+
continue
|
|
513
|
+
func_name = _text(func_name_node)
|
|
514
|
+
parent = current.parent
|
|
515
|
+
if parent is not None and parent.type == "declaration_list":
|
|
516
|
+
grandparent = parent.parent
|
|
517
|
+
if grandparent is not None and grandparent.type == "impl_item":
|
|
518
|
+
type_name = _rust_impl_type_name(grandparent)
|
|
519
|
+
if type_name is not None:
|
|
520
|
+
return f"{type_name}.{func_name}"
|
|
521
|
+
return func_name
|
|
522
|
+
|
|
523
|
+
# C/C++ function_definition: name lives in declarator chain, not a 'name' field.
|
|
524
|
+
# function_definition → function_declarator ('declarator' field) →
|
|
525
|
+
# identifier or field_identifier or qualified_identifier ('declarator' field).
|
|
526
|
+
if language in ("c", "cpp") and current.type == "function_definition":
|
|
527
|
+
c_func_name = _c_cpp_function_name_from_def(current)
|
|
528
|
+
if c_func_name is None:
|
|
529
|
+
current = current.parent
|
|
530
|
+
continue
|
|
531
|
+
# C++ out-of-line method (e.g. 'Circle.area') is already qualified
|
|
532
|
+
if "." in c_func_name:
|
|
533
|
+
return c_func_name
|
|
534
|
+
# C++ in-class: check for enclosing class_specifier/struct_specifier
|
|
535
|
+
if language == "cpp":
|
|
536
|
+
parent = current.parent
|
|
537
|
+
while parent is not None:
|
|
538
|
+
if parent.type in class_types_cpp:
|
|
539
|
+
cls_name_node = parent.child_by_field_name("name")
|
|
540
|
+
if cls_name_node is not None:
|
|
541
|
+
return f"{_text(cls_name_node)}.{c_func_name}"
|
|
542
|
+
parent = parent.parent
|
|
543
|
+
return c_func_name
|
|
544
|
+
|
|
545
|
+
# Swift function_declaration has no 'name' field — name is a simple_identifier child.
|
|
546
|
+
# Also handle protocol_function_declaration the same way.
|
|
547
|
+
if language == "swift" and current.type in (
|
|
548
|
+
"function_declaration",
|
|
549
|
+
"protocol_function_declaration",
|
|
550
|
+
):
|
|
551
|
+
swift_func_name: str | None = None
|
|
552
|
+
for sc in current.children:
|
|
553
|
+
if sc.type == "simple_identifier":
|
|
554
|
+
swift_func_name = _text(sc)
|
|
555
|
+
break
|
|
556
|
+
if swift_func_name is None:
|
|
557
|
+
current = current.parent
|
|
558
|
+
continue
|
|
559
|
+
func_name = swift_func_name
|
|
560
|
+
# Walk up for enclosing class_declaration or protocol_declaration
|
|
561
|
+
parent = current.parent
|
|
562
|
+
while parent is not None:
|
|
563
|
+
if parent.type in class_types_swift:
|
|
564
|
+
# Swift class_declaration has type_identifier child (not 'name' field)
|
|
565
|
+
# extension also uses type_identifier inside user_type
|
|
566
|
+
cls_name: str | None = None
|
|
567
|
+
kw_seen = False
|
|
568
|
+
for pc in parent.children:
|
|
569
|
+
if pc.type in ("class", "struct", "actor", "extension", "enum"):
|
|
570
|
+
kw_seen = True
|
|
571
|
+
continue
|
|
572
|
+
if kw_seen and pc.type == "type_identifier":
|
|
573
|
+
cls_name = _text(pc)
|
|
574
|
+
break
|
|
575
|
+
if kw_seen and pc.type == "user_type":
|
|
576
|
+
# extension node: name in user_type → type_identifier
|
|
577
|
+
for uc in pc.children:
|
|
578
|
+
if uc.type == "type_identifier":
|
|
579
|
+
cls_name = _text(uc)
|
|
580
|
+
break
|
|
581
|
+
if cls_name:
|
|
582
|
+
break
|
|
583
|
+
if kw_seen and pc.type in ("class_body", "protocol_body"):
|
|
584
|
+
break
|
|
585
|
+
if cls_name:
|
|
586
|
+
return f"{cls_name}.{func_name}"
|
|
587
|
+
# Protocol declaration: name is type_identifier directly
|
|
588
|
+
if parent.type == "protocol_declaration":
|
|
589
|
+
for pc in parent.children:
|
|
590
|
+
if pc.type == "type_identifier":
|
|
591
|
+
cls_name = _text(pc)
|
|
592
|
+
break
|
|
593
|
+
if cls_name:
|
|
594
|
+
return f"{cls_name}.{func_name}"
|
|
595
|
+
parent = parent.parent
|
|
596
|
+
return func_name
|
|
597
|
+
|
|
598
|
+
# Named scope (function_declaration, method_definition, function_definition).
|
|
599
|
+
name_node = current.child_by_field_name("name")
|
|
600
|
+
if name_node is None:
|
|
601
|
+
current = current.parent
|
|
602
|
+
continue
|
|
603
|
+
func_name = _text(name_node)
|
|
604
|
+
|
|
605
|
+
# Determine the set of class-context types for this language.
|
|
606
|
+
if language == "python":
|
|
607
|
+
class_types: set[str] = {"class_definition"}
|
|
608
|
+
elif language == "java":
|
|
609
|
+
class_types = class_types_java
|
|
610
|
+
elif language == "csharp":
|
|
611
|
+
class_types = class_types_csharp
|
|
612
|
+
elif language == "ruby":
|
|
613
|
+
class_types = class_types_ruby
|
|
614
|
+
elif language == "cpp":
|
|
615
|
+
class_types = class_types_cpp
|
|
616
|
+
elif language == "php":
|
|
617
|
+
class_types = class_types_php
|
|
618
|
+
elif language == "swift":
|
|
619
|
+
class_types = class_types_swift
|
|
620
|
+
elif language == "c":
|
|
621
|
+
# C has no classes — never qualify
|
|
622
|
+
class_types = set()
|
|
623
|
+
else:
|
|
624
|
+
# TypeScript/JavaScript
|
|
625
|
+
class_types = {"class_declaration", "class_body"}
|
|
626
|
+
|
|
627
|
+
parent = current.parent
|
|
628
|
+
while parent is not None:
|
|
629
|
+
if parent.type in class_types:
|
|
630
|
+
class_name_node = parent.child_by_field_name("name")
|
|
631
|
+
if class_name_node is not None:
|
|
632
|
+
cls_name = _text(class_name_node)
|
|
633
|
+
return f"{cls_name}.{func_name}"
|
|
634
|
+
parent = parent.parent
|
|
635
|
+
return func_name
|
|
636
|
+
current = current.parent
|
|
637
|
+
|
|
638
|
+
# No named function/method found; return arrow fallback or None.
|
|
639
|
+
if fallback_arrow_name is None:
|
|
640
|
+
logger.debug(
|
|
641
|
+
"_find_enclosing_function: no named scope found — edge source "
|
|
642
|
+
"cannot be resolved; edge will be dropped"
|
|
643
|
+
)
|
|
644
|
+
return fallback_arrow_name
|