codemap-kotlin 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.
@@ -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-kotlin
3
+ Version: 0.1.0a1
4
+ Summary: Kotlin indexer plugin for CodeMap
5
+ Project-URL: Homepage, https://github.com/qxbyte/codemap
6
+ Author: CodeMap Contributors
7
+ License: MIT
8
+ Keywords: codemap,indexer,kotlin,tree-sitter
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Programming Language :: Kotlin
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-kotlin>=1.1
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-kotlin
22
+
23
+ > A Kotlin indexer for [CodeMap](https://github.com/qxbyte/codemap),
24
+ > shipped as an independent PyPI package.
25
+
26
+ ## What it captures
27
+
28
+ Backed by `tree-sitter-kotlin`:
29
+
30
+ | AST node | Symbol kind |
31
+ |---|---|
32
+ | `class_declaration` (keyword `class`) | `class` (with `extra.kotlin_kind=class`) |
33
+ | `class_declaration` (keyword `interface`) | `class` (with `extra.kotlin_kind=interface`) |
34
+ | `object_declaration` | `class` (with `extra.kotlin_kind=object`) |
35
+ | `function_declaration` (free) | `function` |
36
+ | `function_declaration` (inside type) | `method` |
37
+ | `property_declaration` (top-level) | `variable` |
38
+ | `property_declaration` (inside type) | `field` |
39
+
40
+ `package_header` is captured as `extra.package` on type symbols.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pip install "git+https://github.com/qxbyte/codemap.git#subdirectory=plugins/codemap-kotlin"
46
+ ```
47
+
48
+ ## SymbolID encoding
49
+
50
+ ```
51
+ scip-kotlin . . . src/main/kotlin/User.kt/User#hello().
52
+ ```
53
+
54
+ ## Limits
55
+
56
+ * Companion object members are not attached to the enclosing class.
57
+ * Generic-parameter descriptors are dropped.
58
+
59
+ ## License
60
+
61
+ MIT.
@@ -0,0 +1,41 @@
1
+ # codemap-kotlin
2
+
3
+ > A Kotlin 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-kotlin`:
9
+
10
+ | AST node | Symbol kind |
11
+ |---|---|
12
+ | `class_declaration` (keyword `class`) | `class` (with `extra.kotlin_kind=class`) |
13
+ | `class_declaration` (keyword `interface`) | `class` (with `extra.kotlin_kind=interface`) |
14
+ | `object_declaration` | `class` (with `extra.kotlin_kind=object`) |
15
+ | `function_declaration` (free) | `function` |
16
+ | `function_declaration` (inside type) | `method` |
17
+ | `property_declaration` (top-level) | `variable` |
18
+ | `property_declaration` (inside type) | `field` |
19
+
20
+ `package_header` is captured as `extra.package` on type symbols.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install "git+https://github.com/qxbyte/codemap.git#subdirectory=plugins/codemap-kotlin"
26
+ ```
27
+
28
+ ## SymbolID encoding
29
+
30
+ ```
31
+ scip-kotlin . . . src/main/kotlin/User.kt/User#hello().
32
+ ```
33
+
34
+ ## Limits
35
+
36
+ * Companion object members are not attached to the enclosing class.
37
+ * Generic-parameter descriptors are dropped.
38
+
39
+ ## License
40
+
41
+ MIT.
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "codemap-kotlin"
7
+ version = "0.1.0a1"
8
+ description = "Kotlin 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", "kotlin", "indexer", "tree-sitter"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Kotlin",
18
+ "Topic :: Software Development",
19
+ ]
20
+ dependencies = [
21
+ "codemap-core>=0.1.0a1,<0.2",
22
+ "tree-sitter>=0.25",
23
+ "tree-sitter-kotlin>=1.1",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ dev = ["pytest>=8.0"]
28
+
29
+ [project.entry-points."codemap.indexers"]
30
+ kotlin = "codemap_kotlin:KotlinIndexer"
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/qxbyte/codemap"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/codemap_kotlin"]
@@ -0,0 +1,8 @@
1
+ """Kotlin indexer plugin for CodeMap."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from codemap_kotlin.indexer import KotlinIndexer
6
+
7
+ __all__ = ["KotlinIndexer"]
8
+ __version__ = "0.1.0"
@@ -0,0 +1,246 @@
1
+ """Kotlin indexer built on tree-sitter-kotlin.
2
+
3
+ The Kotlin grammar uses ``class_declaration`` for both ``class`` and
4
+ ``interface``. The keyword token disambiguates. ``object_declaration``
5
+ is its own node type for singleton objects.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path, PurePosixPath
11
+ from typing import ClassVar
12
+
13
+ import tree_sitter
14
+ import tree_sitter_kotlin
15
+
16
+ from codemap.core.models import Diagnostic, Edge, IndexResult, Range, Symbol
17
+ from codemap.core.symbol import Descriptor, DescriptorKind, SymbolID
18
+ from codemap.indexers.base import IndexContext
19
+
20
+ SCHEME = "scip-kotlin"
21
+ LANG = "kotlin"
22
+
23
+ _KT_LANG = tree_sitter.Language(tree_sitter_kotlin.language())
24
+
25
+
26
+ class KotlinIndexer:
27
+ name: ClassVar[str] = "kotlin"
28
+ version: ClassVar[str] = "0.1.0"
29
+ file_patterns: ClassVar[list[str]] = ["*.kt", "*.kts"]
30
+ languages: ClassVar[list[str]] = [LANG]
31
+
32
+ def supports(self, path: Path) -> bool:
33
+ return path.suffix in {".kt", ".kts"}
34
+
35
+ def index_file(
36
+ self,
37
+ path: Path,
38
+ source: bytes,
39
+ ctx: IndexContext,
40
+ ) -> IndexResult:
41
+ try:
42
+ source.decode("utf-8")
43
+ except UnicodeDecodeError as exc:
44
+ return IndexResult(
45
+ diagnostics=[
46
+ Diagnostic(
47
+ severity="error",
48
+ file=ctx.relative_path,
49
+ code="KT002",
50
+ message=f"not valid UTF-8: {exc}",
51
+ producer=self.name,
52
+ )
53
+ ]
54
+ )
55
+ parser = tree_sitter.Parser(_KT_LANG)
56
+ tree = parser.parse(source)
57
+ visitor = _Visitor(ctx.relative_path)
58
+ visitor.visit(tree.root_node)
59
+ diagnostics = list(visitor.diagnostics)
60
+ if tree.root_node.has_error:
61
+ diagnostics.append(
62
+ Diagnostic(
63
+ severity="warning",
64
+ file=ctx.relative_path,
65
+ range=Range(start_line=1, end_line=1),
66
+ code="KT001",
67
+ message="tree-sitter reported parse errors; symbols may be incomplete",
68
+ producer=self.name,
69
+ )
70
+ )
71
+ return IndexResult(
72
+ symbols=visitor.symbols,
73
+ edges=visitor.edges,
74
+ diagnostics=diagnostics,
75
+ )
76
+
77
+
78
+ class _Visitor:
79
+ def __init__(self, relative_path: PurePosixPath) -> None:
80
+ self.relative_path = relative_path
81
+ self.symbols: list[Symbol] = []
82
+ self.edges: list[Edge] = []
83
+ self.diagnostics: list[Diagnostic] = []
84
+ self._type_stack: list[str] = []
85
+ self._package: str = ""
86
+
87
+ def visit(self, node: tree_sitter.Node) -> None:
88
+ kind = node.type
89
+ if kind == "package_header":
90
+ self._set_package(node)
91
+ return
92
+ if kind == "class_declaration":
93
+ self._visit_class_or_interface(node)
94
+ return
95
+ if kind == "object_declaration":
96
+ self._visit_object(node)
97
+ return
98
+ if kind == "function_declaration":
99
+ self._visit_function(node)
100
+ return
101
+ if kind == "property_declaration":
102
+ self._visit_property(node)
103
+ return
104
+ for child in node.children:
105
+ self.visit(child)
106
+
107
+ # ----------------------------------------------------- packages
108
+
109
+ def _set_package(self, node: tree_sitter.Node) -> None:
110
+ for child in node.children:
111
+ if child.type == "qualified_identifier":
112
+ self._package = _node_text(child)
113
+ return
114
+ if child.type == "identifier":
115
+ self._package = _node_text(child)
116
+ return
117
+
118
+ # --------------------------------------------------- type-level
119
+
120
+ def _visit_class_or_interface(self, node: tree_sitter.Node) -> None:
121
+ kotlin_kind = "class"
122
+ for child in node.children:
123
+ if child.type == "interface":
124
+ kotlin_kind = "interface"
125
+ break
126
+ self._visit_type(node, kotlin_kind=kotlin_kind)
127
+
128
+ def _visit_object(self, node: tree_sitter.Node) -> None:
129
+ self._visit_type(node, kotlin_kind="object")
130
+
131
+ def _visit_type(self, node: tree_sitter.Node, *, kotlin_kind: str) -> None:
132
+ name = _first_identifier(node)
133
+ if name is None:
134
+ return
135
+ sid = self._make_id(name, kind=DescriptorKind.TYPE)
136
+ extra: dict[str, str] = {"kotlin_kind": kotlin_kind}
137
+ if self._package:
138
+ extra["package"] = self._package
139
+ self.symbols.append(
140
+ Symbol(
141
+ id=sid,
142
+ kind="class",
143
+ language=LANG,
144
+ file=self.relative_path,
145
+ range=_node_range(node),
146
+ extra=extra,
147
+ )
148
+ )
149
+ body = _find_class_body(node)
150
+ if body is None:
151
+ return
152
+ self._type_stack.append(name)
153
+ try:
154
+ for child in body.children:
155
+ self.visit(child)
156
+ finally:
157
+ self._type_stack.pop()
158
+
159
+ # ----------------------------------------------------- functions
160
+
161
+ def _visit_function(self, node: tree_sitter.Node) -> None:
162
+ name = _first_identifier(node)
163
+ if name is None:
164
+ return
165
+ kind: str = "method" if self._type_stack else "function"
166
+ sid = self._make_id(name, kind=DescriptorKind.METHOD)
167
+ self.symbols.append(
168
+ Symbol(
169
+ id=sid,
170
+ kind=kind, # type: ignore[arg-type]
171
+ language=LANG,
172
+ file=self.relative_path,
173
+ range=_node_range(node),
174
+ signature=f"fun {name}()",
175
+ )
176
+ )
177
+
178
+ # ---------------------------------------------------- properties
179
+
180
+ def _visit_property(self, node: tree_sitter.Node) -> None:
181
+ # property_declaration > variable_declaration > identifier
182
+ for child in node.children:
183
+ if child.type == "variable_declaration":
184
+ for grand in child.children:
185
+ if grand.type == "identifier":
186
+ name = _node_text(grand)
187
+ if not name:
188
+ return
189
+ sym_kind: str = "field" if self._type_stack else "variable"
190
+ sid = self._make_id(name, kind=DescriptorKind.TERM)
191
+ self.symbols.append(
192
+ Symbol(
193
+ id=sid,
194
+ kind=sym_kind, # type: ignore[arg-type]
195
+ language=LANG,
196
+ file=self.relative_path,
197
+ range=_node_range(node),
198
+ )
199
+ )
200
+ return
201
+
202
+ # -------------------------------------------------------- helpers
203
+
204
+ def _make_id(self, name: str, *, kind: DescriptorKind) -> SymbolID:
205
+ descriptors = list(_path_namespaces(self.relative_path))
206
+ descriptors.extend(Descriptor(name=t, kind=DescriptorKind.TYPE) for t in self._type_stack)
207
+ descriptors.append(Descriptor(name=name, kind=kind))
208
+ return SymbolID(scheme=SCHEME, descriptors=tuple(descriptors))
209
+
210
+
211
+ # ---------------------------------------------------------------------------
212
+ # Pure helpers
213
+ # ---------------------------------------------------------------------------
214
+
215
+
216
+ def _path_namespaces(path: PurePosixPath) -> list[Descriptor]:
217
+ return [Descriptor(name=part, kind=DescriptorKind.NAMESPACE) for part in path.parts]
218
+
219
+
220
+ def _node_range(node: tree_sitter.Node) -> Range:
221
+ sr, sc = node.start_point
222
+ er, ec = node.end_point
223
+ return Range(
224
+ start_line=sr + 1,
225
+ start_col=sc,
226
+ end_line=max(er + 1, sr + 1),
227
+ end_col=ec,
228
+ )
229
+
230
+
231
+ def _node_text(node: tree_sitter.Node) -> str:
232
+ return node.text.decode("utf-8") if node.text is not None else ""
233
+
234
+
235
+ def _first_identifier(node: tree_sitter.Node) -> str | None:
236
+ for child in node.children:
237
+ if child.type == "identifier":
238
+ return _node_text(child)
239
+ return None
240
+
241
+
242
+ def _find_class_body(node: tree_sitter.Node) -> tree_sitter.Node | None:
243
+ for child in node.children:
244
+ if child.type == "class_body":
245
+ return child
246
+ return None
File without changes
@@ -0,0 +1,154 @@
1
+ """Unit tests for the Kotlin indexer plugin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import textwrap
6
+ from pathlib import Path, PurePosixPath
7
+
8
+ from codemap_kotlin import KotlinIndexer
9
+ from codemap_kotlin.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/Main.kt") -> IndexResult:
16
+ code = textwrap.dedent(source).lstrip("\n")
17
+ return KotlinIndexer().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="kotlin",
24
+ ),
25
+ )
26
+
27
+
28
+ def test_indexer_metadata() -> None:
29
+ ix = KotlinIndexer()
30
+ assert ix.name == "kotlin"
31
+ assert ix.languages == ["kotlin"]
32
+ assert ix.supports(Path("a.kt"))
33
+ assert ix.supports(Path("a.kts"))
34
+ assert not ix.supports(Path("a.java"))
35
+
36
+
37
+ def test_scheme_is_consistent() -> None:
38
+ r = _index("class A\nfun f() {}\nval X = 1\n")
39
+ for s in r.symbols:
40
+ assert str(s.id).startswith(f"{SCHEME} ")
41
+
42
+
43
+ def test_class_declaration() -> None:
44
+ r = _index(
45
+ """
46
+ class User(val name: String) {
47
+ fun hello(): String = name
48
+ }
49
+ """
50
+ )
51
+ classes = [s for s in r.symbols if s.kind == "class"]
52
+ assert len(classes) == 1
53
+ assert classes[0].extra.get("kotlin_kind") == "class"
54
+ methods = [s for s in r.symbols if s.kind == "method"]
55
+ assert any("User#hello()." in str(m.id) for m in methods)
56
+
57
+
58
+ def test_interface_kept_as_class_with_extra() -> None:
59
+ r = _index(
60
+ """
61
+ interface Greeter {
62
+ fun hello(): String
63
+ }
64
+ """
65
+ )
66
+ cls = next(s for s in r.symbols if s.kind == "class")
67
+ assert cls.extra.get("kotlin_kind") == "interface"
68
+
69
+
70
+ def test_object_kept_as_class_with_extra() -> None:
71
+ r = _index(
72
+ """
73
+ object Singleton {
74
+ fun greet() = "hi"
75
+ }
76
+ """
77
+ )
78
+ cls = next(s for s in r.symbols if s.kind == "class")
79
+ assert cls.extra.get("kotlin_kind") == "object"
80
+
81
+
82
+ def test_package_captured_in_extra() -> None:
83
+ r = _index(
84
+ """
85
+ package com.example
86
+ class A
87
+ """
88
+ )
89
+ cls = next(s for s in r.symbols if s.kind == "class")
90
+ assert cls.extra.get("package") == "com.example"
91
+
92
+
93
+ def test_free_function() -> None:
94
+ r = _index("fun helper(x: Int): Int = x + 1")
95
+ funcs = [s for s in r.symbols if s.kind == "function"]
96
+ assert len(funcs) == 1
97
+ assert "helper" in str(funcs[0].id)
98
+
99
+
100
+ def test_top_level_val_is_variable() -> None:
101
+ r = _index("val MAX = 10")
102
+ vars_ = [s for s in r.symbols if s.kind == "variable"]
103
+ assert len(vars_) == 1
104
+
105
+
106
+ def test_top_level_var_is_variable() -> None:
107
+ r = _index("var counter = 0")
108
+ vars_ = [s for s in r.symbols if s.kind == "variable"]
109
+ assert len(vars_) == 1
110
+
111
+
112
+ def test_method_inside_class_uses_method_kind() -> None:
113
+ r = _index(
114
+ """
115
+ class User {
116
+ fun hello() = ""
117
+ }
118
+ """
119
+ )
120
+ methods = [s for s in r.symbols if s.kind == "method"]
121
+ funcs = [s for s in r.symbols if s.kind == "function"]
122
+ assert len(methods) == 1
123
+ assert len(funcs) == 0
124
+
125
+
126
+ def test_symbol_id_uses_path_namespaces() -> None:
127
+ r = _index("class A", path="src/main/kotlin/A.kt")
128
+ cls = next(s for s in r.symbols if s.kind == "class")
129
+ assert str(cls.id) == "scip-kotlin . . . src/main/kotlin/A.kt/A#"
130
+
131
+
132
+ def test_empty_file_yields_no_symbols() -> None:
133
+ r = _index("")
134
+ assert r.symbols == []
135
+
136
+
137
+ def test_invalid_utf8_yields_diagnostic() -> None:
138
+ ix = KotlinIndexer()
139
+ r = ix.index_file(
140
+ Path("bad.kt"),
141
+ b"\xff\xfe class",
142
+ IndexContext(
143
+ project_root=Path("/tmp/proj"),
144
+ relative_path=PurePosixPath("bad.kt"),
145
+ language="kotlin",
146
+ ),
147
+ )
148
+ assert r.symbols == []
149
+ assert r.diagnostics[0].code == "KT002"
150
+
151
+
152
+ def test_kts_extension_supported() -> None:
153
+ r = _index("fun main() {}", path="build.kts")
154
+ assert any(s.kind == "function" for s in r.symbols)