codemap-rust 0.1.0__tar.gz

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.
@@ -0,0 +1,43 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Build artifacts
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ *.egg
11
+ .eggs/
12
+
13
+ # Test / coverage
14
+ .pytest_cache/
15
+ .coverage
16
+ .coverage.*
17
+ htmlcov/
18
+ coverage.xml
19
+ .tox/
20
+ .mypy_cache/
21
+ .ruff_cache/
22
+ .benchmarks/
23
+
24
+ # Virtualenv
25
+ .venv/
26
+ venv/
27
+ env/
28
+
29
+ # uv / pdm lockfiles (commit uv.lock once we settle)
30
+ # uv.lock
31
+
32
+ # IDE
33
+ .idea/
34
+ .vscode/
35
+ *.swp
36
+ *.swo
37
+
38
+ # OS
39
+ .DS_Store
40
+ Thumbs.db
41
+
42
+ # CodeMap own index when dogfooding
43
+ .codemap/
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: codemap-rust
3
+ Version: 0.1.0
4
+ Summary: Rust indexer plugin for CodeMap
5
+ Project-URL: Homepage, https://github.com/qxbyte/codemap
6
+ Author: CodeMap Contributors
7
+ License: MIT
8
+ Keywords: codemap,indexer,rust,tree-sitter
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Rust
12
+ Classifier: Topic :: Software Development
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: codemap-core<0.2,>=0.1.0
15
+ Requires-Dist: tree-sitter-rust>=0.23
16
+ Requires-Dist: tree-sitter>=0.25
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=8.0; extra == 'dev'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # codemap-rust
22
+
23
+ > A Rust indexer for [CodeMap](https://github.com/qxbyte/codemap), shipped
24
+ > as an independent PyPI package.
25
+
26
+ ## What it captures
27
+
28
+ Backed by `tree-sitter-rust`:
29
+
30
+ | AST node | Symbol kind |
31
+ |---|---|
32
+ | `function_item` (free) | `function` |
33
+ | `function_item` (inside `impl`) | `method` (attached to the impl'd type) |
34
+ | `function_signature_item` (inside `trait`) | `method` (attached to the trait) |
35
+ | `struct_item` | `class` (with `extra.rust_kind=struct`) |
36
+ | `enum_item` | `class` (with `extra.rust_kind=enum`) |
37
+ | `trait_item` | `class` (with `extra.rust_kind=trait`) |
38
+ | `const_item` | `variable` (with `extra.rust_kind=const`) |
39
+ | `static_item` | `variable` (with `extra.rust_kind=static`) |
40
+
41
+ `impl Trait for Type` blocks attach methods to `Type` (the receiver), so
42
+ both inherent and trait impls end up addressable under the same
43
+ `Type#method()` shape.
44
+
45
+ ## SymbolID encoding
46
+
47
+ ```
48
+ scip-rust . . . src/user.rs/User#login().
49
+ ```
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ pip install codemap-rust
55
+ ```
56
+
57
+ After install, `codemap doctor` lists `rust` alongside the other
58
+ indexers on identical terms (ADR-004 + ADR-L001).
59
+
60
+ ## Limits
61
+
62
+ * Methods on generic types (`impl<T> Foo<T>`) attach to `Foo` without
63
+ the generic parameter — adequate for code search, lossy for type
64
+ checking.
65
+ * Macros are not expanded; their declarations are not captured as
66
+ symbols.
67
+ * `mod` declarations are not turned into namespace prefixes; the file
68
+ path is the only namespace.
69
+
70
+ ## License
71
+
72
+ MIT.
@@ -0,0 +1,52 @@
1
+ # codemap-rust
2
+
3
+ > A Rust indexer for [CodeMap](https://github.com/qxbyte/codemap), shipped
4
+ > as an independent PyPI package.
5
+
6
+ ## What it captures
7
+
8
+ Backed by `tree-sitter-rust`:
9
+
10
+ | AST node | Symbol kind |
11
+ |---|---|
12
+ | `function_item` (free) | `function` |
13
+ | `function_item` (inside `impl`) | `method` (attached to the impl'd type) |
14
+ | `function_signature_item` (inside `trait`) | `method` (attached to the trait) |
15
+ | `struct_item` | `class` (with `extra.rust_kind=struct`) |
16
+ | `enum_item` | `class` (with `extra.rust_kind=enum`) |
17
+ | `trait_item` | `class` (with `extra.rust_kind=trait`) |
18
+ | `const_item` | `variable` (with `extra.rust_kind=const`) |
19
+ | `static_item` | `variable` (with `extra.rust_kind=static`) |
20
+
21
+ `impl Trait for Type` blocks attach methods to `Type` (the receiver), so
22
+ both inherent and trait impls end up addressable under the same
23
+ `Type#method()` shape.
24
+
25
+ ## SymbolID encoding
26
+
27
+ ```
28
+ scip-rust . . . src/user.rs/User#login().
29
+ ```
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install codemap-rust
35
+ ```
36
+
37
+ After install, `codemap doctor` lists `rust` alongside the other
38
+ indexers on identical terms (ADR-004 + ADR-L001).
39
+
40
+ ## Limits
41
+
42
+ * Methods on generic types (`impl<T> Foo<T>`) attach to `Foo` without
43
+ the generic parameter — adequate for code search, lossy for type
44
+ checking.
45
+ * Macros are not expanded; their declarations are not captured as
46
+ symbols.
47
+ * `mod` declarations are not turned into namespace prefixes; the file
48
+ path is the only namespace.
49
+
50
+ ## License
51
+
52
+ MIT.
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "codemap-rust"
7
+ version = "0.1.0"
8
+ description = "Rust indexer plugin for CodeMap"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "CodeMap Contributors" }]
13
+ keywords = ["codemap", "rust", "indexer", "tree-sitter"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Rust",
18
+ "Topic :: Software Development",
19
+ ]
20
+ dependencies = [
21
+ "codemap-core>=0.1.0,<0.2",
22
+ "tree-sitter>=0.25",
23
+ "tree-sitter-rust>=0.23",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ dev = ["pytest>=8.0"]
28
+
29
+ [project.entry-points."codemap.indexers"]
30
+ rust = "codemap_rust:RustIndexer"
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/qxbyte/codemap"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/codemap_rust"]
@@ -0,0 +1,8 @@
1
+ """Rust indexer plugin for CodeMap."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from codemap_rust.indexer import RustIndexer
6
+
7
+ __all__ = ["RustIndexer"]
8
+ __version__ = "0.1.0"
@@ -0,0 +1,324 @@
1
+ """Rust indexer built on tree-sitter-rust.
2
+
3
+ Notable Rust-specific behaviour: ``impl_item`` blocks (both inherent and
4
+ trait impls) attach their inner functions to the impl'd type's namespace,
5
+ so ``impl User { fn login(&self) {} }`` and ``impl Greeter for User {
6
+ fn hello(&self) {} }`` both produce ``User#xxx().`` SymbolIDs. Methods
7
+ defined directly on a trait (``trait T { fn m(&self); }``) attach to the
8
+ trait the same way.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path, PurePosixPath
14
+ from typing import ClassVar
15
+
16
+ import tree_sitter
17
+ import tree_sitter_rust
18
+
19
+ from codemap.core.models import Diagnostic, Edge, IndexResult, Range, Symbol
20
+ from codemap.core.symbol import Descriptor, DescriptorKind, SymbolID
21
+ from codemap.indexers.base import IndexContext
22
+
23
+ SCHEME = "scip-rust"
24
+ LANG = "rust"
25
+
26
+ _RUST_LANG = tree_sitter.Language(tree_sitter_rust.language())
27
+
28
+
29
+ class RustIndexer:
30
+ name: ClassVar[str] = "rust"
31
+ version: ClassVar[str] = "0.1.0"
32
+ file_patterns: ClassVar[list[str]] = ["*.rs"]
33
+ languages: ClassVar[list[str]] = [LANG]
34
+
35
+ def supports(self, path: Path) -> bool:
36
+ return path.suffix == ".rs"
37
+
38
+ def index_file(
39
+ self,
40
+ path: Path,
41
+ source: bytes,
42
+ ctx: IndexContext,
43
+ ) -> IndexResult:
44
+ try:
45
+ source.decode("utf-8")
46
+ except UnicodeDecodeError as exc:
47
+ return IndexResult(
48
+ diagnostics=[
49
+ Diagnostic(
50
+ severity="error",
51
+ file=ctx.relative_path,
52
+ code="RS002",
53
+ message=f"not valid UTF-8: {exc}",
54
+ producer=self.name,
55
+ )
56
+ ]
57
+ )
58
+ parser = tree_sitter.Parser(_RUST_LANG)
59
+ tree = parser.parse(source)
60
+ visitor = _Visitor(ctx.relative_path)
61
+ visitor.visit(tree.root_node)
62
+ diagnostics = list(visitor.diagnostics)
63
+ if tree.root_node.has_error:
64
+ diagnostics.append(
65
+ Diagnostic(
66
+ severity="warning",
67
+ file=ctx.relative_path,
68
+ range=Range(start_line=1, end_line=1),
69
+ code="RS001",
70
+ message="tree-sitter reported parse errors; symbols may be incomplete",
71
+ producer=self.name,
72
+ )
73
+ )
74
+ return IndexResult(
75
+ symbols=visitor.symbols,
76
+ edges=visitor.edges,
77
+ diagnostics=diagnostics,
78
+ )
79
+
80
+
81
+ class _Visitor:
82
+ def __init__(self, relative_path: PurePosixPath) -> None:
83
+ self.relative_path = relative_path
84
+ self.symbols: list[Symbol] = []
85
+ self.edges: list[Edge] = []
86
+ self.diagnostics: list[Diagnostic] = []
87
+ self._type_stack: list[str] = []
88
+
89
+ def visit(self, node: tree_sitter.Node) -> None:
90
+ kind = node.type
91
+ if kind == "function_item":
92
+ self._visit_function_item(node)
93
+ return
94
+ if kind == "function_signature_item" and self._type_stack:
95
+ self._visit_function_signature_item(node)
96
+ return
97
+ if kind == "struct_item":
98
+ self._visit_type_decl(node, rust_kind="struct")
99
+ return
100
+ if kind == "enum_item":
101
+ self._visit_type_decl(node, rust_kind="enum")
102
+ return
103
+ if kind == "trait_item":
104
+ self._visit_trait_item(node)
105
+ return
106
+ if kind == "impl_item":
107
+ self._visit_impl_item(node)
108
+ return
109
+ if kind == "const_item":
110
+ self._visit_const_or_static(node, rust_kind="const")
111
+ return
112
+ if kind == "static_item":
113
+ self._visit_const_or_static(node, rust_kind="static")
114
+ return
115
+ for child in node.children:
116
+ self.visit(child)
117
+
118
+ # ---------------------------------------------------- functions
119
+
120
+ def _visit_function_item(self, node: tree_sitter.Node) -> None:
121
+ name_node = node.child_by_field_name("name")
122
+ if name_node is None:
123
+ return
124
+ name = _node_text(name_node)
125
+ if not name:
126
+ return
127
+ kind: str = "method" if self._type_stack else "function"
128
+ sid = self._make_id(name, kind=DescriptorKind.METHOD)
129
+ self.symbols.append(
130
+ Symbol(
131
+ id=sid,
132
+ kind=kind, # type: ignore[arg-type]
133
+ language=LANG,
134
+ file=self.relative_path,
135
+ range=_node_range(node),
136
+ signature=_fn_signature(node, name),
137
+ )
138
+ )
139
+
140
+ def _visit_function_signature_item(self, node: tree_sitter.Node) -> None:
141
+ name_node = node.child_by_field_name("name")
142
+ if name_node is None:
143
+ return
144
+ name = _node_text(name_node)
145
+ if not name:
146
+ return
147
+ sid = self._make_id(name, kind=DescriptorKind.METHOD)
148
+ self.symbols.append(
149
+ Symbol(
150
+ id=sid,
151
+ kind="method",
152
+ language=LANG,
153
+ file=self.relative_path,
154
+ range=_node_range(node),
155
+ signature=_fn_signature(node, name),
156
+ extra={"rust_kind": "trait_method"},
157
+ )
158
+ )
159
+
160
+ # ------------------------------------------------------ types
161
+
162
+ def _visit_type_decl(self, node: tree_sitter.Node, *, rust_kind: str) -> None:
163
+ name_node = node.child_by_field_name("name")
164
+ if name_node is None:
165
+ return
166
+ name = _node_text(name_node)
167
+ if not name:
168
+ return
169
+ sid = self._make_id(name, kind=DescriptorKind.TYPE)
170
+ self.symbols.append(
171
+ Symbol(
172
+ id=sid,
173
+ kind="class",
174
+ language=LANG,
175
+ file=self.relative_path,
176
+ range=_node_range(node),
177
+ extra={"rust_kind": rust_kind},
178
+ )
179
+ )
180
+
181
+ def _visit_trait_item(self, node: tree_sitter.Node) -> None:
182
+ name_node = node.child_by_field_name("name")
183
+ if name_node is None:
184
+ return
185
+ name = _node_text(name_node)
186
+ if not name:
187
+ return
188
+ sid = self._make_id(name, kind=DescriptorKind.TYPE)
189
+ self.symbols.append(
190
+ Symbol(
191
+ id=sid,
192
+ kind="class",
193
+ language=LANG,
194
+ file=self.relative_path,
195
+ range=_node_range(node),
196
+ extra={"rust_kind": "trait"},
197
+ )
198
+ )
199
+ body = node.child_by_field_name("body")
200
+ if body is None:
201
+ return
202
+ self._type_stack.append(name)
203
+ try:
204
+ for child in body.children:
205
+ self.visit(child)
206
+ finally:
207
+ self._type_stack.pop()
208
+
209
+ def _visit_impl_item(self, node: tree_sitter.Node) -> None:
210
+ owner = _impl_owner_type(node)
211
+ body = node.child_by_field_name("body")
212
+ if owner is None or body is None:
213
+ # Walk anyway to capture function symbols at file scope.
214
+ for child in node.children:
215
+ self.visit(child)
216
+ return
217
+ self._type_stack.append(owner)
218
+ try:
219
+ for child in body.children:
220
+ self.visit(child)
221
+ finally:
222
+ self._type_stack.pop()
223
+
224
+ # ----------------------------------------------- const / static
225
+
226
+ def _visit_const_or_static(self, node: tree_sitter.Node, *, rust_kind: str) -> None:
227
+ name_node = node.child_by_field_name("name")
228
+ if name_node is None:
229
+ return
230
+ name = _node_text(name_node)
231
+ if not name:
232
+ return
233
+ sid = self._make_id(name, kind=DescriptorKind.TERM)
234
+ self.symbols.append(
235
+ Symbol(
236
+ id=sid,
237
+ kind="variable",
238
+ language=LANG,
239
+ file=self.relative_path,
240
+ range=_node_range(node),
241
+ extra={"rust_kind": rust_kind},
242
+ )
243
+ )
244
+
245
+ # ----------------------------------------------------- helpers
246
+
247
+ def _make_id(self, name: str, *, kind: DescriptorKind) -> SymbolID:
248
+ descriptors = list(_path_namespaces(self.relative_path))
249
+ descriptors.extend(Descriptor(name=t, kind=DescriptorKind.TYPE) for t in self._type_stack)
250
+ descriptors.append(Descriptor(name=name, kind=kind))
251
+ return SymbolID(scheme=SCHEME, descriptors=tuple(descriptors))
252
+
253
+
254
+ # ---------------------------------------------------------------------------
255
+ # Pure helpers
256
+ # ---------------------------------------------------------------------------
257
+
258
+
259
+ def _path_namespaces(path: PurePosixPath) -> list[Descriptor]:
260
+ return [Descriptor(name=part, kind=DescriptorKind.NAMESPACE) for part in path.parts]
261
+
262
+
263
+ def _node_range(node: tree_sitter.Node) -> Range:
264
+ sr, sc = node.start_point
265
+ er, ec = node.end_point
266
+ return Range(
267
+ start_line=sr + 1,
268
+ start_col=sc,
269
+ end_line=max(er + 1, sr + 1),
270
+ end_col=ec,
271
+ )
272
+
273
+
274
+ def _node_text(node: tree_sitter.Node) -> str:
275
+ return node.text.decode("utf-8") if node.text is not None else ""
276
+
277
+
278
+ def _fn_signature(node: tree_sitter.Node, name: str) -> str:
279
+ params = node.child_by_field_name("parameters")
280
+ params_text = _node_text(params) if params is not None else "()"
281
+ return_type = node.child_by_field_name("return_type")
282
+ rt_text = (" -> " + _node_text(return_type)) if return_type is not None else ""
283
+ return f"fn {name}{params_text}{rt_text}"
284
+
285
+
286
+ def _impl_owner_type(impl_node: tree_sitter.Node) -> str | None:
287
+ """Return the owning type of an impl block.
288
+
289
+ For ``impl Foo { ... }`` returns ``"Foo"``. For
290
+ ``impl Trait for Foo { ... }`` returns ``"Foo"`` (the receiver, not
291
+ the trait).
292
+ """
293
+ # Field-named child ``type`` is the receiver of the impl (both inherent
294
+ # and trait impls).
295
+ type_node = impl_node.child_by_field_name("type")
296
+ if type_node is not None:
297
+ return _unwrap_type(type_node)
298
+ # Fallback: walk children, take the last `type_identifier` before the
299
+ # `declaration_list`.
300
+ last: tree_sitter.Node | None = None
301
+ for child in impl_node.children:
302
+ if child.type == "declaration_list":
303
+ break
304
+ if child.type in {"type_identifier", "generic_type", "scoped_type_identifier"}:
305
+ last = child
306
+ if last is None:
307
+ return None
308
+ return _unwrap_type(last)
309
+
310
+
311
+ def _unwrap_type(node: tree_sitter.Node) -> str | None:
312
+ if node.type == "type_identifier":
313
+ return _node_text(node)
314
+ if node.type == "generic_type":
315
+ for c in node.children:
316
+ if c.type == "type_identifier":
317
+ return _node_text(c)
318
+ return None
319
+ if node.type == "scoped_type_identifier":
320
+ for c in reversed(node.children):
321
+ if c.type == "type_identifier":
322
+ return _node_text(c)
323
+ return None
324
+ return None
File without changes
@@ -0,0 +1,165 @@
1
+ """Unit tests for the Rust indexer plugin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import textwrap
6
+ from pathlib import Path, PurePosixPath
7
+
8
+ from codemap_rust import RustIndexer
9
+ from codemap_rust.indexer import SCHEME
10
+
11
+ from codemap.core.models import IndexResult
12
+ from codemap.indexers.base import IndexContext
13
+
14
+
15
+ def _index(source: str, *, path: str = "src/lib.rs") -> IndexResult:
16
+ code = textwrap.dedent(source).lstrip("\n")
17
+ return RustIndexer().index_file(
18
+ Path(path),
19
+ code.encode("utf-8"),
20
+ IndexContext(
21
+ project_root=Path("/tmp/proj"),
22
+ relative_path=PurePosixPath(path),
23
+ language="rust",
24
+ ),
25
+ )
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Metadata
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ def test_indexer_metadata() -> None:
34
+ ix = RustIndexer()
35
+ assert ix.name == "rust"
36
+ assert ix.languages == ["rust"]
37
+ assert ix.supports(Path("a.rs"))
38
+ assert not ix.supports(Path("a.go"))
39
+
40
+
41
+ def test_scheme_is_consistent() -> None:
42
+ r = _index(
43
+ """
44
+ fn f() {}
45
+ struct S;
46
+ const C: u32 = 1;
47
+ """
48
+ )
49
+ for s in r.symbols:
50
+ assert str(s.id).startswith(f"{SCHEME} ")
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Function / impl
55
+ # ---------------------------------------------------------------------------
56
+
57
+
58
+ def test_free_function() -> None:
59
+ r = _index("fn helper(x: u32) -> u32 { x + 1 }")
60
+ funcs = [s for s in r.symbols if s.kind == "function"]
61
+ assert len(funcs) == 1
62
+ assert "helper()." in str(funcs[0].id)
63
+
64
+
65
+ def test_inherent_impl_method_attaches_to_type() -> None:
66
+ r = _index(
67
+ """
68
+ struct User;
69
+ impl User {
70
+ fn new() -> Self { User }
71
+ fn login(&mut self) -> bool { false }
72
+ }
73
+ """
74
+ )
75
+ methods = [s for s in r.symbols if s.kind == "method"]
76
+ method_ids = {str(s.id) for s in methods}
77
+ assert any("User#new()." in i for i in method_ids)
78
+ assert any("User#login()." in i for i in method_ids)
79
+
80
+
81
+ def test_trait_impl_method_attaches_to_receiver_type() -> None:
82
+ r = _index(
83
+ """
84
+ trait Greeter { fn hello(&self) -> String; }
85
+ struct User;
86
+ impl Greeter for User {
87
+ fn hello(&self) -> String { String::new() }
88
+ }
89
+ """
90
+ )
91
+ methods = [s for s in r.symbols if s.kind == "method"]
92
+ # User#hello (impl) + Greeter#hello (trait signature) → 2 methods
93
+ ids = {str(s.id) for s in methods}
94
+ assert any("User#hello()." in i for i in ids)
95
+ assert any("Greeter#hello()." in i for i in ids)
96
+
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # Type-level items
100
+ # ---------------------------------------------------------------------------
101
+
102
+
103
+ def test_struct_decl() -> None:
104
+ r = _index("struct User { id: u32 }")
105
+ classes = [s for s in r.symbols if s.kind == "class"]
106
+ assert classes[0].extra.get("rust_kind") == "struct"
107
+
108
+
109
+ def test_enum_decl() -> None:
110
+ r = _index("enum Color { Red, Green, Blue }")
111
+ classes = [s for s in r.symbols if s.kind == "class"]
112
+ assert classes[0].extra.get("rust_kind") == "enum"
113
+
114
+
115
+ def test_trait_decl() -> None:
116
+ r = _index("trait Greeter { fn hello(&self) -> String; }")
117
+ classes = [s for s in r.symbols if s.kind == "class"]
118
+ assert classes[0].extra.get("rust_kind") == "trait"
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Const / static
123
+ # ---------------------------------------------------------------------------
124
+
125
+
126
+ def test_const_decl() -> None:
127
+ r = _index("const MAX: u32 = 10;")
128
+ vars_ = [s for s in r.symbols if s.kind == "variable"]
129
+ assert vars_[0].extra.get("rust_kind") == "const"
130
+
131
+
132
+ def test_static_decl() -> None:
133
+ r = _index("static COUNTER: u32 = 0;")
134
+ vars_ = [s for s in r.symbols if s.kind == "variable"]
135
+ assert vars_[0].extra.get("rust_kind") == "static"
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # SymbolID + edge cases
140
+ # ---------------------------------------------------------------------------
141
+
142
+
143
+ def test_symbol_id_uses_path_namespaces() -> None:
144
+ r = _index("fn helper() {}", path="src/user.rs")
145
+ assert str(r.symbols[0].id) == "scip-rust . . . src/user.rs/helper()."
146
+
147
+
148
+ def test_empty_file_yields_no_symbols() -> None:
149
+ r = _index("")
150
+ assert r.symbols == []
151
+
152
+
153
+ def test_invalid_utf8_yields_diagnostic() -> None:
154
+ ix = RustIndexer()
155
+ r = ix.index_file(
156
+ Path("bad.rs"),
157
+ b"\xff\xfe fn",
158
+ IndexContext(
159
+ project_root=Path("/tmp/proj"),
160
+ relative_path=PurePosixPath("bad.rs"),
161
+ language="rust",
162
+ ),
163
+ )
164
+ assert r.symbols == []
165
+ assert r.diagnostics[0].code == "RS002"