codemap-java 0.1.0a1__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_java-0.1.0a1/.gitignore +43 -0
- codemap_java-0.1.0a1/PKG-INFO +70 -0
- codemap_java-0.1.0a1/README.md +50 -0
- codemap_java-0.1.0a1/pyproject.toml +36 -0
- codemap_java-0.1.0a1/src/codemap_java/__init__.py +8 -0
- codemap_java-0.1.0a1/src/codemap_java/indexer.py +254 -0
- codemap_java-0.1.0a1/tests/__init__.py +0 -0
- codemap_java-0.1.0a1/tests/test_indexer.py +186 -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,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codemap-java
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Java indexer plugin for CodeMap
|
|
5
|
+
Project-URL: Homepage, https://github.com/qxbyte/codemap
|
|
6
|
+
Author: CodeMap Contributors
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: codemap,indexer,java,tree-sitter
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Java
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Software Development
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: codemap-core<0.2,>=0.1.0a1
|
|
15
|
+
Requires-Dist: tree-sitter-java>=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-java
|
|
22
|
+
|
|
23
|
+
> A Java indexer for [CodeMap](https://github.com/qxbyte/codemap),
|
|
24
|
+
> distributed as an independent PyPI package.
|
|
25
|
+
|
|
26
|
+
## What it captures
|
|
27
|
+
|
|
28
|
+
Backed by `tree-sitter-java`. Single-file, no cross-file type inference (MVP):
|
|
29
|
+
|
|
30
|
+
| AST node | Symbol kind |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `class_declaration` | `class` |
|
|
33
|
+
| `interface_declaration` | `interface` (stored as `class` with `extra.java_kind=interface`) |
|
|
34
|
+
| `enum_declaration` | stored as `class` with `extra.java_kind=enum` |
|
|
35
|
+
| `record_declaration` | stored as `class` with `extra.java_kind=record` |
|
|
36
|
+
| `method_declaration` (inside type) | `method` |
|
|
37
|
+
| `constructor_declaration` | `method` (signature prefixed with `<init>`) |
|
|
38
|
+
| `field_declaration` (inside type) | `field` |
|
|
39
|
+
|
|
40
|
+
Package declarations are captured and used as a prefix for the in-file
|
|
41
|
+
`package` namespace.
|
|
42
|
+
|
|
43
|
+
## SymbolID encoding
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
scip-java . . . src/com/example/Greeter.java/Greeter#hello().
|
|
47
|
+
└────────┘ └────────────────────────────────┘ └──────┘ └─────┘
|
|
48
|
+
scheme file path type method
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install "git+https://github.com/qxbyte/codemap.git#subdirectory=plugins/codemap-java"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
After installation, `codemap doctor` lists `java` next to the other
|
|
58
|
+
indexers on identical terms — same Indexer Protocol, same entry-point
|
|
59
|
+
group, no main-repo change required (ADR-004 + ADR-L001).
|
|
60
|
+
|
|
61
|
+
## Limits
|
|
62
|
+
|
|
63
|
+
* No `extends` / `implements` edges yet. Easy to add in v0.2.0.
|
|
64
|
+
* No generic-parameter descriptors.
|
|
65
|
+
* No annotation extraction (planned).
|
|
66
|
+
* Anonymous classes are skipped.
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# codemap-java
|
|
2
|
+
|
|
3
|
+
> A Java indexer for [CodeMap](https://github.com/qxbyte/codemap),
|
|
4
|
+
> distributed as an independent PyPI package.
|
|
5
|
+
|
|
6
|
+
## What it captures
|
|
7
|
+
|
|
8
|
+
Backed by `tree-sitter-java`. Single-file, no cross-file type inference (MVP):
|
|
9
|
+
|
|
10
|
+
| AST node | Symbol kind |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `class_declaration` | `class` |
|
|
13
|
+
| `interface_declaration` | `interface` (stored as `class` with `extra.java_kind=interface`) |
|
|
14
|
+
| `enum_declaration` | stored as `class` with `extra.java_kind=enum` |
|
|
15
|
+
| `record_declaration` | stored as `class` with `extra.java_kind=record` |
|
|
16
|
+
| `method_declaration` (inside type) | `method` |
|
|
17
|
+
| `constructor_declaration` | `method` (signature prefixed with `<init>`) |
|
|
18
|
+
| `field_declaration` (inside type) | `field` |
|
|
19
|
+
|
|
20
|
+
Package declarations are captured and used as a prefix for the in-file
|
|
21
|
+
`package` namespace.
|
|
22
|
+
|
|
23
|
+
## SymbolID encoding
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
scip-java . . . src/com/example/Greeter.java/Greeter#hello().
|
|
27
|
+
└────────┘ └────────────────────────────────┘ └──────┘ └─────┘
|
|
28
|
+
scheme file path type method
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install "git+https://github.com/qxbyte/codemap.git#subdirectory=plugins/codemap-java"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
After installation, `codemap doctor` lists `java` next to the other
|
|
38
|
+
indexers on identical terms — same Indexer Protocol, same entry-point
|
|
39
|
+
group, no main-repo change required (ADR-004 + ADR-L001).
|
|
40
|
+
|
|
41
|
+
## Limits
|
|
42
|
+
|
|
43
|
+
* No `extends` / `implements` edges yet. Easy to add in v0.2.0.
|
|
44
|
+
* No generic-parameter descriptors.
|
|
45
|
+
* No annotation extraction (planned).
|
|
46
|
+
* Anonymous classes are skipped.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "codemap-java"
|
|
7
|
+
version = "0.1.0a1"
|
|
8
|
+
description = "Java 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", "java", "indexer", "tree-sitter"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Java",
|
|
18
|
+
"Topic :: Software Development",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"codemap-core>=0.1.0a1,<0.2",
|
|
22
|
+
"tree-sitter>=0.25",
|
|
23
|
+
"tree-sitter-java>=0.23",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = ["pytest>=8.0"]
|
|
28
|
+
|
|
29
|
+
[project.entry-points."codemap.indexers"]
|
|
30
|
+
java = "codemap_java:JavaIndexer"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/qxbyte/codemap"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/codemap_java"]
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Java indexer built on tree-sitter-java.
|
|
2
|
+
|
|
3
|
+
Covers class / interface / enum / record / method / constructor / field
|
|
4
|
+
declarations. Package declarations are honoured as a namespace prefix
|
|
5
|
+
under the file path. Nested types track a class stack to produce the
|
|
6
|
+
correct ``Cls#Inner#m()`` chain.
|
|
7
|
+
|
|
8
|
+
The indexer is single-file by design; cross-file `extends` / `implements`
|
|
9
|
+
resolution lives in a future bridge so the indexer surface stays narrow.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path, PurePosixPath
|
|
15
|
+
from typing import ClassVar
|
|
16
|
+
|
|
17
|
+
import tree_sitter
|
|
18
|
+
import tree_sitter_java
|
|
19
|
+
|
|
20
|
+
from codemap.core.models import Diagnostic, Edge, IndexResult, Range, Symbol
|
|
21
|
+
from codemap.core.symbol import Descriptor, DescriptorKind, SymbolID
|
|
22
|
+
from codemap.indexers.base import IndexContext
|
|
23
|
+
|
|
24
|
+
SCHEME = "scip-java"
|
|
25
|
+
LANG = "java"
|
|
26
|
+
|
|
27
|
+
_JAVA_LANG = tree_sitter.Language(tree_sitter_java.language())
|
|
28
|
+
|
|
29
|
+
_TYPE_DECLS = frozenset(
|
|
30
|
+
{
|
|
31
|
+
"class_declaration",
|
|
32
|
+
"interface_declaration",
|
|
33
|
+
"enum_declaration",
|
|
34
|
+
"record_declaration",
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class JavaIndexer:
|
|
40
|
+
name: ClassVar[str] = "java"
|
|
41
|
+
version: ClassVar[str] = "0.1.0"
|
|
42
|
+
file_patterns: ClassVar[list[str]] = ["*.java"]
|
|
43
|
+
languages: ClassVar[list[str]] = [LANG]
|
|
44
|
+
|
|
45
|
+
def supports(self, path: Path) -> bool:
|
|
46
|
+
return path.suffix == ".java"
|
|
47
|
+
|
|
48
|
+
def index_file(
|
|
49
|
+
self,
|
|
50
|
+
path: Path,
|
|
51
|
+
source: bytes,
|
|
52
|
+
ctx: IndexContext,
|
|
53
|
+
) -> IndexResult:
|
|
54
|
+
try:
|
|
55
|
+
source.decode("utf-8")
|
|
56
|
+
except UnicodeDecodeError as exc:
|
|
57
|
+
return IndexResult(
|
|
58
|
+
diagnostics=[
|
|
59
|
+
Diagnostic(
|
|
60
|
+
severity="error",
|
|
61
|
+
file=ctx.relative_path,
|
|
62
|
+
code="JAVA002",
|
|
63
|
+
message=f"not valid UTF-8: {exc}",
|
|
64
|
+
producer=self.name,
|
|
65
|
+
)
|
|
66
|
+
]
|
|
67
|
+
)
|
|
68
|
+
parser = tree_sitter.Parser(_JAVA_LANG)
|
|
69
|
+
tree = parser.parse(source)
|
|
70
|
+
visitor = _Visitor(ctx.relative_path)
|
|
71
|
+
visitor.visit(tree.root_node)
|
|
72
|
+
diagnostics = list(visitor.diagnostics)
|
|
73
|
+
if tree.root_node.has_error:
|
|
74
|
+
diagnostics.append(
|
|
75
|
+
Diagnostic(
|
|
76
|
+
severity="warning",
|
|
77
|
+
file=ctx.relative_path,
|
|
78
|
+
range=Range(start_line=1, end_line=1),
|
|
79
|
+
code="JAVA001",
|
|
80
|
+
message="tree-sitter reported parse errors; symbols may be incomplete",
|
|
81
|
+
producer=self.name,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
return IndexResult(
|
|
85
|
+
symbols=visitor.symbols,
|
|
86
|
+
edges=visitor.edges,
|
|
87
|
+
diagnostics=diagnostics,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# AST visitor
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class _Visitor:
|
|
97
|
+
def __init__(self, relative_path: PurePosixPath) -> None:
|
|
98
|
+
self.relative_path = relative_path
|
|
99
|
+
self.symbols: list[Symbol] = []
|
|
100
|
+
self.edges: list[Edge] = []
|
|
101
|
+
self.diagnostics: list[Diagnostic] = []
|
|
102
|
+
self._class_stack: list[str] = []
|
|
103
|
+
self._package: str = ""
|
|
104
|
+
|
|
105
|
+
def visit(self, node: tree_sitter.Node) -> None:
|
|
106
|
+
if node.type == "package_declaration":
|
|
107
|
+
self._package = _node_text(node.children[1]) if node.child_count > 1 else ""
|
|
108
|
+
return
|
|
109
|
+
if node.type in _TYPE_DECLS:
|
|
110
|
+
self._visit_type(node)
|
|
111
|
+
return
|
|
112
|
+
if node.type == "method_declaration" and self._class_stack:
|
|
113
|
+
self._visit_method(node, is_constructor=False)
|
|
114
|
+
return
|
|
115
|
+
if node.type == "constructor_declaration" and self._class_stack:
|
|
116
|
+
self._visit_method(node, is_constructor=True)
|
|
117
|
+
return
|
|
118
|
+
if node.type == "field_declaration" and self._class_stack:
|
|
119
|
+
self._visit_field(node)
|
|
120
|
+
return
|
|
121
|
+
for child in node.children:
|
|
122
|
+
self.visit(child)
|
|
123
|
+
|
|
124
|
+
# ------------------------------------------------------------- types
|
|
125
|
+
|
|
126
|
+
def _visit_type(self, node: tree_sitter.Node) -> None:
|
|
127
|
+
name = _name_child(node)
|
|
128
|
+
if name is None:
|
|
129
|
+
return
|
|
130
|
+
java_kind = node.type.removesuffix("_declaration")
|
|
131
|
+
sid = self._make_id(name, kind=DescriptorKind.TYPE)
|
|
132
|
+
self.symbols.append(
|
|
133
|
+
Symbol(
|
|
134
|
+
id=sid,
|
|
135
|
+
kind="class", # Symbol schema has no separate interface/enum kind
|
|
136
|
+
language=LANG,
|
|
137
|
+
file=self.relative_path,
|
|
138
|
+
range=_node_range(node),
|
|
139
|
+
extra={"java_kind": java_kind, "package": self._package}
|
|
140
|
+
if self._package or java_kind != "class"
|
|
141
|
+
else {},
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
body = node.child_by_field_name("body")
|
|
145
|
+
if body is None:
|
|
146
|
+
return
|
|
147
|
+
self._class_stack.append(name)
|
|
148
|
+
try:
|
|
149
|
+
for child in body.children:
|
|
150
|
+
self.visit(child)
|
|
151
|
+
finally:
|
|
152
|
+
self._class_stack.pop()
|
|
153
|
+
|
|
154
|
+
# ----------------------------------------------------------- members
|
|
155
|
+
|
|
156
|
+
def _visit_method(self, node: tree_sitter.Node, *, is_constructor: bool) -> None:
|
|
157
|
+
name = _name_child(node)
|
|
158
|
+
if name is None:
|
|
159
|
+
return
|
|
160
|
+
if is_constructor:
|
|
161
|
+
display = "<init>"
|
|
162
|
+
sid = self._make_id(display, kind=DescriptorKind.METHOD)
|
|
163
|
+
else:
|
|
164
|
+
display = name
|
|
165
|
+
sid = self._make_id(name, kind=DescriptorKind.METHOD)
|
|
166
|
+
signature = _method_signature(node, name, is_constructor=is_constructor)
|
|
167
|
+
self.symbols.append(
|
|
168
|
+
Symbol(
|
|
169
|
+
id=sid,
|
|
170
|
+
kind="method",
|
|
171
|
+
language=LANG,
|
|
172
|
+
file=self.relative_path,
|
|
173
|
+
range=_node_range(node),
|
|
174
|
+
signature=signature,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def _visit_field(self, node: tree_sitter.Node) -> None:
|
|
179
|
+
for child in node.children:
|
|
180
|
+
if child.type != "variable_declarator":
|
|
181
|
+
continue
|
|
182
|
+
name_node = child.child_by_field_name("name")
|
|
183
|
+
if name_node is None:
|
|
184
|
+
continue
|
|
185
|
+
name = _node_text(name_node)
|
|
186
|
+
if not name:
|
|
187
|
+
continue
|
|
188
|
+
sid = self._make_id(name, kind=DescriptorKind.TERM)
|
|
189
|
+
self.symbols.append(
|
|
190
|
+
Symbol(
|
|
191
|
+
id=sid,
|
|
192
|
+
kind="field",
|
|
193
|
+
language=LANG,
|
|
194
|
+
file=self.relative_path,
|
|
195
|
+
range=_node_range(child),
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# ----------------------------------------------------------- helpers
|
|
200
|
+
|
|
201
|
+
def _make_id(self, name: str, *, kind: DescriptorKind) -> SymbolID:
|
|
202
|
+
descriptors = list(_path_namespaces(self.relative_path))
|
|
203
|
+
descriptors.extend(
|
|
204
|
+
Descriptor(name=cls, kind=DescriptorKind.TYPE) for cls in self._class_stack
|
|
205
|
+
)
|
|
206
|
+
descriptors.append(Descriptor(name=name, kind=kind))
|
|
207
|
+
return SymbolID(scheme=SCHEME, descriptors=tuple(descriptors))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
# Pure helpers
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _path_namespaces(path: PurePosixPath) -> list[Descriptor]:
|
|
216
|
+
return [Descriptor(name=part, kind=DescriptorKind.NAMESPACE) for part in path.parts]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _node_range(node: tree_sitter.Node) -> Range:
|
|
220
|
+
sr, sc = node.start_point
|
|
221
|
+
er, ec = node.end_point
|
|
222
|
+
return Range(
|
|
223
|
+
start_line=sr + 1,
|
|
224
|
+
start_col=sc,
|
|
225
|
+
end_line=max(er + 1, sr + 1),
|
|
226
|
+
end_col=ec,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _node_text(node: tree_sitter.Node) -> str:
|
|
231
|
+
return node.text.decode("utf-8") if node.text is not None else ""
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _name_child(node: tree_sitter.Node) -> str | None:
|
|
235
|
+
name_node = node.child_by_field_name("name")
|
|
236
|
+
if name_node is None or name_node.text is None:
|
|
237
|
+
return None
|
|
238
|
+
text = _node_text(name_node).strip()
|
|
239
|
+
return text or None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _method_signature(
|
|
243
|
+
node: tree_sitter.Node,
|
|
244
|
+
name: str,
|
|
245
|
+
*,
|
|
246
|
+
is_constructor: bool,
|
|
247
|
+
) -> str:
|
|
248
|
+
params = node.child_by_field_name("parameters")
|
|
249
|
+
params_text = _node_text(params) if params is not None else "()"
|
|
250
|
+
if is_constructor:
|
|
251
|
+
return f"{name}{params_text}"
|
|
252
|
+
return_type = node.child_by_field_name("type")
|
|
253
|
+
rt_text = _node_text(return_type) + " " if return_type is not None else ""
|
|
254
|
+
return f"{rt_text}{name}{params_text}"
|
|
File without changes
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Unit tests for the Java indexer plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import textwrap
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
|
|
8
|
+
from codemap_java import JavaIndexer
|
|
9
|
+
from codemap_java.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/Foo.java") -> IndexResult:
|
|
16
|
+
code = textwrap.dedent(source).lstrip("\n")
|
|
17
|
+
return JavaIndexer().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="java",
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Metadata
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_indexer_metadata() -> None:
|
|
34
|
+
ix = JavaIndexer()
|
|
35
|
+
assert ix.name == "java"
|
|
36
|
+
assert ix.languages == ["java"]
|
|
37
|
+
assert "*.java" in ix.file_patterns
|
|
38
|
+
assert ix.supports(Path("a.java"))
|
|
39
|
+
assert not ix.supports(Path("a.kt"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_scheme_is_consistent() -> None:
|
|
43
|
+
r = _index(
|
|
44
|
+
"""
|
|
45
|
+
class A {
|
|
46
|
+
int x;
|
|
47
|
+
void m() {}
|
|
48
|
+
}
|
|
49
|
+
"""
|
|
50
|
+
)
|
|
51
|
+
for s in r.symbols:
|
|
52
|
+
assert str(s.id).startswith(f"{SCHEME} ")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Declarations
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_class_with_method_field_constructor() -> None:
|
|
61
|
+
r = _index(
|
|
62
|
+
"""
|
|
63
|
+
class Greeter {
|
|
64
|
+
String name;
|
|
65
|
+
Greeter(String n) { name = n; }
|
|
66
|
+
String hello() { return name; }
|
|
67
|
+
}
|
|
68
|
+
"""
|
|
69
|
+
)
|
|
70
|
+
kinds = sorted(s.kind for s in r.symbols)
|
|
71
|
+
assert kinds == ["class", "field", "method", "method"]
|
|
72
|
+
method_sigs = sorted(s.signature or "" for s in r.symbols if s.kind == "method")
|
|
73
|
+
assert any("hello" in sig for sig in method_sigs)
|
|
74
|
+
assert any("Greeter" in sig for sig in method_sigs)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_interface_marked_in_extra() -> None:
|
|
78
|
+
r = _index(
|
|
79
|
+
"""
|
|
80
|
+
interface Repo {
|
|
81
|
+
void save();
|
|
82
|
+
}
|
|
83
|
+
"""
|
|
84
|
+
)
|
|
85
|
+
types = [s for s in r.symbols if s.kind == "class"]
|
|
86
|
+
assert len(types) == 1
|
|
87
|
+
assert types[0].extra.get("java_kind") == "interface"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_enum_marked_in_extra() -> None:
|
|
91
|
+
r = _index(
|
|
92
|
+
"""
|
|
93
|
+
enum Color { RED, GREEN, BLUE }
|
|
94
|
+
"""
|
|
95
|
+
)
|
|
96
|
+
types = [s for s in r.symbols if s.kind == "class"]
|
|
97
|
+
assert types[0].extra.get("java_kind") == "enum"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_record_marked_in_extra() -> None:
|
|
101
|
+
r = _index("public record Point(int x, int y) {}")
|
|
102
|
+
types = [s for s in r.symbols if s.kind == "class"]
|
|
103
|
+
assert types[0].extra.get("java_kind") == "record"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_nested_class_namespacing() -> None:
|
|
107
|
+
r = _index(
|
|
108
|
+
"""
|
|
109
|
+
class Outer {
|
|
110
|
+
class Inner {
|
|
111
|
+
void m() {}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
"""
|
|
115
|
+
)
|
|
116
|
+
ids = [str(s.id) for s in r.symbols]
|
|
117
|
+
assert any("Outer#Inner#" in i for i in ids)
|
|
118
|
+
assert any("Outer#Inner#m()." in i for i in ids)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_package_captured_in_extra() -> None:
|
|
122
|
+
r = _index(
|
|
123
|
+
"""
|
|
124
|
+
package com.example;
|
|
125
|
+
class A {}
|
|
126
|
+
"""
|
|
127
|
+
)
|
|
128
|
+
cls = next(s for s in r.symbols if s.kind == "class")
|
|
129
|
+
assert cls.extra.get("package") == "com.example"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_multiple_fields_in_one_decl() -> None:
|
|
133
|
+
r = _index(
|
|
134
|
+
"""
|
|
135
|
+
class A {
|
|
136
|
+
int x, y, z;
|
|
137
|
+
}
|
|
138
|
+
"""
|
|
139
|
+
)
|
|
140
|
+
fields = [s for s in r.symbols if s.kind == "field"]
|
|
141
|
+
assert sorted(s.id.descriptors[-1].name for s in fields) == ["x", "y", "z"]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# SymbolID + edge cases
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_symbol_id_uses_path_namespaces() -> None:
|
|
150
|
+
r = _index("class A {}", path="src/com/foo/A.java")
|
|
151
|
+
assert str(r.symbols[0].id) == "scip-java . . . src/com/foo/A.java/A#"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_empty_file_yields_no_symbols() -> None:
|
|
155
|
+
r = _index("")
|
|
156
|
+
assert r.symbols == []
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_invalid_utf8_yields_diagnostic() -> None:
|
|
160
|
+
ix = JavaIndexer()
|
|
161
|
+
r = ix.index_file(
|
|
162
|
+
Path("bad.java"),
|
|
163
|
+
b"\xff\xfe class",
|
|
164
|
+
IndexContext(
|
|
165
|
+
project_root=Path("/tmp/proj"),
|
|
166
|
+
relative_path=PurePosixPath("bad.java"),
|
|
167
|
+
language="java",
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
assert r.symbols == []
|
|
171
|
+
assert r.diagnostics[0].code == "JAVA002"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_syntax_error_yields_warning() -> None:
|
|
175
|
+
r = _index("class Broken { void m( ")
|
|
176
|
+
codes = {d.code for d in r.diagnostics}
|
|
177
|
+
assert "JAVA001" in codes
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_method_outside_class_is_not_recorded() -> None:
|
|
181
|
+
"""Defensive: top-level identifiers that aren't inside a type don't
|
|
182
|
+
accidentally become methods."""
|
|
183
|
+
r = _index("class A {} class B { void f() {} }")
|
|
184
|
+
methods = [s for s in r.symbols if s.kind == "method"]
|
|
185
|
+
assert len(methods) == 1
|
|
186
|
+
assert "B#f()." in str(methods[0].id)
|