codemap-scala 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.
- codemap_scala-0.1.0/.gitignore +43 -0
- codemap_scala-0.1.0/PKG-INFO +61 -0
- codemap_scala-0.1.0/README.md +42 -0
- codemap_scala-0.1.0/pyproject.toml +35 -0
- codemap_scala-0.1.0/src/codemap_scala/__init__.py +8 -0
- codemap_scala-0.1.0/src/codemap_scala/indexer.py +287 -0
- codemap_scala-0.1.0/tests/__init__.py +0 -0
- codemap_scala-0.1.0/tests/test_indexer.py +173 -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,61 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codemap-scala
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Scala language indexer plugin for CodeMap
|
|
5
|
+
Project-URL: Homepage, https://github.com/qxbyte/codemap
|
|
6
|
+
Author: CodeMap Contributors
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: codemap,indexer,scala,tree-sitter
|
|
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-scala>=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-scala
|
|
21
|
+
|
|
22
|
+
> A Scala language indexer for [CodeMap](https://github.com/qxbyte/codemap),
|
|
23
|
+
> shipped as an independent PyPI package.
|
|
24
|
+
|
|
25
|
+
## What it captures
|
|
26
|
+
|
|
27
|
+
Backed by `tree-sitter-scala`:
|
|
28
|
+
|
|
29
|
+
| AST node | Symbol kind |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `package_clause` (incl. `package_identifier`) | namespace prefix |
|
|
32
|
+
| `class_definition` (incl. `case class`) | `class` (`extra.scala_kind=class` / `case_class`) |
|
|
33
|
+
| `object_definition` (incl. `case object`) | `class` (`extra.scala_kind=object`) |
|
|
34
|
+
| `trait_definition` | `class` (`extra.scala_kind=trait`) |
|
|
35
|
+
| `type_definition` (top-level alias) | `class` (`extra.scala_kind=type`) |
|
|
36
|
+
| `function_definition` / `function_declaration` | `method` |
|
|
37
|
+
| `val_definition` (top-level or in template body) | `field` (`extra.scala_kind=val`) |
|
|
38
|
+
| `var_definition` (top-level or in template body) | `field` (`extra.scala_kind=var`) |
|
|
39
|
+
| `class_parameter` (in `case class`) | `field` (`extra.scala_kind=case_field`) |
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install codemap-scala
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## File patterns
|
|
48
|
+
|
|
49
|
+
* `*.scala`, `*.sc`
|
|
50
|
+
|
|
51
|
+
## Limits
|
|
52
|
+
|
|
53
|
+
* Implicits and Scala 3 `given` declarations parse but are not specifically
|
|
54
|
+
tagged.
|
|
55
|
+
* Anonymous classes (`new T { ... }`) are not surfaced.
|
|
56
|
+
* Block-scoped `def` inside a method body is not emitted — it is private
|
|
57
|
+
to that method.
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# codemap-scala
|
|
2
|
+
|
|
3
|
+
> A Scala language indexer for [CodeMap](https://github.com/qxbyte/codemap),
|
|
4
|
+
> shipped as an independent PyPI package.
|
|
5
|
+
|
|
6
|
+
## What it captures
|
|
7
|
+
|
|
8
|
+
Backed by `tree-sitter-scala`:
|
|
9
|
+
|
|
10
|
+
| AST node | Symbol kind |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `package_clause` (incl. `package_identifier`) | namespace prefix |
|
|
13
|
+
| `class_definition` (incl. `case class`) | `class` (`extra.scala_kind=class` / `case_class`) |
|
|
14
|
+
| `object_definition` (incl. `case object`) | `class` (`extra.scala_kind=object`) |
|
|
15
|
+
| `trait_definition` | `class` (`extra.scala_kind=trait`) |
|
|
16
|
+
| `type_definition` (top-level alias) | `class` (`extra.scala_kind=type`) |
|
|
17
|
+
| `function_definition` / `function_declaration` | `method` |
|
|
18
|
+
| `val_definition` (top-level or in template body) | `field` (`extra.scala_kind=val`) |
|
|
19
|
+
| `var_definition` (top-level or in template body) | `field` (`extra.scala_kind=var`) |
|
|
20
|
+
| `class_parameter` (in `case class`) | `field` (`extra.scala_kind=case_field`) |
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install codemap-scala
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## File patterns
|
|
29
|
+
|
|
30
|
+
* `*.scala`, `*.sc`
|
|
31
|
+
|
|
32
|
+
## Limits
|
|
33
|
+
|
|
34
|
+
* Implicits and Scala 3 `given` declarations parse but are not specifically
|
|
35
|
+
tagged.
|
|
36
|
+
* Anonymous classes (`new T { ... }`) are not surfaced.
|
|
37
|
+
* Block-scoped `def` inside a method body is not emitted — it is private
|
|
38
|
+
to that method.
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
MIT.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "codemap-scala"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Scala language indexer plugin for CodeMap"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "CodeMap Contributors" }]
|
|
13
|
+
keywords = ["codemap", "scala", "indexer", "tree-sitter"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Topic :: Software Development",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"codemap-core>=0.1.0,<0.2",
|
|
21
|
+
"tree-sitter>=0.25",
|
|
22
|
+
"tree-sitter-scala>=0.23",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = ["pytest>=8.0"]
|
|
27
|
+
|
|
28
|
+
[project.entry-points."codemap.indexers"]
|
|
29
|
+
scala = "codemap_scala:ScalaIndexer"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/qxbyte/codemap"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/codemap_scala"]
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Scala indexer built on tree-sitter-scala.
|
|
2
|
+
|
|
3
|
+
A leading ``package_clause`` (or multiple, for nested-package syntax)
|
|
4
|
+
prefixes every symbol with namespace descriptors. Classes / objects /
|
|
5
|
+
traits expose their template body so nested ``def`` / ``val`` / ``var``
|
|
6
|
+
members become methods/fields of the enclosing type.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path, PurePosixPath
|
|
12
|
+
from typing import ClassVar
|
|
13
|
+
|
|
14
|
+
import tree_sitter
|
|
15
|
+
import tree_sitter_scala
|
|
16
|
+
|
|
17
|
+
from codemap.core.models import Diagnostic, Edge, IndexResult, Range, Symbol
|
|
18
|
+
from codemap.core.symbol import Descriptor, DescriptorKind, SymbolID
|
|
19
|
+
from codemap.indexers.base import IndexContext
|
|
20
|
+
|
|
21
|
+
SCHEME = "scip-scala"
|
|
22
|
+
LANG = "scala"
|
|
23
|
+
|
|
24
|
+
_SCALA_LANG = tree_sitter.Language(tree_sitter_scala.language())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ScalaIndexer:
|
|
28
|
+
name: ClassVar[str] = "scala"
|
|
29
|
+
version: ClassVar[str] = "0.1.0"
|
|
30
|
+
file_patterns: ClassVar[list[str]] = ["*.scala", "*.sc"]
|
|
31
|
+
languages: ClassVar[list[str]] = [LANG]
|
|
32
|
+
|
|
33
|
+
def supports(self, path: Path) -> bool:
|
|
34
|
+
return path.suffix in {".scala", ".sc"}
|
|
35
|
+
|
|
36
|
+
def index_file(
|
|
37
|
+
self,
|
|
38
|
+
path: Path,
|
|
39
|
+
source: bytes,
|
|
40
|
+
ctx: IndexContext,
|
|
41
|
+
) -> IndexResult:
|
|
42
|
+
try:
|
|
43
|
+
source.decode("utf-8")
|
|
44
|
+
except UnicodeDecodeError as exc:
|
|
45
|
+
return IndexResult(
|
|
46
|
+
diagnostics=[
|
|
47
|
+
Diagnostic(
|
|
48
|
+
severity="error",
|
|
49
|
+
file=ctx.relative_path,
|
|
50
|
+
code="SCALA002",
|
|
51
|
+
message=f"not valid UTF-8: {exc}",
|
|
52
|
+
producer=self.name,
|
|
53
|
+
)
|
|
54
|
+
]
|
|
55
|
+
)
|
|
56
|
+
parser = tree_sitter.Parser(_SCALA_LANG)
|
|
57
|
+
tree = parser.parse(source)
|
|
58
|
+
visitor = _Visitor(ctx.relative_path)
|
|
59
|
+
visitor.visit_root(tree.root_node)
|
|
60
|
+
diagnostics = list(visitor.diagnostics)
|
|
61
|
+
if tree.root_node.has_error:
|
|
62
|
+
diagnostics.append(
|
|
63
|
+
Diagnostic(
|
|
64
|
+
severity="warning",
|
|
65
|
+
file=ctx.relative_path,
|
|
66
|
+
range=Range(start_line=1, end_line=1),
|
|
67
|
+
code="SCALA001",
|
|
68
|
+
message="tree-sitter reported parse errors; symbols may be incomplete",
|
|
69
|
+
producer=self.name,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
return IndexResult(
|
|
73
|
+
symbols=visitor.symbols,
|
|
74
|
+
edges=visitor.edges,
|
|
75
|
+
diagnostics=diagnostics,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class _Visitor:
|
|
80
|
+
def __init__(self, relative_path: PurePosixPath) -> None:
|
|
81
|
+
self.relative_path = relative_path
|
|
82
|
+
self.symbols: list[Symbol] = []
|
|
83
|
+
self.edges: list[Edge] = []
|
|
84
|
+
self.diagnostics: list[Diagnostic] = []
|
|
85
|
+
|
|
86
|
+
def visit_root(self, root: tree_sitter.Node) -> None:
|
|
87
|
+
scope: list[Descriptor] = []
|
|
88
|
+
for child in root.children:
|
|
89
|
+
kind = child.type
|
|
90
|
+
if kind == "package_clause":
|
|
91
|
+
scope.extend(
|
|
92
|
+
Descriptor(name=part, kind=DescriptorKind.NAMESPACE)
|
|
93
|
+
for part in _package_parts(child)
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
self._visit(child, scope=scope)
|
|
97
|
+
|
|
98
|
+
def _visit(
|
|
99
|
+
self,
|
|
100
|
+
node: tree_sitter.Node,
|
|
101
|
+
*,
|
|
102
|
+
scope: list[Descriptor],
|
|
103
|
+
) -> None:
|
|
104
|
+
kind = node.type
|
|
105
|
+
if kind == "class_definition":
|
|
106
|
+
scala_kind = "case_class" if _is_case(node) else "class"
|
|
107
|
+
self._emit_type(node, scope=scope, scala_kind=scala_kind)
|
|
108
|
+
elif kind == "object_definition":
|
|
109
|
+
scala_kind = "case_object" if _is_case(node) else "object"
|
|
110
|
+
self._emit_type(node, scope=scope, scala_kind=scala_kind)
|
|
111
|
+
elif kind == "trait_definition":
|
|
112
|
+
self._emit_type(node, scope=scope, scala_kind="trait")
|
|
113
|
+
elif kind == "type_definition":
|
|
114
|
+
self._emit_type_alias(node, scope=scope)
|
|
115
|
+
elif kind in {"function_definition", "function_declaration"}:
|
|
116
|
+
self._emit_method(node, scope=scope)
|
|
117
|
+
elif kind == "val_definition":
|
|
118
|
+
self._emit_value(node, scope=scope, scala_kind="val")
|
|
119
|
+
elif kind == "var_definition":
|
|
120
|
+
self._emit_value(node, scope=scope, scala_kind="var")
|
|
121
|
+
|
|
122
|
+
def _emit_type(
|
|
123
|
+
self,
|
|
124
|
+
node: tree_sitter.Node,
|
|
125
|
+
*,
|
|
126
|
+
scope: list[Descriptor],
|
|
127
|
+
scala_kind: str,
|
|
128
|
+
) -> None:
|
|
129
|
+
name = _first_child_text(node, "identifier")
|
|
130
|
+
if name is None:
|
|
131
|
+
return
|
|
132
|
+
type_desc = Descriptor(name=name, kind=DescriptorKind.TYPE)
|
|
133
|
+
descriptors = [*list(scope), type_desc]
|
|
134
|
+
self.symbols.append(
|
|
135
|
+
Symbol(
|
|
136
|
+
id=self._make_id(descriptors),
|
|
137
|
+
kind="class",
|
|
138
|
+
language=LANG,
|
|
139
|
+
file=self.relative_path,
|
|
140
|
+
range=_node_range(node),
|
|
141
|
+
extra={"scala_kind": scala_kind},
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
# Case-class constructor parameters become fields.
|
|
145
|
+
if scala_kind == "case_class":
|
|
146
|
+
params = _first_child(node, "class_parameters")
|
|
147
|
+
if params is not None:
|
|
148
|
+
for child in params.children:
|
|
149
|
+
if child.type == "class_parameter":
|
|
150
|
+
pname = _first_child_text(child, "identifier")
|
|
151
|
+
if pname is None:
|
|
152
|
+
continue
|
|
153
|
+
self.symbols.append(
|
|
154
|
+
Symbol(
|
|
155
|
+
id=self._make_id(
|
|
156
|
+
[
|
|
157
|
+
*list(descriptors),
|
|
158
|
+
Descriptor(name=pname, kind=DescriptorKind.TERM),
|
|
159
|
+
]
|
|
160
|
+
),
|
|
161
|
+
kind="field",
|
|
162
|
+
language=LANG,
|
|
163
|
+
file=self.relative_path,
|
|
164
|
+
range=_node_range(child),
|
|
165
|
+
extra={"scala_kind": "case_field"},
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
# Walk the template body for nested members.
|
|
169
|
+
body = _first_child(node, "template_body")
|
|
170
|
+
if body is None:
|
|
171
|
+
return
|
|
172
|
+
for child in body.children:
|
|
173
|
+
self._visit(child, scope=descriptors)
|
|
174
|
+
|
|
175
|
+
def _emit_type_alias(
|
|
176
|
+
self,
|
|
177
|
+
node: tree_sitter.Node,
|
|
178
|
+
*,
|
|
179
|
+
scope: list[Descriptor],
|
|
180
|
+
) -> None:
|
|
181
|
+
name = _first_child_text(node, "type_identifier")
|
|
182
|
+
if name is None:
|
|
183
|
+
return
|
|
184
|
+
self.symbols.append(
|
|
185
|
+
Symbol(
|
|
186
|
+
id=self._make_id([*list(scope), Descriptor(name=name, kind=DescriptorKind.TYPE)]),
|
|
187
|
+
kind="class",
|
|
188
|
+
language=LANG,
|
|
189
|
+
file=self.relative_path,
|
|
190
|
+
range=_node_range(node),
|
|
191
|
+
extra={"scala_kind": "type"},
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def _emit_method(
|
|
196
|
+
self,
|
|
197
|
+
node: tree_sitter.Node,
|
|
198
|
+
*,
|
|
199
|
+
scope: list[Descriptor],
|
|
200
|
+
) -> None:
|
|
201
|
+
name = _first_child_text(node, "identifier")
|
|
202
|
+
if name is None:
|
|
203
|
+
return
|
|
204
|
+
self.symbols.append(
|
|
205
|
+
Symbol(
|
|
206
|
+
id=self._make_id([*list(scope), Descriptor(name=name, kind=DescriptorKind.METHOD)]),
|
|
207
|
+
kind="method",
|
|
208
|
+
language=LANG,
|
|
209
|
+
file=self.relative_path,
|
|
210
|
+
range=_node_range(node),
|
|
211
|
+
signature=f"{name}()",
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def _emit_value(
|
|
216
|
+
self,
|
|
217
|
+
node: tree_sitter.Node,
|
|
218
|
+
*,
|
|
219
|
+
scope: list[Descriptor],
|
|
220
|
+
scala_kind: str,
|
|
221
|
+
) -> None:
|
|
222
|
+
name = _first_child_text(node, "identifier")
|
|
223
|
+
if name is None:
|
|
224
|
+
return
|
|
225
|
+
self.symbols.append(
|
|
226
|
+
Symbol(
|
|
227
|
+
id=self._make_id([*list(scope), Descriptor(name=name, kind=DescriptorKind.TERM)]),
|
|
228
|
+
kind="field",
|
|
229
|
+
language=LANG,
|
|
230
|
+
file=self.relative_path,
|
|
231
|
+
range=_node_range(node),
|
|
232
|
+
extra={"scala_kind": scala_kind},
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def _make_id(self, descriptors: list[Descriptor]) -> SymbolID:
|
|
237
|
+
full = list(_path_namespaces(self.relative_path))
|
|
238
|
+
full.extend(descriptors)
|
|
239
|
+
return SymbolID(scheme=SCHEME, descriptors=tuple(full))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
# Pure helpers
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _path_namespaces(path: PurePosixPath) -> list[Descriptor]:
|
|
248
|
+
return [Descriptor(name=part, kind=DescriptorKind.NAMESPACE) for part in path.parts]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _node_range(node: tree_sitter.Node) -> Range:
|
|
252
|
+
sr, sc = node.start_point
|
|
253
|
+
er, ec = node.end_point
|
|
254
|
+
return Range(
|
|
255
|
+
start_line=sr + 1,
|
|
256
|
+
start_col=sc,
|
|
257
|
+
end_line=max(er + 1, sr + 1),
|
|
258
|
+
end_col=ec,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _node_text(node: tree_sitter.Node) -> str:
|
|
263
|
+
return node.text.decode("utf-8") if node.text is not None else ""
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _first_child(node: tree_sitter.Node, kind: str) -> tree_sitter.Node | None:
|
|
267
|
+
for child in node.children:
|
|
268
|
+
if child.type == kind:
|
|
269
|
+
return child
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _first_child_text(node: tree_sitter.Node, kind: str) -> str | None:
|
|
274
|
+
found = _first_child(node, kind)
|
|
275
|
+
return _node_text(found) if found is not None else None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _is_case(node: tree_sitter.Node) -> bool:
|
|
279
|
+
return any(child.type == "case" for child in node.children)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _package_parts(node: tree_sitter.Node) -> list[str]:
|
|
283
|
+
ident_node = _first_child(node, "package_identifier")
|
|
284
|
+
if ident_node is None:
|
|
285
|
+
# Fallback: single identifier under package_clause.
|
|
286
|
+
return [_node_text(c) for c in node.children if c.type == "identifier"]
|
|
287
|
+
return [_node_text(c) for c in ident_node.children if c.type == "identifier"]
|
|
File without changes
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Unit tests for the Scala indexer plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import textwrap
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
|
|
8
|
+
from codemap_scala import ScalaIndexer
|
|
9
|
+
from codemap_scala.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/Lib.scala") -> IndexResult:
|
|
16
|
+
code = textwrap.dedent(source).lstrip("\n")
|
|
17
|
+
return ScalaIndexer().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="scala",
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_indexer_metadata() -> None:
|
|
29
|
+
ix = ScalaIndexer()
|
|
30
|
+
assert ix.name == "scala"
|
|
31
|
+
assert ix.languages == ["scala"]
|
|
32
|
+
assert ix.supports(Path("a.scala"))
|
|
33
|
+
assert ix.supports(Path("a.sc"))
|
|
34
|
+
assert not ix.supports(Path("a.py"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_scheme_is_consistent() -> None:
|
|
38
|
+
r = _index("class C")
|
|
39
|
+
for s in r.symbols:
|
|
40
|
+
assert str(s.id).startswith(f"{SCHEME} ")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_class_with_val_var_def() -> None:
|
|
44
|
+
r = _index(
|
|
45
|
+
"""
|
|
46
|
+
class Foo {
|
|
47
|
+
val x: Int = 1
|
|
48
|
+
var y: String = "hi"
|
|
49
|
+
def bar(a: Int): Int = a + 1
|
|
50
|
+
}
|
|
51
|
+
"""
|
|
52
|
+
)
|
|
53
|
+
classes = [s for s in r.symbols if s.kind == "class"]
|
|
54
|
+
methods = [s for s in r.symbols if s.kind == "method"]
|
|
55
|
+
fields = [s for s in r.symbols if s.kind == "field"]
|
|
56
|
+
assert {c.id.descriptors[-1].name for c in classes} == {"Foo"}
|
|
57
|
+
assert {m.id.descriptors[-1].name for m in methods} == {"bar"}
|
|
58
|
+
assert {f.id.descriptors[-1].name for f in fields} == {"x", "y"}
|
|
59
|
+
val_field = next(f for f in fields if f.id.descriptors[-1].name == "x")
|
|
60
|
+
var_field = next(f for f in fields if f.id.descriptors[-1].name == "y")
|
|
61
|
+
assert val_field.extra["scala_kind"] == "val"
|
|
62
|
+
assert var_field.extra["scala_kind"] == "var"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_object_definition() -> None:
|
|
66
|
+
r = _index(
|
|
67
|
+
"""
|
|
68
|
+
object Helpers {
|
|
69
|
+
def hello = "world"
|
|
70
|
+
}
|
|
71
|
+
"""
|
|
72
|
+
)
|
|
73
|
+
obj = next(s for s in r.symbols if s.kind == "class")
|
|
74
|
+
assert obj.extra["scala_kind"] == "object"
|
|
75
|
+
methods = [s for s in r.symbols if s.kind == "method"]
|
|
76
|
+
assert {m.id.descriptors[-1].name for m in methods} == {"hello"}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_trait_with_function_declaration() -> None:
|
|
80
|
+
r = _index(
|
|
81
|
+
"""
|
|
82
|
+
trait Greeter {
|
|
83
|
+
def greet: String
|
|
84
|
+
}
|
|
85
|
+
"""
|
|
86
|
+
)
|
|
87
|
+
trait = next(s for s in r.symbols if s.kind == "class")
|
|
88
|
+
assert trait.extra["scala_kind"] == "trait"
|
|
89
|
+
methods = [s for s in r.symbols if s.kind == "method"]
|
|
90
|
+
assert {m.id.descriptors[-1].name for m in methods} == {"greet"}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_case_class_parameters_become_fields() -> None:
|
|
94
|
+
r = _index("case class Person(name: String, age: Int)")
|
|
95
|
+
cls = next(s for s in r.symbols if s.kind == "class")
|
|
96
|
+
assert cls.extra["scala_kind"] == "case_class"
|
|
97
|
+
fields = [s for s in r.symbols if s.kind == "field"]
|
|
98
|
+
assert {f.id.descriptors[-1].name for f in fields} == {"name", "age"}
|
|
99
|
+
for f in fields:
|
|
100
|
+
assert f.extra["scala_kind"] == "case_field"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_top_level_type_alias() -> None:
|
|
104
|
+
r = _index("type Aliased = Int")
|
|
105
|
+
classes = [s for s in r.symbols if s.kind == "class"]
|
|
106
|
+
assert classes[0].extra["scala_kind"] == "type"
|
|
107
|
+
assert classes[0].id.descriptors[-1].name == "Aliased"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_package_clause_prefixes_descriptors() -> None:
|
|
111
|
+
r = _index(
|
|
112
|
+
"""
|
|
113
|
+
package com.example.app
|
|
114
|
+
class Foo
|
|
115
|
+
"""
|
|
116
|
+
)
|
|
117
|
+
foo = next(s for s in r.symbols if s.kind == "class")
|
|
118
|
+
names = [d.name for d in foo.id.descriptors]
|
|
119
|
+
assert names.index("com") < names.index("example") < names.index("app") < names.index("Foo")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_nested_class_in_object() -> None:
|
|
123
|
+
r = _index(
|
|
124
|
+
"""
|
|
125
|
+
object Outer {
|
|
126
|
+
class Inner {
|
|
127
|
+
val a: Int = 1
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
"""
|
|
131
|
+
)
|
|
132
|
+
inner = next(s for s in r.symbols if s.kind == "class" and s.id.descriptors[-1].name == "Inner")
|
|
133
|
+
names = [d.name for d in inner.id.descriptors]
|
|
134
|
+
assert names.index("Outer") < names.index("Inner")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_top_level_val_and_def() -> None:
|
|
138
|
+
r = _index(
|
|
139
|
+
"""
|
|
140
|
+
val greeting = "hi"
|
|
141
|
+
def shout(s: String): String = s.toUpperCase
|
|
142
|
+
"""
|
|
143
|
+
)
|
|
144
|
+
fields = [s for s in r.symbols if s.kind == "field"]
|
|
145
|
+
methods = [s for s in r.symbols if s.kind == "method"]
|
|
146
|
+
assert {f.id.descriptors[-1].name for f in fields} == {"greeting"}
|
|
147
|
+
assert {m.id.descriptors[-1].name for m in methods} == {"shout"}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_symbol_id_uses_path_namespaces() -> None:
|
|
151
|
+
r = _index("class C", path="src/util/Helpers.scala")
|
|
152
|
+
cls = next(s for s in r.symbols if s.kind == "class")
|
|
153
|
+
assert str(cls.id) == "scip-scala . . . src/util/Helpers.scala/C#"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_empty_file_yields_no_symbols() -> None:
|
|
157
|
+
r = _index("")
|
|
158
|
+
assert r.symbols == []
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_invalid_utf8_yields_diagnostic() -> None:
|
|
162
|
+
ix = ScalaIndexer()
|
|
163
|
+
r = ix.index_file(
|
|
164
|
+
Path("Bad.scala"),
|
|
165
|
+
b"\xff\xfe class",
|
|
166
|
+
IndexContext(
|
|
167
|
+
project_root=Path("/tmp/proj"),
|
|
168
|
+
relative_path=PurePosixPath("Bad.scala"),
|
|
169
|
+
language="scala",
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
assert r.symbols == []
|
|
173
|
+
assert r.diagnostics[0].code == "SCALA002"
|