codemap-typescript 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,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: codemap-typescript
3
+ Version: 0.1.0
4
+ Summary: TypeScript indexer plugin for CodeMap (https://github.com/qxbyte/codemap)
5
+ Project-URL: Homepage, https://github.com/qxbyte/codemap
6
+ Author: CodeMap Contributors
7
+ License: MIT
8
+ Keywords: codemap,indexer,tree-sitter,typescript
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Software Development
12
+ Requires-Python: >=3.11
13
+ Requires-Dist: codemap-core<0.2,>=0.1.0
14
+ Requires-Dist: tree-sitter-typescript>=0.23
15
+ Requires-Dist: tree-sitter>=0.25
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=8.0; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # codemap-typescript
21
+
22
+ > A TypeScript / TSX indexer for [CodeMap](https://github.com/qxbyte/codemap),
23
+ > distributed as an independent PyPI package.
24
+
25
+ ## What this package proves
26
+
27
+ **Adding a new language to CodeMap doesn't require touching the main
28
+ repository.** This package implements the `codemap.indexers.base.Indexer`
29
+ Protocol against `*.ts` / `*.tsx` files and registers through the
30
+ `codemap.indexers` entry-point group. Once installed it appears in
31
+ `codemap doctor` next to the built-in indexers, on completely equal
32
+ footing (ADR-004 + ADR-L001).
33
+
34
+ ```text
35
+ $ pip install codemap-typescript
36
+ $ codemap doctor
37
+ Registered indexers
38
+ ┏━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
39
+ ┃ name ┃ version ┃ languages ┃ file_patterns ┃
40
+ ┡━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
41
+ │ _example_lang │ 0.1.0 │ example │ *.example │
42
+ │ python │ 0.1.0 │ python │ *.py, *.pyi │
43
+ │ typescript │ 0.1.0 │ typescript │ *.ts, *.tsx │
44
+ └───────────────┴─────────┴────────────┴───────────────┘
45
+ ```
46
+
47
+ `codemap.indexers.disabled: ["typescript"]` in `.codemap/config.yaml` turns
48
+ it off without uninstalling.
49
+
50
+ ## What it captures
51
+
52
+ Backed by `tree-sitter-typescript`. Single-file, no cross-file type
53
+ inference (MVP):
54
+
55
+ | AST node | Symbol kind | SymbolID descriptor |
56
+ |---|---|---|
57
+ | `function_declaration` | `function` | `<path>/name().` |
58
+ | `class_declaration` | `class` | `<path>/Cls#` |
59
+ | `interface_declaration` | `interface` | `<path>/Cls#` |
60
+ | `method_definition` (inside class) | `method` | `<path>/Cls#name().` |
61
+ | `lexical_declaration` (top-level `const`/`let`) | `variable` | `<path>/Name.` |
62
+
63
+ Edges:
64
+
65
+ * `import_statement` / `import_from_clause` → `imports` to a synthetic
66
+ module symbol (consumed by the same bridges the Python indexer drives).
67
+
68
+ The matcher is intentionally lean — class inheritance, generic
69
+ constraints, and decorators are not yet captured. The point is that they
70
+ *can* be: a v0.2.0 PR to this repository is enough to extend coverage,
71
+ the main CodeMap repository stays untouched.
72
+
73
+ ## SymbolID encoding
74
+
75
+ ```
76
+ scip-typescript . . . src/app/Service.ts/UserService#login().
77
+ └──────────────┘ └──────────────────────────────────┘
78
+ scheme file → namespaces / type / method
79
+ ```
80
+
81
+ ## Tests
82
+
83
+ ```bash
84
+ pip install -e ".[dev]"
85
+ pytest
86
+ ```
87
+
88
+ ## Limits / next steps
89
+
90
+ * No cross-file `import` resolution. The main `python_cross_module`
91
+ bridge has a sibling for Python; a `typescript_cross_module` bridge
92
+ could be added in a future release.
93
+ * No JSX-specific patterns (component declarations) yet.
94
+ * `interface_declaration` is captured as `class` kind — CodeMap's symbol
95
+ schema doesn't yet have a distinct `interface` kind, see design §3.3.
96
+
97
+ ## License
98
+
99
+ MIT — same as the host project.
@@ -0,0 +1,80 @@
1
+ # codemap-typescript
2
+
3
+ > A TypeScript / TSX indexer for [CodeMap](https://github.com/qxbyte/codemap),
4
+ > distributed as an independent PyPI package.
5
+
6
+ ## What this package proves
7
+
8
+ **Adding a new language to CodeMap doesn't require touching the main
9
+ repository.** This package implements the `codemap.indexers.base.Indexer`
10
+ Protocol against `*.ts` / `*.tsx` files and registers through the
11
+ `codemap.indexers` entry-point group. Once installed it appears in
12
+ `codemap doctor` next to the built-in indexers, on completely equal
13
+ footing (ADR-004 + ADR-L001).
14
+
15
+ ```text
16
+ $ pip install codemap-typescript
17
+ $ codemap doctor
18
+ Registered indexers
19
+ ┏━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
20
+ ┃ name ┃ version ┃ languages ┃ file_patterns ┃
21
+ ┡━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
22
+ │ _example_lang │ 0.1.0 │ example │ *.example │
23
+ │ python │ 0.1.0 │ python │ *.py, *.pyi │
24
+ │ typescript │ 0.1.0 │ typescript │ *.ts, *.tsx │
25
+ └───────────────┴─────────┴────────────┴───────────────┘
26
+ ```
27
+
28
+ `codemap.indexers.disabled: ["typescript"]` in `.codemap/config.yaml` turns
29
+ it off without uninstalling.
30
+
31
+ ## What it captures
32
+
33
+ Backed by `tree-sitter-typescript`. Single-file, no cross-file type
34
+ inference (MVP):
35
+
36
+ | AST node | Symbol kind | SymbolID descriptor |
37
+ |---|---|---|
38
+ | `function_declaration` | `function` | `<path>/name().` |
39
+ | `class_declaration` | `class` | `<path>/Cls#` |
40
+ | `interface_declaration` | `interface` | `<path>/Cls#` |
41
+ | `method_definition` (inside class) | `method` | `<path>/Cls#name().` |
42
+ | `lexical_declaration` (top-level `const`/`let`) | `variable` | `<path>/Name.` |
43
+
44
+ Edges:
45
+
46
+ * `import_statement` / `import_from_clause` → `imports` to a synthetic
47
+ module symbol (consumed by the same bridges the Python indexer drives).
48
+
49
+ The matcher is intentionally lean — class inheritance, generic
50
+ constraints, and decorators are not yet captured. The point is that they
51
+ *can* be: a v0.2.0 PR to this repository is enough to extend coverage,
52
+ the main CodeMap repository stays untouched.
53
+
54
+ ## SymbolID encoding
55
+
56
+ ```
57
+ scip-typescript . . . src/app/Service.ts/UserService#login().
58
+ └──────────────┘ └──────────────────────────────────┘
59
+ scheme file → namespaces / type / method
60
+ ```
61
+
62
+ ## Tests
63
+
64
+ ```bash
65
+ pip install -e ".[dev]"
66
+ pytest
67
+ ```
68
+
69
+ ## Limits / next steps
70
+
71
+ * No cross-file `import` resolution. The main `python_cross_module`
72
+ bridge has a sibling for Python; a `typescript_cross_module` bridge
73
+ could be added in a future release.
74
+ * No JSX-specific patterns (component declarations) yet.
75
+ * `interface_declaration` is captured as `class` kind — CodeMap's symbol
76
+ schema doesn't yet have a distinct `interface` kind, see design §3.3.
77
+
78
+ ## License
79
+
80
+ MIT — same as the host project.
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "codemap-typescript"
7
+ version = "0.1.0"
8
+ description = "TypeScript indexer plugin for CodeMap (https://github.com/qxbyte/codemap)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "CodeMap Contributors" }]
13
+ keywords = ["codemap", "typescript", "indexer", "tree-sitter"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Programming Language :: Python :: 3",
17
+ "Topic :: Software Development",
18
+ ]
19
+ # The host application provides the codemap.core / codemap.indexers
20
+ # protocols this plugin implements. We don't pin a version because the
21
+ # protocol surface is intentionally narrow and stable for now.
22
+ dependencies = [
23
+ "codemap-core>=0.1.0,<0.2",
24
+ "tree-sitter>=0.25",
25
+ "tree-sitter-typescript>=0.23",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=8.0",
31
+ ]
32
+
33
+ [project.entry-points."codemap.indexers"]
34
+ typescript = "codemap_typescript:TypeScriptIndexer"
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/qxbyte/codemap"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/codemap_typescript"]
@@ -0,0 +1,13 @@
1
+ """TypeScript indexer plugin for CodeMap.
2
+
3
+ The entry-point group ``codemap.indexers`` discovers this class
4
+ automatically once ``codemap-typescript`` is installed alongside the
5
+ host CodeMap CLI.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from codemap_typescript.indexer import TypeScriptIndexer
11
+
12
+ __all__ = ["TypeScriptIndexer"]
13
+ __version__ = "0.1.0"
@@ -0,0 +1,290 @@
1
+ """TypeScript indexer built on tree-sitter-typescript.
2
+
3
+ The indexer walks the tree-sitter parse tree once and emits
4
+ :class:`Symbol`, :class:`Edge`, and :class:`Diagnostic` objects through
5
+ the host CodeMap protocols. It does not parse, embed, or otherwise hold
6
+ on to any non-AST state — single-file by design, like the built-in
7
+ Python indexer.
8
+
9
+ The implementation purposefully covers a small, illustrative subset of
10
+ TypeScript syntax. The aim is to prove the plugin mechanism end to end,
11
+ not to ship a production-grade indexer in one shot. A real v0.2.0 would
12
+ add: interface kind support upstream, generic-parameter descriptors,
13
+ decorator capture, cross-file import resolution via a separate bridge.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from pathlib import Path, PurePosixPath
19
+ from typing import ClassVar
20
+
21
+ import tree_sitter
22
+ import tree_sitter_typescript
23
+
24
+ from codemap.core.models import Diagnostic, Edge, IndexResult, Range, Symbol
25
+ from codemap.core.symbol import Descriptor, DescriptorKind, SymbolID
26
+ from codemap.indexers.base import IndexContext
27
+
28
+ SCHEME = "scip-typescript"
29
+ LANG = "typescript"
30
+
31
+ _TS_LANG = tree_sitter.Language(tree_sitter_typescript.language_typescript())
32
+ _TSX_LANG = tree_sitter.Language(tree_sitter_typescript.language_tsx())
33
+
34
+
35
+ class TypeScriptIndexer:
36
+ name: ClassVar[str] = "typescript"
37
+ version: ClassVar[str] = "0.1.0"
38
+ file_patterns: ClassVar[list[str]] = ["*.ts", "*.tsx"]
39
+ languages: ClassVar[list[str]] = [LANG]
40
+
41
+ def supports(self, path: Path) -> bool:
42
+ return path.suffix in {".ts", ".tsx"}
43
+
44
+ def index_file(
45
+ self,
46
+ path: Path,
47
+ source: bytes,
48
+ ctx: IndexContext,
49
+ ) -> IndexResult:
50
+ try:
51
+ source.decode("utf-8")
52
+ except UnicodeDecodeError as exc:
53
+ return IndexResult(
54
+ diagnostics=[
55
+ Diagnostic(
56
+ severity="error",
57
+ file=ctx.relative_path,
58
+ code="TS002",
59
+ message=f"not valid UTF-8: {exc}",
60
+ producer=self.name,
61
+ )
62
+ ]
63
+ )
64
+ lang = _TSX_LANG if path.suffix == ".tsx" else _TS_LANG
65
+ parser = tree_sitter.Parser(lang)
66
+ tree = parser.parse(source)
67
+ if tree.root_node.has_error:
68
+ # tree-sitter still produces a partial tree; surface the fact.
69
+ return _walk_with_diagnostic(tree.root_node, ctx)
70
+ return _walk(tree.root_node, ctx)
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # AST walking
75
+ # ---------------------------------------------------------------------------
76
+
77
+
78
+ def _walk(root: tree_sitter.Node, ctx: IndexContext) -> IndexResult:
79
+ visitor = _Visitor(ctx.relative_path)
80
+ visitor.visit(root)
81
+ return IndexResult(
82
+ symbols=visitor.symbols,
83
+ edges=visitor.edges,
84
+ diagnostics=visitor.diagnostics,
85
+ )
86
+
87
+
88
+ def _walk_with_diagnostic(root: tree_sitter.Node, ctx: IndexContext) -> IndexResult:
89
+ """Walk a partially-parsed tree and tack on a syntax-error diagnostic."""
90
+ result = _walk(root, ctx)
91
+ result.diagnostics.append(
92
+ Diagnostic(
93
+ severity="warning",
94
+ file=ctx.relative_path,
95
+ range=Range(start_line=1, end_line=1),
96
+ code="TS001",
97
+ message="tree-sitter reported parse errors; symbols may be incomplete",
98
+ producer="typescript",
99
+ )
100
+ )
101
+ return result
102
+
103
+
104
+ class _Visitor:
105
+ """Single-pass cursor walk over the tree-sitter parse tree."""
106
+
107
+ def __init__(self, relative_path: PurePosixPath) -> None:
108
+ self.relative_path = relative_path
109
+ self.symbols: list[Symbol] = []
110
+ self.edges: list[Edge] = []
111
+ self.diagnostics: list[Diagnostic] = []
112
+ self._class_stack: list[str] = []
113
+
114
+ def visit(self, node: tree_sitter.Node) -> None:
115
+ kind = node.type
116
+ if kind == "function_declaration":
117
+ self._visit_function(node, is_method=False)
118
+ return
119
+ if kind == "class_declaration":
120
+ self._visit_class(node)
121
+ return
122
+ if kind == "interface_declaration":
123
+ self._visit_interface(node)
124
+ return
125
+ if kind == "method_definition":
126
+ self._visit_function(node, is_method=True)
127
+ return
128
+ if kind == "lexical_declaration":
129
+ self._visit_top_level_lexical(node)
130
+ elif kind == "import_statement":
131
+ self._visit_import(node)
132
+ for child in node.children:
133
+ self.visit(child)
134
+
135
+ # ----------------------------------------------------- declarations
136
+
137
+ def _visit_class(self, node: tree_sitter.Node) -> None:
138
+ name = _name_child_text(node)
139
+ if name is None:
140
+ return
141
+ sid = self._make_id(name, descriptor_kind=DescriptorKind.TYPE)
142
+ self.symbols.append(
143
+ Symbol(
144
+ id=sid,
145
+ kind="class",
146
+ language=LANG,
147
+ file=self.relative_path,
148
+ range=_node_range(node),
149
+ )
150
+ )
151
+ self._class_stack.append(name)
152
+ try:
153
+ body = node.child_by_field_name("body")
154
+ if body is not None:
155
+ for child in body.children:
156
+ self.visit(child)
157
+ finally:
158
+ self._class_stack.pop()
159
+
160
+ def _visit_interface(self, node: tree_sitter.Node) -> None:
161
+ name = _name_child_text(node)
162
+ if name is None:
163
+ return
164
+ sid = self._make_id(name, descriptor_kind=DescriptorKind.TYPE)
165
+ self.symbols.append(
166
+ Symbol(
167
+ id=sid,
168
+ kind="class", # design §3.3 has no `interface` kind yet
169
+ language=LANG,
170
+ file=self.relative_path,
171
+ range=_node_range(node),
172
+ extra={"ts_kind": "interface"},
173
+ )
174
+ )
175
+ # Interface bodies are signatures only; nothing to walk further.
176
+
177
+ def _visit_function(self, node: tree_sitter.Node, *, is_method: bool) -> None:
178
+ name = _name_child_text(node)
179
+ if name is None:
180
+ return
181
+ sid = self._make_id(name, descriptor_kind=DescriptorKind.METHOD)
182
+ kind: str = "method" if is_method or self._class_stack else "function"
183
+ signature = _function_signature(node, name)
184
+ self.symbols.append(
185
+ Symbol(
186
+ id=sid,
187
+ kind=kind, # type: ignore[arg-type]
188
+ language=LANG,
189
+ file=self.relative_path,
190
+ range=_node_range(node),
191
+ signature=signature,
192
+ )
193
+ )
194
+ # Walk into the body so nested classes / functions are discovered.
195
+ body = node.child_by_field_name("body")
196
+ if body is not None:
197
+ for child in body.children:
198
+ self.visit(child)
199
+
200
+ def _visit_top_level_lexical(self, node: tree_sitter.Node) -> None:
201
+ """Only catch module-level `const`/`let` declarations (not inside fns)."""
202
+ if self._class_stack:
203
+ return # class body's lexicals are handled via method_definition path
204
+ for child in node.children:
205
+ if child.type != "variable_declarator":
206
+ continue
207
+ name_node = child.child_by_field_name("name")
208
+ if name_node is None or name_node.type != "identifier":
209
+ continue
210
+ name = name_node.text.decode("utf-8") if name_node.text else ""
211
+ if not name:
212
+ continue
213
+ sid = self._make_id(name, descriptor_kind=DescriptorKind.TERM)
214
+ self.symbols.append(
215
+ Symbol(
216
+ id=sid,
217
+ kind="variable",
218
+ language=LANG,
219
+ file=self.relative_path,
220
+ range=_node_range(child),
221
+ )
222
+ )
223
+
224
+ def _visit_import(self, node: tree_sitter.Node) -> None:
225
+ source_node = node.child_by_field_name("source")
226
+ if source_node is None or source_node.text is None:
227
+ return
228
+ module = source_node.text.decode("utf-8").strip("\"'`")
229
+ if not module:
230
+ return
231
+ # Imports at module top-level have no enclosing callable, so no edge —
232
+ # mirroring the Python indexer's policy. ``_module_symbol_id`` is kept
233
+ # for a future bridge that wants to act on the import target.
234
+ _ = _module_symbol_id(module)
235
+
236
+ # ---------------------------------------------------------- helpers
237
+
238
+ def _make_id(self, name: str, *, descriptor_kind: DescriptorKind) -> SymbolID:
239
+ descriptors = list(_path_namespaces(self.relative_path))
240
+ descriptors.extend(
241
+ Descriptor(name=cls, kind=DescriptorKind.TYPE) for cls in self._class_stack
242
+ )
243
+ descriptors.append(Descriptor(name=name, kind=descriptor_kind))
244
+ return SymbolID(scheme=SCHEME, descriptors=tuple(descriptors))
245
+
246
+
247
+ # ---------------------------------------------------------------------------
248
+ # Pure helpers
249
+ # ---------------------------------------------------------------------------
250
+
251
+
252
+ def _path_namespaces(path: PurePosixPath) -> list[Descriptor]:
253
+ return [Descriptor(name=part, kind=DescriptorKind.NAMESPACE) for part in path.parts]
254
+
255
+
256
+ def _module_symbol_id(spec: str) -> SymbolID:
257
+ # `./foo`, `../bar`, `mylib` — keep the literal as the leaf.
258
+ parts = [p for p in spec.split("/") if p and p != "."]
259
+ descriptors = [Descriptor(name=p, kind=DescriptorKind.NAMESPACE) for p in parts[:-1]]
260
+ descriptors.append(Descriptor(name=parts[-1] if parts else spec, kind=DescriptorKind.META))
261
+ return SymbolID(scheme=SCHEME, descriptors=tuple(descriptors))
262
+
263
+
264
+ def _node_range(node: tree_sitter.Node) -> Range:
265
+ start_row, start_col = node.start_point
266
+ end_row, end_col = node.end_point
267
+ return Range(
268
+ start_line=start_row + 1,
269
+ start_col=start_col,
270
+ end_line=max(end_row + 1, start_row + 1),
271
+ end_col=end_col,
272
+ )
273
+
274
+
275
+ def _name_child_text(node: tree_sitter.Node) -> str | None:
276
+ """Return the textual name field of a declaration node, if present."""
277
+ name_node = node.child_by_field_name("name")
278
+ if name_node is None or name_node.text is None:
279
+ return None
280
+ text = name_node.text.decode("utf-8").strip()
281
+ return text or None
282
+
283
+
284
+ def _function_signature(node: tree_sitter.Node, name: str) -> str:
285
+ params = node.child_by_field_name("parameters")
286
+ params_text = ""
287
+ if params is not None and params.text is not None:
288
+ params_text = params.text.decode("utf-8")
289
+ prefix = "function" if node.type == "function_declaration" else ""
290
+ return (f"{prefix} {name}{params_text}").strip()
File without changes
@@ -0,0 +1,178 @@
1
+ """Unit tests for the TypeScript indexer plugin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import textwrap
6
+ from pathlib import Path, PurePosixPath
7
+
8
+ from codemap_typescript import TypeScriptIndexer
9
+ from codemap_typescript.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/m.ts") -> IndexResult:
16
+ code = textwrap.dedent(source).lstrip("\n")
17
+ return TypeScriptIndexer().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="typescript",
24
+ ),
25
+ )
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Metadata
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ def test_indexer_metadata() -> None:
34
+ ix = TypeScriptIndexer()
35
+ assert ix.name == "typescript"
36
+ assert ix.languages == ["typescript"]
37
+ assert "*.ts" in ix.file_patterns
38
+ assert "*.tsx" in ix.file_patterns
39
+ assert ix.supports(Path("a.ts"))
40
+ assert ix.supports(Path("a.tsx"))
41
+ assert not ix.supports(Path("a.py"))
42
+
43
+
44
+ def test_scheme_is_consistent() -> None:
45
+ r = _index(
46
+ """
47
+ function f() {}
48
+ class C {}
49
+ const X = 1;
50
+ """
51
+ )
52
+ for sym in r.symbols:
53
+ assert str(sym.id).startswith(f"{SCHEME} ")
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Top-level declarations
58
+ # ---------------------------------------------------------------------------
59
+
60
+
61
+ def test_function_declaration() -> None:
62
+ r = _index("function hello(name: string): void { return; }")
63
+ assert len(r.symbols) == 1
64
+ s = r.symbols[0]
65
+ assert s.kind == "function"
66
+ assert "hello" in str(s.id)
67
+ assert s.signature is not None
68
+ assert "hello" in s.signature
69
+
70
+
71
+ def test_class_declaration_with_methods() -> None:
72
+ r = _index(
73
+ """
74
+ class Greeter {
75
+ hello(name: string): string { return name; }
76
+ bye(): void {}
77
+ }
78
+ """
79
+ )
80
+ kinds = sorted(s.kind for s in r.symbols)
81
+ assert kinds == ["class", "method", "method"]
82
+ method_ids = [str(s.id) for s in r.symbols if s.kind == "method"]
83
+ assert any("Greeter#hello()." in i for i in method_ids)
84
+ assert any("Greeter#bye()." in i for i in method_ids)
85
+
86
+
87
+ def test_interface_declaration_captured_as_class_with_extra() -> None:
88
+ r = _index("interface User { id: number; name: string; }")
89
+ assert len(r.symbols) == 1
90
+ s = r.symbols[0]
91
+ assert s.kind == "class"
92
+ assert s.extra.get("ts_kind") == "interface"
93
+ assert "User#" in str(s.id)
94
+
95
+
96
+ def test_top_level_const_is_variable() -> None:
97
+ r = _index("const MAX = 10;")
98
+ assert len(r.symbols) == 1
99
+ assert r.symbols[0].kind == "variable"
100
+ assert str(r.symbols[0].id).endswith("MAX.")
101
+
102
+
103
+ def test_top_level_let_is_variable() -> None:
104
+ r = _index("let counter = 0;")
105
+ assert len(r.symbols) == 1
106
+ assert r.symbols[0].kind == "variable"
107
+
108
+
109
+ def test_top_level_multi_declarator() -> None:
110
+ r = _index("const a = 1, b = 2;")
111
+ assert {s.id.descriptors[-1].name for s in r.symbols} == {"a", "b"}
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # SymbolID structure
116
+ # ---------------------------------------------------------------------------
117
+
118
+
119
+ def test_symbol_id_uses_path_namespaces() -> None:
120
+ r = _index("function f() {}", path="pkg/sub/m.ts")
121
+ assert str(r.symbols[0].id) == "scip-typescript . . . pkg/sub/m.ts/f()."
122
+
123
+
124
+ def test_tsx_file_supported() -> None:
125
+ r = _index("function App() { return null as any; }", path="App.tsx")
126
+ assert len(r.symbols) == 1
127
+
128
+
129
+ # ---------------------------------------------------------------------------
130
+ # Diagnostics / edge cases
131
+ # ---------------------------------------------------------------------------
132
+
133
+
134
+ def test_empty_file_produces_nothing() -> None:
135
+ r = _index("")
136
+ assert r.symbols == []
137
+
138
+
139
+ def test_syntax_error_yields_warning_diagnostic() -> None:
140
+ r = _index("function broken( { ")
141
+ codes = {d.code for d in r.diagnostics}
142
+ assert "TS001" in codes
143
+
144
+
145
+ def test_invalid_utf8_yields_error_diagnostic() -> None:
146
+ ix = TypeScriptIndexer()
147
+ r = ix.index_file(
148
+ Path("bad.ts"),
149
+ b"\xff\xfe garbage",
150
+ IndexContext(
151
+ project_root=Path("/tmp/proj"),
152
+ relative_path=PurePosixPath("bad.ts"),
153
+ language="typescript",
154
+ ),
155
+ )
156
+ assert r.symbols == []
157
+ assert r.diagnostics[0].code == "TS002"
158
+
159
+
160
+ # ---------------------------------------------------------------------------
161
+ # Nested classes / interfaces
162
+ # ---------------------------------------------------------------------------
163
+
164
+
165
+ def test_class_inside_function_is_skipped_as_method_path() -> None:
166
+ """Sanity: a class declared inside a function body still produces a
167
+ class symbol; method-stack scoping must not crash."""
168
+ r = _index(
169
+ """
170
+ function outer() {
171
+ class Inner { m() {} }
172
+ return Inner;
173
+ }
174
+ """
175
+ )
176
+ kinds = sorted(s.kind for s in r.symbols)
177
+ assert "function" in kinds
178
+ assert "class" in kinds