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 import mapping extraction and resolution — Java, C#, Ruby, C, C++, PHP.
|
|
2
|
+
|
|
3
|
+
LAYER: pure leaf module — imports only stdlib + tree_sitter types.
|
|
4
|
+
Must NOT import from seam.query, seam.server, confidence.py, or any other
|
|
5
|
+
seam module (same contract as imports.py).
|
|
6
|
+
|
|
7
|
+
LAYERING:
|
|
8
|
+
imports_ext (this file — leaf, no seam deps)
|
|
9
|
+
↑
|
|
10
|
+
imports.py (dispatch entry point — imports this at top level)
|
|
11
|
+
|
|
12
|
+
Entry points (called from imports.extract_import_mappings and
|
|
13
|
+
imports.resolve_import_source):
|
|
14
|
+
_extract_<lang>(root, filepath) -> list[ImportMapping]
|
|
15
|
+
_resolve_<lang>(source, ref, root) -> list[str]
|
|
16
|
+
|
|
17
|
+
All functions NEVER raise. On failure they return [] (empty list),
|
|
18
|
+
degrading cleanly to the name-count resolution rule in confidence.py.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, TypedDict
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# WHY this type alias: imports_ext is a leaf that must not import from imports.py
|
|
28
|
+
# (imports.py imports imports_ext at its top level). We use TypedDict locally to
|
|
29
|
+
# construct ImportMapping-compatible dicts without the circular import.
|
|
30
|
+
_ImportMappingList = list[Any]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _ImportMapping(TypedDict):
|
|
34
|
+
"""Local copy of ImportMapping TypedDict to avoid circular import from imports.py."""
|
|
35
|
+
|
|
36
|
+
local_name: str
|
|
37
|
+
exported_name: str
|
|
38
|
+
source_module: str
|
|
39
|
+
is_default: bool
|
|
40
|
+
is_namespace: bool
|
|
41
|
+
is_wildcard: bool
|
|
42
|
+
line: int
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _text_node(node: Any) -> str:
|
|
46
|
+
"""Safely decode a tree-sitter node's text bytes to str."""
|
|
47
|
+
try:
|
|
48
|
+
raw = node.text
|
|
49
|
+
if raw is None:
|
|
50
|
+
return ""
|
|
51
|
+
return raw.decode("utf-8", errors="replace")
|
|
52
|
+
except Exception: # noqa: BLE001
|
|
53
|
+
return ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _make_mapping(
|
|
57
|
+
local_name: str,
|
|
58
|
+
exported_name: str,
|
|
59
|
+
source_module: str,
|
|
60
|
+
line: int,
|
|
61
|
+
*,
|
|
62
|
+
is_default: bool = False,
|
|
63
|
+
is_namespace: bool = False,
|
|
64
|
+
is_wildcard: bool = False,
|
|
65
|
+
) -> _ImportMapping:
|
|
66
|
+
"""Construct an ImportMapping dict with safe defaults."""
|
|
67
|
+
return _ImportMapping(
|
|
68
|
+
local_name=local_name,
|
|
69
|
+
exported_name=exported_name,
|
|
70
|
+
source_module=source_module,
|
|
71
|
+
is_default=is_default,
|
|
72
|
+
is_namespace=is_namespace,
|
|
73
|
+
is_wildcard=is_wildcard,
|
|
74
|
+
line=line,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ── Java ──────────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _java_scoped_identifier_last_segment(node: Any) -> str | None:
|
|
82
|
+
"""Extract the rightmost identifier from a Java scoped_identifier or identifier.
|
|
83
|
+
|
|
84
|
+
scoped_identifier.name → rightmost segment (e.g. 'List' from java.util.List)
|
|
85
|
+
identifier → the text itself
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
if node.type == "identifier":
|
|
89
|
+
return _text_node(node)
|
|
90
|
+
if node.type == "scoped_identifier":
|
|
91
|
+
name_node = node.child_by_field_name("name")
|
|
92
|
+
if name_node is not None:
|
|
93
|
+
return _text_node(name_node)
|
|
94
|
+
except Exception: # noqa: BLE001
|
|
95
|
+
pass
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _java_scoped_identifier_full(node: Any) -> str:
|
|
100
|
+
"""Return the full dotted name of a scoped_identifier or identifier.
|
|
101
|
+
|
|
102
|
+
Used for the source_module field — e.g. 'java.util.List'.
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
return _text_node(node).strip()
|
|
106
|
+
except Exception: # noqa: BLE001
|
|
107
|
+
return ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _extract_java(root: object, filepath: Path) -> _ImportMappingList:
|
|
111
|
+
"""Extract ImportMapping records from a Java AST root node.
|
|
112
|
+
|
|
113
|
+
Handles:
|
|
114
|
+
import java.util.List; → local_name='List', source_module='java.util.List'
|
|
115
|
+
import java.util.*; → is_wildcard=True (skipped — no single target)
|
|
116
|
+
import static java.lang.Math.abs; → local_name='abs', source_module='java.lang.Math.abs'
|
|
117
|
+
|
|
118
|
+
Resolution: Java package-to-directory resolution is out of scope per the spec;
|
|
119
|
+
resolve always returns [] (degrades to name-count rule in confidence.py).
|
|
120
|
+
Never raises. Returns [] on any failure.
|
|
121
|
+
"""
|
|
122
|
+
result: _ImportMappingList = []
|
|
123
|
+
try:
|
|
124
|
+
from tree_sitter import Node
|
|
125
|
+
|
|
126
|
+
if not isinstance(root, Node):
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
def _walk(node: Any) -> None:
|
|
130
|
+
if node.type == "import_declaration":
|
|
131
|
+
line = node.start_point[0] + 1
|
|
132
|
+
# WHY pre-scan: in `import java.util.*;` the tree is:
|
|
133
|
+
# import_declaration → [scoped_identifier('java.util'), '.', asterisk, ';']
|
|
134
|
+
# Iterating named_children left-to-right processes 'java.util' BEFORE the
|
|
135
|
+
# asterisk, which would emit 'util' as a spurious mapping. Pre-scan prevents.
|
|
136
|
+
if any(child.type == "asterisk" for child in node.children):
|
|
137
|
+
# Wildcard import (`import java.util.*;`) — no single binding target.
|
|
138
|
+
# Skip entirely: emitting 'util' (the scope before the *) would be a
|
|
139
|
+
# spurious mapping that does not correspond to any importable symbol.
|
|
140
|
+
# This matches the convention in imports.py where wildcard imports that
|
|
141
|
+
# cannot be meaningfully resolved are omitted from the mapping list.
|
|
142
|
+
return # No need to recurse.
|
|
143
|
+
|
|
144
|
+
for child in node.named_children:
|
|
145
|
+
last = _java_scoped_identifier_last_segment(child)
|
|
146
|
+
full = _java_scoped_identifier_full(child)
|
|
147
|
+
if last:
|
|
148
|
+
result.append(
|
|
149
|
+
_make_mapping(
|
|
150
|
+
local_name=last,
|
|
151
|
+
exported_name=last,
|
|
152
|
+
source_module=full,
|
|
153
|
+
line=line,
|
|
154
|
+
is_default=True,
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
return # No need to recurse into import_declaration children.
|
|
158
|
+
for child in node.named_children:
|
|
159
|
+
_walk(child)
|
|
160
|
+
|
|
161
|
+
_walk(root)
|
|
162
|
+
except Exception as exc: # noqa: BLE001
|
|
163
|
+
logger.debug("_extract_java: extraction failed for %s: %r", filepath, exc)
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _resolve_java(
|
|
168
|
+
source_module: str,
|
|
169
|
+
referencing_file: Path,
|
|
170
|
+
repo_root: Path,
|
|
171
|
+
) -> list[str]:
|
|
172
|
+
"""Resolve a Java import path to source file paths.
|
|
173
|
+
|
|
174
|
+
Package-to-directory resolution is out of scope for the MVP (see PRD §implementation).
|
|
175
|
+
Returns [] — degrading cleanly to the name-count resolution rule.
|
|
176
|
+
"""
|
|
177
|
+
return [] # Out of scope per spec — Java package resolution not implemented
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ── C# ────────────────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _csharp_using_last_segment(node: Any) -> str | None:
|
|
184
|
+
"""Extract the rightmost identifier from a C# qualified_name or identifier.
|
|
185
|
+
|
|
186
|
+
qualified_name.name → rightmost segment (e.g. 'Generic' from System.Collections.Generic)
|
|
187
|
+
identifier → the text itself
|
|
188
|
+
"""
|
|
189
|
+
try:
|
|
190
|
+
if node.type == "identifier":
|
|
191
|
+
return _text_node(node)
|
|
192
|
+
if node.type == "qualified_name":
|
|
193
|
+
name_node = node.child_by_field_name("name")
|
|
194
|
+
if name_node is not None:
|
|
195
|
+
return _text_node(name_node)
|
|
196
|
+
except Exception: # noqa: BLE001
|
|
197
|
+
pass
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _extract_csharp(root: object, filepath: Path) -> _ImportMappingList:
|
|
202
|
+
"""Extract ImportMapping records from a C# AST root node.
|
|
203
|
+
|
|
204
|
+
Handles:
|
|
205
|
+
using System; → local_name='System'
|
|
206
|
+
using System.Collections.Generic; → local_name='Generic'
|
|
207
|
+
|
|
208
|
+
Resolution: C# namespace-to-directory resolution is out of scope per the spec.
|
|
209
|
+
Never raises. Returns [] on any failure.
|
|
210
|
+
"""
|
|
211
|
+
result: _ImportMappingList = []
|
|
212
|
+
try:
|
|
213
|
+
from tree_sitter import Node
|
|
214
|
+
|
|
215
|
+
if not isinstance(root, Node):
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
def _walk(node: Any) -> None:
|
|
219
|
+
if node.type == "using_directive":
|
|
220
|
+
line = node.start_point[0] + 1
|
|
221
|
+
named = node.named_children
|
|
222
|
+
# WHY alias detection: `using Foo = System.Collections.Generic;` produces:
|
|
223
|
+
# using_directive → [using, identifier('Foo'), '=', qualified_name(...), ';']
|
|
224
|
+
# Without this check, the first named child (identifier 'Foo') is emitted as
|
|
225
|
+
# the import target — a spurious alias edge. Detect the alias form by checking
|
|
226
|
+
# for BOTH an identifier AND a qualified_name sibling; if present, use only
|
|
227
|
+
# the qualified_name's last segment; the alias identifier is the local_name.
|
|
228
|
+
has_identifier = any(c.type == "identifier" for c in named)
|
|
229
|
+
has_qualified = any(c.type == "qualified_name" for c in named)
|
|
230
|
+
if has_identifier and has_qualified:
|
|
231
|
+
# Alias form: record alias as local_name, real namespace as exported_name.
|
|
232
|
+
alias = next((_text_node(c) for c in named if c.type == "identifier"), None)
|
|
233
|
+
for child in named:
|
|
234
|
+
if child.type == "qualified_name":
|
|
235
|
+
last = _csharp_using_last_segment(child)
|
|
236
|
+
full = _text_node(child).strip()
|
|
237
|
+
if last and alias:
|
|
238
|
+
result.append(
|
|
239
|
+
_make_mapping(
|
|
240
|
+
local_name=alias,
|
|
241
|
+
exported_name=last,
|
|
242
|
+
source_module=full,
|
|
243
|
+
line=line,
|
|
244
|
+
is_default=True,
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
return # No further children needed.
|
|
248
|
+
|
|
249
|
+
# Non-alias form: emit the last segment of whichever name is present.
|
|
250
|
+
for child in named:
|
|
251
|
+
last = _csharp_using_last_segment(child)
|
|
252
|
+
full = _text_node(child).strip()
|
|
253
|
+
if last:
|
|
254
|
+
result.append(
|
|
255
|
+
_make_mapping(
|
|
256
|
+
local_name=last,
|
|
257
|
+
exported_name=last,
|
|
258
|
+
source_module=full,
|
|
259
|
+
line=line,
|
|
260
|
+
is_default=True,
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
return # No need to recurse into using_directive children.
|
|
264
|
+
for child in node.named_children:
|
|
265
|
+
_walk(child)
|
|
266
|
+
|
|
267
|
+
_walk(root)
|
|
268
|
+
except Exception as exc: # noqa: BLE001
|
|
269
|
+
logger.debug("_extract_csharp: extraction failed for %s: %r", filepath, exc)
|
|
270
|
+
return result
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _resolve_csharp(
|
|
274
|
+
source_module: str,
|
|
275
|
+
referencing_file: Path,
|
|
276
|
+
repo_root: Path,
|
|
277
|
+
) -> list[str]:
|
|
278
|
+
"""Resolve a C# using directive to source file paths.
|
|
279
|
+
|
|
280
|
+
Namespace-to-directory resolution is out of scope for the MVP.
|
|
281
|
+
Returns [] — degrading cleanly to the name-count resolution rule.
|
|
282
|
+
"""
|
|
283
|
+
return [] # Out of scope per spec — C# namespace resolution not implemented
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ── Ruby ──────────────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _ruby_require_string_content(string_node: Any) -> str | None:
|
|
290
|
+
"""Extract the content from a Ruby string node.
|
|
291
|
+
|
|
292
|
+
Ruby string: string → [" string_content "]
|
|
293
|
+
Falls back to stripping quotes from full text if string_content not found.
|
|
294
|
+
"""
|
|
295
|
+
try:
|
|
296
|
+
for child in string_node.named_children:
|
|
297
|
+
if child.type == "string_content":
|
|
298
|
+
return _text_node(child)
|
|
299
|
+
# Fallback: strip surrounding single/double quotes.
|
|
300
|
+
raw = _text_node(string_node)
|
|
301
|
+
return raw.strip("'\"")
|
|
302
|
+
except Exception: # noqa: BLE001
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _extract_ruby(root: object, filepath: Path) -> _ImportMappingList:
|
|
307
|
+
"""Extract ImportMapping records from a Ruby AST root node.
|
|
308
|
+
|
|
309
|
+
Handles:
|
|
310
|
+
require 'json' → local_name='json', source_module='json'
|
|
311
|
+
require_relative './x' → local_name='x', source_module='./x'
|
|
312
|
+
require 'active_record' → local_name='active_record', source_module='active_record'
|
|
313
|
+
|
|
314
|
+
Resolution: require_relative './x' → relative .rb file probe (via _resolve_ruby).
|
|
315
|
+
require 'x' → [] (gem/load path out of scope per spec).
|
|
316
|
+
Never raises. Returns [] on any failure.
|
|
317
|
+
"""
|
|
318
|
+
result: _ImportMappingList = []
|
|
319
|
+
try:
|
|
320
|
+
from tree_sitter import Node
|
|
321
|
+
|
|
322
|
+
if not isinstance(root, Node):
|
|
323
|
+
return result
|
|
324
|
+
|
|
325
|
+
def _walk(node: Any) -> None:
|
|
326
|
+
# Ruby require/require_relative appear as top-level 'call' nodes.
|
|
327
|
+
if node.type == "call":
|
|
328
|
+
method_node = node.child_by_field_name("method")
|
|
329
|
+
if method_node is None or method_node.type != "identifier":
|
|
330
|
+
# Not a plain identifier call — skip, still recurse.
|
|
331
|
+
for child in node.named_children:
|
|
332
|
+
_walk(child)
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
method_name = _text_node(method_node)
|
|
336
|
+
line = node.start_point[0] + 1
|
|
337
|
+
|
|
338
|
+
if method_name in ("require", "require_relative"):
|
|
339
|
+
arg_list = node.child_by_field_name("arguments")
|
|
340
|
+
if arg_list is not None:
|
|
341
|
+
for child in arg_list.named_children:
|
|
342
|
+
if child.type == "string":
|
|
343
|
+
content = _ruby_require_string_content(child)
|
|
344
|
+
if content:
|
|
345
|
+
from pathlib import Path as _Path
|
|
346
|
+
|
|
347
|
+
local = _Path(content).stem
|
|
348
|
+
result.append(
|
|
349
|
+
_make_mapping(
|
|
350
|
+
local_name=local,
|
|
351
|
+
exported_name=local,
|
|
352
|
+
source_module=content,
|
|
353
|
+
line=line,
|
|
354
|
+
is_default=True,
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
return # don't recurse into require call
|
|
358
|
+
|
|
359
|
+
# Recurse into other calls' children
|
|
360
|
+
for child in node.named_children:
|
|
361
|
+
_walk(child)
|
|
362
|
+
return
|
|
363
|
+
|
|
364
|
+
for child in node.named_children:
|
|
365
|
+
_walk(child)
|
|
366
|
+
|
|
367
|
+
_walk(root)
|
|
368
|
+
except Exception as exc: # noqa: BLE001
|
|
369
|
+
logger.debug("_extract_ruby: extraction failed for %s: %r", filepath, exc)
|
|
370
|
+
return result
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _resolve_ruby(
|
|
374
|
+
source_module: str,
|
|
375
|
+
referencing_file: Path,
|
|
376
|
+
repo_root: Path,
|
|
377
|
+
) -> list[str]:
|
|
378
|
+
"""Resolve a Ruby require source to file paths.
|
|
379
|
+
|
|
380
|
+
require_relative './x' → probe same directory as referencing_file for x.rb.
|
|
381
|
+
require 'x' → [] (gem / load path resolution out of scope per spec).
|
|
382
|
+
|
|
383
|
+
Never raises.
|
|
384
|
+
"""
|
|
385
|
+
try:
|
|
386
|
+
# Only resolve relative paths (those starting with ./ or ../).
|
|
387
|
+
if not (source_module.startswith("./") or source_module.startswith("../")):
|
|
388
|
+
return [] # absolute gem name — out of scope
|
|
389
|
+
|
|
390
|
+
ref_dir = referencing_file.parent
|
|
391
|
+
# The source_module may or may not include .rb extension.
|
|
392
|
+
if not source_module.endswith(".rb"):
|
|
393
|
+
candidate = ref_dir / (source_module + ".rb")
|
|
394
|
+
else:
|
|
395
|
+
candidate = ref_dir / source_module
|
|
396
|
+
|
|
397
|
+
if candidate.exists():
|
|
398
|
+
return [str(candidate)]
|
|
399
|
+
|
|
400
|
+
# Fallback: try without extension adjustment.
|
|
401
|
+
candidate2 = ref_dir / source_module
|
|
402
|
+
if candidate2.exists():
|
|
403
|
+
return [str(candidate2)]
|
|
404
|
+
except Exception: # noqa: BLE001
|
|
405
|
+
pass
|
|
406
|
+
return []
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# ── C ─────────────────────────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _c_extract_include(node: Any, filepath: Path) -> _ImportMapping | None:
|
|
413
|
+
"""Extract an ImportMapping from a C/C++ preproc_include node.
|
|
414
|
+
|
|
415
|
+
Handles:
|
|
416
|
+
#include "utils.h" → local_name='utils', source_module='utils.h'
|
|
417
|
+
#include <stdio.h> → local_name='stdio', source_module='<stdio.h>'
|
|
418
|
+
|
|
419
|
+
Returns None if the include path cannot be resolved.
|
|
420
|
+
"""
|
|
421
|
+
try:
|
|
422
|
+
path_node = node.child_by_field_name("path")
|
|
423
|
+
if path_node is None:
|
|
424
|
+
return None
|
|
425
|
+
|
|
426
|
+
line = node.start_point[0] + 1
|
|
427
|
+
|
|
428
|
+
if path_node.type == "string_literal":
|
|
429
|
+
# Local include: #include "utils.h"
|
|
430
|
+
content = None
|
|
431
|
+
for child in path_node.children:
|
|
432
|
+
if child.type == "string_content":
|
|
433
|
+
content = _text_node(child)
|
|
434
|
+
break
|
|
435
|
+
if content is None:
|
|
436
|
+
content = _text_node(path_node).strip('"')
|
|
437
|
+
if not content:
|
|
438
|
+
return None
|
|
439
|
+
stem = Path(content).stem
|
|
440
|
+
return _make_mapping(
|
|
441
|
+
local_name=stem,
|
|
442
|
+
exported_name=stem,
|
|
443
|
+
source_module=content,
|
|
444
|
+
line=line,
|
|
445
|
+
is_default=True,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
if path_node.type == "system_lib_string":
|
|
449
|
+
# System include: #include <stdio.h>
|
|
450
|
+
raw = _text_node(path_node).strip("<>")
|
|
451
|
+
if not raw:
|
|
452
|
+
return None
|
|
453
|
+
stem = Path(raw).stem
|
|
454
|
+
return _make_mapping(
|
|
455
|
+
local_name=stem,
|
|
456
|
+
exported_name=stem,
|
|
457
|
+
source_module=f"<{raw}>",
|
|
458
|
+
line=line,
|
|
459
|
+
is_default=True,
|
|
460
|
+
)
|
|
461
|
+
except Exception: # noqa: BLE001
|
|
462
|
+
pass
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _extract_c(root: object, filepath: Path) -> _ImportMappingList:
|
|
467
|
+
"""Extract ImportMapping records from a C AST root node.
|
|
468
|
+
|
|
469
|
+
Handles:
|
|
470
|
+
#include "utils.h" → local_name='utils', source_module='utils.h'
|
|
471
|
+
#include <stdio.h> → local_name='stdio', source_module='<stdio.h>'
|
|
472
|
+
|
|
473
|
+
Resolution: relative #include "x.h" → best-effort file path probe
|
|
474
|
+
(same dir, then repo_root). System #include <x> → [].
|
|
475
|
+
Never raises. Returns [] on any failure.
|
|
476
|
+
"""
|
|
477
|
+
result: _ImportMappingList = []
|
|
478
|
+
try:
|
|
479
|
+
from tree_sitter import Node
|
|
480
|
+
|
|
481
|
+
if not isinstance(root, Node):
|
|
482
|
+
return result
|
|
483
|
+
|
|
484
|
+
def _walk(node: Any) -> None:
|
|
485
|
+
if node.type == "preproc_include":
|
|
486
|
+
mapping = _c_extract_include(node, filepath)
|
|
487
|
+
if mapping is not None:
|
|
488
|
+
result.append(mapping)
|
|
489
|
+
return # no need to recurse into include node
|
|
490
|
+
for child in node.named_children:
|
|
491
|
+
_walk(child)
|
|
492
|
+
|
|
493
|
+
_walk(root)
|
|
494
|
+
except Exception as exc: # noqa: BLE001
|
|
495
|
+
logger.debug("_extract_c: extraction failed for %s: %r", filepath, exc)
|
|
496
|
+
return result
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _resolve_c(
|
|
500
|
+
source_module: str,
|
|
501
|
+
referencing_file: Path,
|
|
502
|
+
repo_root: Path,
|
|
503
|
+
) -> list[str]:
|
|
504
|
+
"""Resolve a C #include path to source file paths.
|
|
505
|
+
|
|
506
|
+
#include "x.h" → probe same directory as referencing_file, then repo_root.
|
|
507
|
+
#include <x.h> → [] (system header — out of scope per spec).
|
|
508
|
+
|
|
509
|
+
Returns a list of matching file paths (as str), or [] if not found.
|
|
510
|
+
Never raises.
|
|
511
|
+
"""
|
|
512
|
+
try:
|
|
513
|
+
# System includes start with '<' — out of scope per spec
|
|
514
|
+
if source_module.startswith("<"):
|
|
515
|
+
return []
|
|
516
|
+
|
|
517
|
+
# Local include: probe relative to the referencing file's directory
|
|
518
|
+
ref_dir = referencing_file.parent
|
|
519
|
+
candidate = ref_dir / source_module
|
|
520
|
+
if candidate.exists():
|
|
521
|
+
return [str(candidate)]
|
|
522
|
+
|
|
523
|
+
# Fallback: probe relative to repo root
|
|
524
|
+
candidate2 = repo_root / source_module
|
|
525
|
+
if candidate2.exists():
|
|
526
|
+
return [str(candidate2)]
|
|
527
|
+
except Exception: # noqa: BLE001
|
|
528
|
+
pass
|
|
529
|
+
return []
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
# ── C++ ───────────────────────────────────────────────────────────────────────
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _extract_cpp(root: object, filepath: Path) -> _ImportMappingList:
|
|
536
|
+
"""Extract ImportMapping records from a C++ AST root node.
|
|
537
|
+
|
|
538
|
+
C++ uses the same preproc_include mechanism as C. Both local and system
|
|
539
|
+
includes are extracted; system includes resolve to [] at read time.
|
|
540
|
+
Never raises. Returns [] on any failure.
|
|
541
|
+
"""
|
|
542
|
+
# C++ and C share the same preproc_include grammar node — reuse _extract_c.
|
|
543
|
+
return _extract_c(root, filepath)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _resolve_cpp(
|
|
547
|
+
source_module: str,
|
|
548
|
+
referencing_file: Path,
|
|
549
|
+
repo_root: Path,
|
|
550
|
+
) -> list[str]:
|
|
551
|
+
"""Resolve a C++ #include path to source file paths.
|
|
552
|
+
|
|
553
|
+
#include "x.h" → probe same dir, then repo_root (same as C).
|
|
554
|
+
#include <x> → [] (system/STL header — out of scope per spec).
|
|
555
|
+
|
|
556
|
+
Never raises.
|
|
557
|
+
"""
|
|
558
|
+
return _resolve_c(source_module, referencing_file, repo_root)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
# ── PHP ───────────────────────────────────────────────────────────────────────
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _php_qualified_name_last_segment(node: Any) -> str | None:
|
|
565
|
+
"""Extract the last 'name' segment from a PHP qualified_name node.
|
|
566
|
+
|
|
567
|
+
qualified_name: [namespace_name, "\\", name]
|
|
568
|
+
Returns the final 'name' child text (e.g. 'User' from 'App\\Models\\User').
|
|
569
|
+
Also handles bare 'name' nodes directly.
|
|
570
|
+
"""
|
|
571
|
+
try:
|
|
572
|
+
if node.type == "name":
|
|
573
|
+
return _text_node(node)
|
|
574
|
+
if node.type == "qualified_name":
|
|
575
|
+
# Find the last 'name' child (after all backslashes).
|
|
576
|
+
last_name = None
|
|
577
|
+
for child in node.children:
|
|
578
|
+
if child.type == "name":
|
|
579
|
+
last_name = _text_node(child)
|
|
580
|
+
return last_name
|
|
581
|
+
except Exception: # noqa: BLE001
|
|
582
|
+
pass
|
|
583
|
+
return None
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _php_clause_mapping(clause: Any, line: int) -> "_ImportMapping | None":
|
|
587
|
+
"""Extract an ImportMapping from a single PHP namespace_use_clause node.
|
|
588
|
+
|
|
589
|
+
Handles two forms:
|
|
590
|
+
Plain: namespace_use_clause → qualified_name (→ name segments)
|
|
591
|
+
local_name = exported_name = last segment (e.g. 'User')
|
|
592
|
+
Aliased: namespace_use_clause → qualified_name + 'as' + name (alias)
|
|
593
|
+
local_name = alias (e.g. 'Col'), exported_name = last segment (e.g. 'Collection')
|
|
594
|
+
WHY: consistent with TS/Rust alias convention — local_name is the
|
|
595
|
+
binding used in this file; exported_name is the real symbol name.
|
|
596
|
+
|
|
597
|
+
Returns None if the clause cannot be parsed.
|
|
598
|
+
"""
|
|
599
|
+
named = clause.named_children
|
|
600
|
+
if not named:
|
|
601
|
+
return None
|
|
602
|
+
|
|
603
|
+
# Detect alias form: named_children = [qualified_name, name(alias)]
|
|
604
|
+
# The alias 'name' follows the qualified_name (and the 'as' keyword child).
|
|
605
|
+
qual_node = None
|
|
606
|
+
alias_node = None
|
|
607
|
+
for child in named:
|
|
608
|
+
if child.type == "qualified_name":
|
|
609
|
+
qual_node = child
|
|
610
|
+
elif child.type == "name":
|
|
611
|
+
# This is either a bare clause (use Foo) or the alias after 'as'.
|
|
612
|
+
alias_node = child
|
|
613
|
+
|
|
614
|
+
if qual_node is not None:
|
|
615
|
+
exported = _php_qualified_name_last_segment(qual_node)
|
|
616
|
+
full = _text_node(qual_node).strip()
|
|
617
|
+
if not exported:
|
|
618
|
+
return None
|
|
619
|
+
if alias_node is not None:
|
|
620
|
+
# Aliased: use App\Foo as Bar → local_name='Bar', exported_name='Foo'
|
|
621
|
+
local = _text_node(alias_node).strip()
|
|
622
|
+
else:
|
|
623
|
+
# Plain: use App\Foo → local_name='Foo', exported_name='Foo'
|
|
624
|
+
local = exported
|
|
625
|
+
return _make_mapping(
|
|
626
|
+
local_name=local,
|
|
627
|
+
exported_name=exported,
|
|
628
|
+
source_module=full,
|
|
629
|
+
line=line,
|
|
630
|
+
is_default=True,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
if alias_node is not None:
|
|
634
|
+
# Bare name clause (e.g. inside a group: use App\{Foo} → bare 'Foo')
|
|
635
|
+
name = _text_node(alias_node).strip()
|
|
636
|
+
if name:
|
|
637
|
+
return _make_mapping(
|
|
638
|
+
local_name=name,
|
|
639
|
+
exported_name=name,
|
|
640
|
+
source_module=name,
|
|
641
|
+
line=line,
|
|
642
|
+
is_default=True,
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
return None
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _extract_php(root: object, filepath: Path) -> _ImportMappingList:
|
|
649
|
+
"""Extract ImportMapping records from a PHP AST root node.
|
|
650
|
+
|
|
651
|
+
Handles:
|
|
652
|
+
use App\\Models\\User; → local_name='User', source='App\\Models\\User'
|
|
653
|
+
use App\\Support\\Collection as Col → local_name='Col', exported_name='Collection'
|
|
654
|
+
use App\\{Foo, Bar}; → local_name='Foo'; local_name='Bar'
|
|
655
|
+
|
|
656
|
+
Resolution: PSR-4 autoload mapping is out of scope per the spec → returns [].
|
|
657
|
+
Never raises. Returns [] on any failure.
|
|
658
|
+
"""
|
|
659
|
+
result: _ImportMappingList = []
|
|
660
|
+
try:
|
|
661
|
+
from tree_sitter import Node
|
|
662
|
+
|
|
663
|
+
if not isinstance(root, Node):
|
|
664
|
+
return result
|
|
665
|
+
|
|
666
|
+
def _walk(node: Any) -> None:
|
|
667
|
+
if node.type == "namespace_use_declaration":
|
|
668
|
+
line = node.start_point[0] + 1
|
|
669
|
+
for child in node.named_children:
|
|
670
|
+
if child.type == "namespace_use_clause":
|
|
671
|
+
mapping = _php_clause_mapping(child, line)
|
|
672
|
+
if mapping is not None:
|
|
673
|
+
result.append(mapping)
|
|
674
|
+
elif child.type == "namespace_use_group":
|
|
675
|
+
# Grouped use: use App\{Foo, Bar} — descend into the group.
|
|
676
|
+
for clause in child.named_children:
|
|
677
|
+
if clause.type == "namespace_use_clause":
|
|
678
|
+
mapping = _php_clause_mapping(clause, line)
|
|
679
|
+
if mapping is not None:
|
|
680
|
+
result.append(mapping)
|
|
681
|
+
return # no recursion needed into use declaration
|
|
682
|
+
|
|
683
|
+
for child in node.named_children:
|
|
684
|
+
_walk(child)
|
|
685
|
+
|
|
686
|
+
_walk(root)
|
|
687
|
+
except Exception as exc: # noqa: BLE001
|
|
688
|
+
logger.debug("_extract_php: extraction failed for %s: %r", filepath, exc)
|
|
689
|
+
return result
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def _resolve_php(
|
|
693
|
+
source_module: str,
|
|
694
|
+
referencing_file: Path,
|
|
695
|
+
repo_root: Path,
|
|
696
|
+
) -> list[str]:
|
|
697
|
+
"""Resolve a PHP use statement to source file paths.
|
|
698
|
+
|
|
699
|
+
PSR-4 autoload mapping is out of scope per the spec.
|
|
700
|
+
Returns [] — degrading cleanly to the name-count resolution rule.
|
|
701
|
+
|
|
702
|
+
WHY: PHP PSR-4 requires a composer.json namespace→directory mapping which
|
|
703
|
+
is not available at extraction time. Returning [] causes confidence.py to
|
|
704
|
+
fall back to the name-count rule (AMBIGUOUS if multiple declarations exist).
|
|
705
|
+
"""
|
|
706
|
+
return [] # Out of scope per spec — PHP PSR-4 autoload not implemented
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
# ── Swift ─────────────────────────────────────────────────────────────────────
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _extract_swift(root: object, filepath: Path) -> _ImportMappingList:
|
|
713
|
+
"""Extract ImportMapping records from a Swift AST root node.
|
|
714
|
+
|
|
715
|
+
Handles:
|
|
716
|
+
import Foundation → local_name='Foundation', source_module='Foundation'
|
|
717
|
+
import UIKit.UIView → local_name='UIView', source_module='UIKit.UIView'
|
|
718
|
+
(last segment of dotted path per spec)
|
|
719
|
+
|
|
720
|
+
Resolution: Swift modules are not file-path-resolvable in-repo without a build
|
|
721
|
+
graph (frameworks live outside the project tree). _resolve_swift always returns [].
|
|
722
|
+
Never raises. Returns [] on any failure.
|
|
723
|
+
"""
|
|
724
|
+
result: _ImportMappingList = []
|
|
725
|
+
try:
|
|
726
|
+
from tree_sitter import Node
|
|
727
|
+
|
|
728
|
+
if not isinstance(root, Node):
|
|
729
|
+
return result
|
|
730
|
+
|
|
731
|
+
def _walk(node: Any) -> None:
|
|
732
|
+
if node.type == "import_declaration":
|
|
733
|
+
line = node.start_point[0] + 1
|
|
734
|
+
# Find the 'identifier' child → collect simple_identifier segments → last one.
|
|
735
|
+
for child in node.children:
|
|
736
|
+
if child.type == "identifier":
|
|
737
|
+
segments = [
|
|
738
|
+
_text_node(gc)
|
|
739
|
+
for gc in child.children
|
|
740
|
+
if gc.type == "simple_identifier"
|
|
741
|
+
]
|
|
742
|
+
if segments:
|
|
743
|
+
target = segments[-1]
|
|
744
|
+
full = ".".join(s for s in segments if s)
|
|
745
|
+
result.append(
|
|
746
|
+
_make_mapping(
|
|
747
|
+
local_name=target,
|
|
748
|
+
exported_name=target,
|
|
749
|
+
source_module=full,
|
|
750
|
+
line=line,
|
|
751
|
+
is_default=True,
|
|
752
|
+
)
|
|
753
|
+
)
|
|
754
|
+
break
|
|
755
|
+
return # No recursion into import_declaration needed.
|
|
756
|
+
for child in node.named_children:
|
|
757
|
+
_walk(child)
|
|
758
|
+
|
|
759
|
+
_walk(root)
|
|
760
|
+
except Exception as exc: # noqa: BLE001
|
|
761
|
+
logger.debug("_extract_swift: extraction failed for %s: %r", filepath, exc)
|
|
762
|
+
return result
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def _resolve_swift(
|
|
766
|
+
source_module: str,
|
|
767
|
+
referencing_file: Path,
|
|
768
|
+
repo_root: Path,
|
|
769
|
+
) -> list[str]:
|
|
770
|
+
"""Resolve a Swift import path to source file paths.
|
|
771
|
+
|
|
772
|
+
Swift modules (Foundation, UIKit, SwiftUI, etc.) are Apple SDK frameworks
|
|
773
|
+
and are NOT file-path-resolvable within the repo without a full build graph.
|
|
774
|
+
Returns [] — degrading cleanly to the name-count resolution rule in confidence.py.
|
|
775
|
+
|
|
776
|
+
WHY: Swift module resolution requires Xcode project / SwiftPM manifest parsing
|
|
777
|
+
which is out of scope per the spec. The degraded name-count rule still provides
|
|
778
|
+
EXTRACTED/AMBIGUOUS distinction for in-repo symbols.
|
|
779
|
+
"""
|
|
780
|
+
return [] # Out of scope per spec — Swift module resolution not implemented
|