codemap-javascript 0.2.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
+ """JavaScript / JSX indexer plugin for CodeMap.
2
+
3
+ The entry-point group ``codemap.indexers`` discovers this class
4
+ automatically once ``codemap-javascript`` is installed alongside the
5
+ host CodeMap CLI.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from codemap_javascript.indexer import JavaScriptIndexer
11
+
12
+ __all__ = ["JavaScriptIndexer"]
13
+ __version__ = "0.2.0a1"
@@ -0,0 +1,260 @@
1
+ """JavaScript / JSX indexer built on tree-sitter-javascript.
2
+
3
+ Covers ``.js`` / ``.jsx`` / ``.mjs`` / ``.cjs``. The grammar handles JSX
4
+ natively (no separate TSX-style language object needed) so one parser
5
+ instance covers every file extension.
6
+
7
+ Symbol coverage mirrors ``codemap-typescript`` for cross-language
8
+ consistency: top-level functions, classes (with methods),
9
+ variable declarations, and import statements. Anything declared inside
10
+ a function body is treated as private state and not surfaced.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path, PurePosixPath
16
+ from typing import ClassVar
17
+
18
+ import tree_sitter
19
+ import tree_sitter_javascript
20
+
21
+ from codemap.core.models import Diagnostic, IndexResult, Range, Symbol
22
+ from codemap.core.symbol import Descriptor, DescriptorKind, SymbolID
23
+ from codemap.indexers.base import IndexContext
24
+
25
+ SCHEME = "scip-javascript"
26
+ LANG = "javascript"
27
+
28
+ _JS_LANG = tree_sitter.Language(tree_sitter_javascript.language())
29
+
30
+
31
+ class JavaScriptIndexer:
32
+ name: ClassVar[str] = "javascript"
33
+ version: ClassVar[str] = "0.2.0"
34
+ file_patterns: ClassVar[list[str]] = ["*.js", "*.jsx", "*.mjs", "*.cjs"]
35
+ languages: ClassVar[list[str]] = [LANG]
36
+
37
+ def supports(self, path: Path) -> bool:
38
+ return path.suffix in {".js", ".jsx", ".mjs", ".cjs"}
39
+
40
+ def index_file(
41
+ self,
42
+ path: Path,
43
+ source: bytes,
44
+ ctx: IndexContext,
45
+ ) -> IndexResult:
46
+ try:
47
+ source.decode("utf-8")
48
+ except UnicodeDecodeError as exc:
49
+ return IndexResult(
50
+ diagnostics=[
51
+ Diagnostic(
52
+ severity="error",
53
+ file=ctx.relative_path,
54
+ code="JS002",
55
+ message=f"not valid UTF-8: {exc}",
56
+ producer=self.name,
57
+ )
58
+ ]
59
+ )
60
+ parser = tree_sitter.Parser(_JS_LANG)
61
+ tree = parser.parse(source)
62
+ if tree.root_node.has_error:
63
+ return _walk_with_diagnostic(tree.root_node, ctx)
64
+ return _walk(tree.root_node, ctx)
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # AST walking
69
+ # ---------------------------------------------------------------------------
70
+
71
+
72
+ def _walk(root: tree_sitter.Node, ctx: IndexContext) -> IndexResult:
73
+ visitor = _Visitor(ctx.relative_path)
74
+ visitor.visit(root)
75
+ return IndexResult(
76
+ symbols=visitor.symbols,
77
+ edges=visitor.edges,
78
+ diagnostics=visitor.diagnostics,
79
+ )
80
+
81
+
82
+ def _walk_with_diagnostic(root: tree_sitter.Node, ctx: IndexContext) -> IndexResult:
83
+ """Walk a partially-parsed tree and tack on a syntax-error diagnostic."""
84
+ result = _walk(root, ctx)
85
+ result.diagnostics.append(
86
+ Diagnostic(
87
+ severity="warning",
88
+ file=ctx.relative_path,
89
+ range=Range(start_line=1, end_line=1),
90
+ code="JS001",
91
+ message="tree-sitter reported parse errors; symbols may be incomplete",
92
+ producer=LANG,
93
+ )
94
+ )
95
+ return result
96
+
97
+
98
+ class _Visitor:
99
+ """Single-pass cursor walk over the tree-sitter parse tree."""
100
+
101
+ def __init__(self, relative_path: PurePosixPath) -> None:
102
+ self.relative_path = relative_path
103
+ self.symbols: list[Symbol] = []
104
+ self.edges: list = [] # left empty for parity with typescript
105
+ self.diagnostics: list[Diagnostic] = []
106
+ self._class_stack: list[str] = []
107
+
108
+ def visit(self, node: tree_sitter.Node) -> None:
109
+ kind = node.type
110
+ if kind == "function_declaration":
111
+ self._visit_function(node, is_method=False)
112
+ return
113
+ if kind == "class_declaration":
114
+ self._visit_class(node)
115
+ return
116
+ if kind == "method_definition":
117
+ self._visit_function(node, is_method=True)
118
+ return
119
+ if kind in {"lexical_declaration", "variable_declaration"}:
120
+ self._visit_top_level_declaration(node)
121
+ elif kind == "import_statement":
122
+ self._visit_import(node)
123
+ for child in node.children:
124
+ self.visit(child)
125
+
126
+ # ----------------------------------------------------- declarations
127
+
128
+ def _visit_class(self, node: tree_sitter.Node) -> None:
129
+ name = _name_child_text(node)
130
+ if name is None:
131
+ return
132
+ sid = self._make_id(name, descriptor_kind=DescriptorKind.TYPE)
133
+ self.symbols.append(
134
+ Symbol(
135
+ id=sid,
136
+ kind="class",
137
+ language=LANG,
138
+ file=self.relative_path,
139
+ range=_node_range(node),
140
+ )
141
+ )
142
+ self._class_stack.append(name)
143
+ try:
144
+ body = node.child_by_field_name("body")
145
+ if body is not None:
146
+ for child in body.children:
147
+ self.visit(child)
148
+ finally:
149
+ self._class_stack.pop()
150
+
151
+ def _visit_function(self, node: tree_sitter.Node, *, is_method: bool) -> None:
152
+ name = _name_child_text(node)
153
+ if name is None:
154
+ return
155
+ sid = self._make_id(name, descriptor_kind=DescriptorKind.METHOD)
156
+ kind: str = "method" if is_method or self._class_stack else "function"
157
+ signature = _function_signature(node, name)
158
+ self.symbols.append(
159
+ Symbol(
160
+ id=sid,
161
+ kind=kind, # type: ignore[arg-type]
162
+ language=LANG,
163
+ file=self.relative_path,
164
+ range=_node_range(node),
165
+ signature=signature,
166
+ )
167
+ )
168
+ body = node.child_by_field_name("body")
169
+ if body is not None:
170
+ for child in body.children:
171
+ self.visit(child)
172
+
173
+ def _visit_top_level_declaration(self, node: tree_sitter.Node) -> None:
174
+ """Catch module-level ``const`` / ``let`` / ``var`` declarations."""
175
+ if self._class_stack:
176
+ return
177
+ for child in node.children:
178
+ if child.type != "variable_declarator":
179
+ continue
180
+ name_node = child.child_by_field_name("name")
181
+ if name_node is None or name_node.type != "identifier":
182
+ continue
183
+ name = name_node.text.decode("utf-8") if name_node.text else ""
184
+ if not name:
185
+ continue
186
+ sid = self._make_id(name, descriptor_kind=DescriptorKind.TERM)
187
+ self.symbols.append(
188
+ Symbol(
189
+ id=sid,
190
+ kind="variable",
191
+ language=LANG,
192
+ file=self.relative_path,
193
+ range=_node_range(child),
194
+ )
195
+ )
196
+
197
+ def _visit_import(self, node: tree_sitter.Node) -> None:
198
+ # Imports are recorded for future cross-module bridge consumption;
199
+ # no edge is emitted at the module top-level (parity with typescript).
200
+ source_node = node.child_by_field_name("source")
201
+ if source_node is None or source_node.text is None:
202
+ return
203
+ module = source_node.text.decode("utf-8").strip("\"'`")
204
+ if not module:
205
+ return
206
+ _ = _module_symbol_id(module)
207
+
208
+ # ---------------------------------------------------------- helpers
209
+
210
+ def _make_id(self, name: str, *, descriptor_kind: DescriptorKind) -> SymbolID:
211
+ descriptors = list(_path_namespaces(self.relative_path))
212
+ descriptors.extend(
213
+ Descriptor(name=cls, kind=DescriptorKind.TYPE) for cls in self._class_stack
214
+ )
215
+ descriptors.append(Descriptor(name=name, kind=descriptor_kind))
216
+ return SymbolID(scheme=SCHEME, descriptors=tuple(descriptors))
217
+
218
+
219
+ # ---------------------------------------------------------------------------
220
+ # Pure helpers
221
+ # ---------------------------------------------------------------------------
222
+
223
+
224
+ def _path_namespaces(path: PurePosixPath) -> list[Descriptor]:
225
+ return [Descriptor(name=part, kind=DescriptorKind.NAMESPACE) for part in path.parts]
226
+
227
+
228
+ def _module_symbol_id(spec: str) -> SymbolID:
229
+ parts = [p for p in spec.split("/") if p and p != "."]
230
+ descriptors = [Descriptor(name=p, kind=DescriptorKind.NAMESPACE) for p in parts[:-1]]
231
+ descriptors.append(Descriptor(name=parts[-1] if parts else spec, kind=DescriptorKind.META))
232
+ return SymbolID(scheme=SCHEME, descriptors=tuple(descriptors))
233
+
234
+
235
+ def _node_range(node: tree_sitter.Node) -> Range:
236
+ start_row, start_col = node.start_point
237
+ end_row, end_col = node.end_point
238
+ return Range(
239
+ start_line=start_row + 1,
240
+ start_col=start_col,
241
+ end_line=max(end_row + 1, start_row + 1),
242
+ end_col=end_col,
243
+ )
244
+
245
+
246
+ def _name_child_text(node: tree_sitter.Node) -> str | None:
247
+ name_node = node.child_by_field_name("name")
248
+ if name_node is None or name_node.text is None:
249
+ return None
250
+ text = name_node.text.decode("utf-8").strip()
251
+ return text or None
252
+
253
+
254
+ def _function_signature(node: tree_sitter.Node, name: str) -> str:
255
+ params = node.child_by_field_name("parameters")
256
+ params_text = ""
257
+ if params is not None and params.text is not None:
258
+ params_text = params.text.decode("utf-8")
259
+ prefix = "function" if node.type == "function_declaration" else ""
260
+ return (f"{prefix} {name}{params_text}").strip()
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: codemap-javascript
3
+ Version: 0.2.0
4
+ Summary: JavaScript / JSX 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,javascript,jsx,tree-sitter
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Programming Language :: JavaScript
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: codemap-core<0.3,>=0.2.0
15
+ Requires-Dist: tree-sitter-javascript>=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-javascript
22
+
23
+ > A JavaScript / JSX indexer for [CodeMap](https://github.com/qxbyte/codemap),
24
+ > distributed as an independent PyPI package.
25
+
26
+ ## What this package covers
27
+
28
+ | Extension | Grammar | Notes |
29
+ |---|---|---|
30
+ | `.js` | `tree-sitter-javascript` | Plain JavaScript |
31
+ | `.jsx` | `tree-sitter-javascript` | React-style JSX (the grammar handles JSX natively) |
32
+ | `.mjs` | `tree-sitter-javascript` | ES module |
33
+ | `.cjs` | `tree-sitter-javascript` | CommonJS module |
34
+
35
+ A single grammar instance handles all four extensions — `.jsx` is **not**
36
+ a separate grammar in tree-sitter-javascript.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install codemap-javascript
42
+ ```
43
+
44
+ After installation, `codemap doctor` lists `javascript` alongside the
45
+ built-in indexers (ADR-004 + ADR-L001).
46
+
47
+ ## What it captures
48
+
49
+ Backed by `tree-sitter-javascript`. Single-file, no cross-file resolution:
50
+
51
+ | AST node | Symbol kind | SymbolID descriptor |
52
+ |---|---|---|
53
+ | `function_declaration` | `function` | `<path>/name().` |
54
+ | `class_declaration` | `class` | `<path>/Cls#` |
55
+ | `method_definition` (inside class) | `method` | `<path>/Cls#name().` |
56
+ | Top-level `const` / `let` / `var` | `variable` | `<path>/Name.` |
57
+
58
+ Edges:
59
+
60
+ * `import_statement` is read for future cross-module bridge consumption,
61
+ but no edge is emitted at the module top-level (parity with the
62
+ TypeScript plugin).
63
+
64
+ ## SymbolID encoding
65
+
66
+ ```
67
+ scip-javascript . . . src/app/Service.js/UserService#login().
68
+ └─────────────┘ └──────────────────────────────────────┘
69
+ scheme file → namespaces / type / method
70
+ ```
71
+
72
+ ## Why a separate plugin (vs. extending `codemap-typescript`)
73
+
74
+ TypeScript is a syntactic superset of JavaScript, so `tree-sitter-typescript`
75
+ *can* parse most `.js` / `.jsx` files. We ship a dedicated plugin instead
76
+ for three reasons:
77
+
78
+ 1. **Clean dependency**: `codemap-javascript` does not depend on
79
+ `tree-sitter-typescript`. Users who only have JavaScript code do not
80
+ need the larger grammar.
81
+ 2. **JS-specific error recovery**: when a `.js` file has a syntax error,
82
+ the JavaScript grammar's error nodes point at JS-shaped tokens rather
83
+ than TS-shaped ones, which produces tighter diagnostics.
84
+ 3. **Independent versioning**: JS language updates (decorators, class
85
+ fields, RegExp `v` flag) can ship at their own cadence without
86
+ bumping the TypeScript plugin.
87
+
88
+ For pure type-extraction purposes the two plugins produce equivalent
89
+ symbol output for `.js` / `.jsx` files. Install whichever matches your
90
+ codebase; install both if you have a mixed code-base.
91
+
92
+ ## Tests
93
+
94
+ ```bash
95
+ pip install -e ".[dev]"
96
+ pytest
97
+ ```
98
+
99
+ ## Limits / next steps
100
+
101
+ * No cross-file `import` resolution. A `javascript_cross_module` bridge,
102
+ sibling to `python_cross_module`, would be a future addition.
103
+ * Arrow functions assigned to top-level `const` are indexed as
104
+ `variable`, not `function` — matching the TypeScript plugin. Named
105
+ arrow detection across both plugins is a v0.2.x improvement.
106
+ * JSX-specific component patterns are not yet captured beyond the
107
+ underlying function/class symbols.
108
+
109
+ ## License
110
+
111
+ MIT — same as the host project.
@@ -0,0 +1,6 @@
1
+ codemap_javascript/__init__.py,sha256=EyT4H26OkSUhrV4tTnFQwKgvrTHNzj9Wyu1CsuEd8vs,355
2
+ codemap_javascript/indexer.py,sha256=R1hELiC31BT9_HeG706br1XMm8nXhJZ8x4oUWlFRYGg,9322
3
+ codemap_javascript-0.2.0.dist-info/METADATA,sha256=cWrcSZsokXDkMtijeSSmgK98t9tRvkVg7dVeiPSy7ec,3936
4
+ codemap_javascript-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
5
+ codemap_javascript-0.2.0.dist-info/entry_points.txt,sha256=L-dFNOekeZk2uim4fCdI3NrAqsszJqT-ul0sB19nCzE,69
6
+ codemap_javascript-0.2.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
+ javascript = codemap_javascript:JavaScriptIndexer