codemap-typescript 0.1.0a1__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.0a1
|
|
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.0a1
|
|
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.0a1.dist-info/METADATA,sha256=UGe3JnJyAKF0udGsAR2tyFwZrtEg2a7Zib_Snf5Cji4,3915
|
|
4
|
+
codemap_typescript-0.1.0a1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
5
|
+
codemap_typescript-0.1.0a1.dist-info/entry_points.txt,sha256=ua1mkwxZg72mYbV5R703-2DABcdd1A5Y21qnpcUbYxg,69
|
|
6
|
+
codemap_typescript-0.1.0a1.dist-info/RECORD,,
|