codemap-javascript 0.2.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.
- codemap_javascript-0.2.0/.gitignore +43 -0
- codemap_javascript-0.2.0/PKG-INFO +111 -0
- codemap_javascript-0.2.0/README.md +91 -0
- codemap_javascript-0.2.0/pyproject.toml +36 -0
- codemap_javascript-0.2.0/src/codemap_javascript/__init__.py +13 -0
- codemap_javascript-0.2.0/src/codemap_javascript/indexer.py +260 -0
- codemap_javascript-0.2.0/tests/__init__.py +0 -0
- codemap_javascript-0.2.0/tests/test_indexer.py +223 -0
|
@@ -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,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,91 @@
|
|
|
1
|
+
# codemap-javascript
|
|
2
|
+
|
|
3
|
+
> A JavaScript / JSX indexer for [CodeMap](https://github.com/qxbyte/codemap),
|
|
4
|
+
> distributed as an independent PyPI package.
|
|
5
|
+
|
|
6
|
+
## What this package covers
|
|
7
|
+
|
|
8
|
+
| Extension | Grammar | Notes |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| `.js` | `tree-sitter-javascript` | Plain JavaScript |
|
|
11
|
+
| `.jsx` | `tree-sitter-javascript` | React-style JSX (the grammar handles JSX natively) |
|
|
12
|
+
| `.mjs` | `tree-sitter-javascript` | ES module |
|
|
13
|
+
| `.cjs` | `tree-sitter-javascript` | CommonJS module |
|
|
14
|
+
|
|
15
|
+
A single grammar instance handles all four extensions — `.jsx` is **not**
|
|
16
|
+
a separate grammar in tree-sitter-javascript.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install codemap-javascript
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
After installation, `codemap doctor` lists `javascript` alongside the
|
|
25
|
+
built-in indexers (ADR-004 + ADR-L001).
|
|
26
|
+
|
|
27
|
+
## What it captures
|
|
28
|
+
|
|
29
|
+
Backed by `tree-sitter-javascript`. Single-file, no cross-file resolution:
|
|
30
|
+
|
|
31
|
+
| AST node | Symbol kind | SymbolID descriptor |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `function_declaration` | `function` | `<path>/name().` |
|
|
34
|
+
| `class_declaration` | `class` | `<path>/Cls#` |
|
|
35
|
+
| `method_definition` (inside class) | `method` | `<path>/Cls#name().` |
|
|
36
|
+
| Top-level `const` / `let` / `var` | `variable` | `<path>/Name.` |
|
|
37
|
+
|
|
38
|
+
Edges:
|
|
39
|
+
|
|
40
|
+
* `import_statement` is read for future cross-module bridge consumption,
|
|
41
|
+
but no edge is emitted at the module top-level (parity with the
|
|
42
|
+
TypeScript plugin).
|
|
43
|
+
|
|
44
|
+
## SymbolID encoding
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
scip-javascript . . . src/app/Service.js/UserService#login().
|
|
48
|
+
└─────────────┘ └──────────────────────────────────────┘
|
|
49
|
+
scheme file → namespaces / type / method
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Why a separate plugin (vs. extending `codemap-typescript`)
|
|
53
|
+
|
|
54
|
+
TypeScript is a syntactic superset of JavaScript, so `tree-sitter-typescript`
|
|
55
|
+
*can* parse most `.js` / `.jsx` files. We ship a dedicated plugin instead
|
|
56
|
+
for three reasons:
|
|
57
|
+
|
|
58
|
+
1. **Clean dependency**: `codemap-javascript` does not depend on
|
|
59
|
+
`tree-sitter-typescript`. Users who only have JavaScript code do not
|
|
60
|
+
need the larger grammar.
|
|
61
|
+
2. **JS-specific error recovery**: when a `.js` file has a syntax error,
|
|
62
|
+
the JavaScript grammar's error nodes point at JS-shaped tokens rather
|
|
63
|
+
than TS-shaped ones, which produces tighter diagnostics.
|
|
64
|
+
3. **Independent versioning**: JS language updates (decorators, class
|
|
65
|
+
fields, RegExp `v` flag) can ship at their own cadence without
|
|
66
|
+
bumping the TypeScript plugin.
|
|
67
|
+
|
|
68
|
+
For pure type-extraction purposes the two plugins produce equivalent
|
|
69
|
+
symbol output for `.js` / `.jsx` files. Install whichever matches your
|
|
70
|
+
codebase; install both if you have a mixed code-base.
|
|
71
|
+
|
|
72
|
+
## Tests
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install -e ".[dev]"
|
|
76
|
+
pytest
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Limits / next steps
|
|
80
|
+
|
|
81
|
+
* No cross-file `import` resolution. A `javascript_cross_module` bridge,
|
|
82
|
+
sibling to `python_cross_module`, would be a future addition.
|
|
83
|
+
* Arrow functions assigned to top-level `const` are indexed as
|
|
84
|
+
`variable`, not `function` — matching the TypeScript plugin. Named
|
|
85
|
+
arrow detection across both plugins is a v0.2.x improvement.
|
|
86
|
+
* JSX-specific component patterns are not yet captured beyond the
|
|
87
|
+
underlying function/class symbols.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT — same as the host project.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "codemap-javascript"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "JavaScript / JSX 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", "javascript", "jsx", "indexer", "tree-sitter"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Programming Language :: JavaScript",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Software Development",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"codemap-core>=0.2.0,<0.3",
|
|
22
|
+
"tree-sitter>=0.25",
|
|
23
|
+
"tree-sitter-javascript>=0.23",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = ["pytest>=8.0"]
|
|
28
|
+
|
|
29
|
+
[project.entry-points."codemap.indexers"]
|
|
30
|
+
javascript = "codemap_javascript:JavaScriptIndexer"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/qxbyte/codemap"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/codemap_javascript"]
|
|
@@ -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()
|
|
File without changes
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Unit tests for the JavaScript indexer plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import textwrap
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
|
|
8
|
+
from codemap_javascript import JavaScriptIndexer
|
|
9
|
+
from codemap_javascript.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.js") -> IndexResult:
|
|
16
|
+
code = textwrap.dedent(source).lstrip("\n")
|
|
17
|
+
return JavaScriptIndexer().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="javascript",
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Metadata
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_indexer_metadata() -> None:
|
|
34
|
+
ix = JavaScriptIndexer()
|
|
35
|
+
assert ix.name == "javascript"
|
|
36
|
+
assert ix.languages == ["javascript"]
|
|
37
|
+
assert set(ix.file_patterns) == {"*.js", "*.jsx", "*.mjs", "*.cjs"}
|
|
38
|
+
assert ix.supports(Path("a.js"))
|
|
39
|
+
assert ix.supports(Path("a.jsx"))
|
|
40
|
+
assert ix.supports(Path("a.mjs"))
|
|
41
|
+
assert ix.supports(Path("a.cjs"))
|
|
42
|
+
assert not ix.supports(Path("a.ts"))
|
|
43
|
+
assert not ix.supports(Path("a.py"))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_scheme_is_consistent() -> None:
|
|
47
|
+
r = _index(
|
|
48
|
+
"""
|
|
49
|
+
function f() {}
|
|
50
|
+
class C {}
|
|
51
|
+
const X = 1;
|
|
52
|
+
"""
|
|
53
|
+
)
|
|
54
|
+
for sym in r.symbols:
|
|
55
|
+
assert str(sym.id).startswith(f"{SCHEME} ")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# Top-level declarations
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_function_declaration() -> None:
|
|
64
|
+
r = _index("function hello(name) { return name; }")
|
|
65
|
+
assert len(r.symbols) == 1
|
|
66
|
+
s = r.symbols[0]
|
|
67
|
+
assert s.kind == "function"
|
|
68
|
+
assert "hello" in str(s.id)
|
|
69
|
+
assert s.signature is not None
|
|
70
|
+
assert "hello" in s.signature
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_class_declaration_with_methods() -> None:
|
|
74
|
+
r = _index(
|
|
75
|
+
"""
|
|
76
|
+
class Greeter {
|
|
77
|
+
hello(name) { return name; }
|
|
78
|
+
bye() {}
|
|
79
|
+
}
|
|
80
|
+
"""
|
|
81
|
+
)
|
|
82
|
+
kinds = sorted(s.kind for s in r.symbols)
|
|
83
|
+
assert kinds == ["class", "method", "method"]
|
|
84
|
+
method_ids = [str(s.id) for s in r.symbols if s.kind == "method"]
|
|
85
|
+
assert any("Greeter#hello()." in i for i in method_ids)
|
|
86
|
+
assert any("Greeter#bye()." in i for i in method_ids)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_top_level_const_is_variable() -> None:
|
|
90
|
+
r = _index("const MAX = 10;")
|
|
91
|
+
assert len(r.symbols) == 1
|
|
92
|
+
assert r.symbols[0].kind == "variable"
|
|
93
|
+
assert str(r.symbols[0].id).endswith("MAX.")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_top_level_let_is_variable() -> None:
|
|
97
|
+
r = _index("let counter = 0;")
|
|
98
|
+
assert len(r.symbols) == 1
|
|
99
|
+
assert r.symbols[0].kind == "variable"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_top_level_var_is_variable() -> None:
|
|
103
|
+
r = _index("var legacy = 1;")
|
|
104
|
+
assert len(r.symbols) == 1
|
|
105
|
+
assert r.symbols[0].kind == "variable"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_top_level_multi_declarator() -> None:
|
|
109
|
+
r = _index("const a = 1, b = 2;")
|
|
110
|
+
assert {s.id.descriptors[-1].name for s in r.symbols} == {"a", "b"}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# SymbolID structure
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_symbol_id_uses_path_namespaces() -> None:
|
|
119
|
+
r = _index("function f() {}", path="pkg/sub/m.js")
|
|
120
|
+
assert str(r.symbols[0].id) == "scip-javascript . . . pkg/sub/m.js/f()."
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_jsx_file_supported() -> None:
|
|
124
|
+
r = _index(
|
|
125
|
+
"""
|
|
126
|
+
function App() {
|
|
127
|
+
return <div>hello</div>;
|
|
128
|
+
}
|
|
129
|
+
""",
|
|
130
|
+
path="App.jsx",
|
|
131
|
+
)
|
|
132
|
+
assert len(r.symbols) == 1
|
|
133
|
+
assert r.symbols[0].kind == "function"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_mjs_module_supported() -> None:
|
|
137
|
+
r = _index(
|
|
138
|
+
"""
|
|
139
|
+
export function fromEsm() { return 1; }
|
|
140
|
+
""",
|
|
141
|
+
path="lib.mjs",
|
|
142
|
+
)
|
|
143
|
+
assert any(s.kind == "function" and "fromEsm" in str(s.id) for s in r.symbols)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_cjs_module_supported() -> None:
|
|
147
|
+
r = _index(
|
|
148
|
+
"""
|
|
149
|
+
function fromCjs() { return 1; }
|
|
150
|
+
module.exports = { fromCjs };
|
|
151
|
+
""",
|
|
152
|
+
path="lib.cjs",
|
|
153
|
+
)
|
|
154
|
+
assert any(s.kind == "function" and "fromCjs" in str(s.id) for s in r.symbols)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Diagnostics / edge cases
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_empty_file_produces_nothing() -> None:
|
|
163
|
+
r = _index("")
|
|
164
|
+
assert r.symbols == []
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_syntax_error_yields_warning_diagnostic() -> None:
|
|
168
|
+
r = _index("function broken( { ")
|
|
169
|
+
codes = {d.code for d in r.diagnostics}
|
|
170
|
+
assert "JS001" in codes
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_invalid_utf8_yields_error_diagnostic() -> None:
|
|
174
|
+
ix = JavaScriptIndexer()
|
|
175
|
+
r = ix.index_file(
|
|
176
|
+
Path("bad.js"),
|
|
177
|
+
b"\xff\xfe garbage",
|
|
178
|
+
IndexContext(
|
|
179
|
+
project_root=Path("/tmp/proj"),
|
|
180
|
+
relative_path=PurePosixPath("bad.js"),
|
|
181
|
+
language="javascript",
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
assert r.symbols == []
|
|
185
|
+
assert r.diagnostics[0].code == "JS002"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
# Nested classes
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_class_inside_function_is_still_indexed() -> None:
|
|
194
|
+
r = _index(
|
|
195
|
+
"""
|
|
196
|
+
function outer() {
|
|
197
|
+
class Inner {
|
|
198
|
+
m() {}
|
|
199
|
+
}
|
|
200
|
+
return Inner;
|
|
201
|
+
}
|
|
202
|
+
"""
|
|
203
|
+
)
|
|
204
|
+
kinds = sorted(s.kind for s in r.symbols)
|
|
205
|
+
assert "function" in kinds
|
|
206
|
+
assert "class" in kinds
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
# Arrow functions / object methods (not currently indexed — sanity only)
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_arrow_function_not_indexed_at_top_level() -> None:
|
|
215
|
+
"""Top-level ``const fn = () => ...`` is a variable, not a function.
|
|
216
|
+
|
|
217
|
+
Matching the typescript indexer's behavior: arrows assigned to consts
|
|
218
|
+
surface as variables, not functions. A future PR may add named arrow
|
|
219
|
+
detection across both plugins.
|
|
220
|
+
"""
|
|
221
|
+
r = _index("const add = (a, b) => a + b;")
|
|
222
|
+
assert len(r.symbols) == 1
|
|
223
|
+
assert r.symbols[0].kind == "variable"
|