codemap-typescript 0.1.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.
@@ -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()
@@ -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,6 @@
1
+ codemap_typescript/__init__.py,sha256=MiaaJcSY--LNtje-vvOeoU1bH6D5ESOgUGrnsz3omFc,347
2
+ codemap_typescript/indexer.py,sha256=tmqud34XOB3UAE1oFpIHYcALbiSjX0W4nXjpqcbu7BY,10740
3
+ codemap_typescript-0.1.0.dist-info/METADATA,sha256=JCbjRZp1YEIeXuuhkeW1vMhTITsHI7U2BngOb3tIv-4,3911
4
+ codemap_typescript-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
5
+ codemap_typescript-0.1.0.dist-info/entry_points.txt,sha256=ua1mkwxZg72mYbV5R703-2DABcdd1A5Y21qnpcUbYxg,69
6
+ codemap_typescript-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [codemap.indexers]
2
+ typescript = codemap_typescript:TypeScriptIndexer