codemap-csharp 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_csharp-0.1.0a1/.gitignore +43 -0
- codemap_csharp-0.1.0a1/PKG-INFO +65 -0
- codemap_csharp-0.1.0a1/README.md +45 -0
- codemap_csharp-0.1.0a1/pyproject.toml +36 -0
- codemap_csharp-0.1.0a1/src/codemap_csharp/__init__.py +8 -0
- codemap_csharp-0.1.0a1/src/codemap_csharp/indexer.py +315 -0
- codemap_csharp-0.1.0a1/tests/__init__.py +0 -0
- codemap_csharp-0.1.0a1/tests/test_indexer.py +181 -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,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codemap-csharp
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: C# language indexer plugin for CodeMap
|
|
5
|
+
Project-URL: Homepage, https://github.com/qxbyte/codemap
|
|
6
|
+
Author: CodeMap Contributors
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: c-sharp,codemap,csharp,dotnet,indexer,tree-sitter
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: C#
|
|
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-c-sharp>=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-csharp
|
|
22
|
+
|
|
23
|
+
> A C# language 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-c-sharp`:
|
|
29
|
+
|
|
30
|
+
| AST node | Symbol kind |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `namespace_declaration` (incl. `qualified_name`) | namespace prefix (recursed into) |
|
|
33
|
+
| `class_declaration` | `class` |
|
|
34
|
+
| `interface_declaration` | `class` (`extra.csharp_kind=interface`) |
|
|
35
|
+
| `struct_declaration` | `class` (`extra.csharp_kind=struct`) |
|
|
36
|
+
| `record_declaration` | `class` (`extra.csharp_kind=record`) |
|
|
37
|
+
| `enum_declaration` | `class` (`extra.csharp_kind=enum`) |
|
|
38
|
+
| `delegate_declaration` | `class` (`extra.csharp_kind=delegate`) |
|
|
39
|
+
| `method_declaration` | `method` |
|
|
40
|
+
| `property_declaration` | `field` (`extra.csharp_kind=property`) |
|
|
41
|
+
| `field_declaration` → `variable_declarator` | `field` |
|
|
42
|
+
| `enum_member_declaration` | `field` |
|
|
43
|
+
|
|
44
|
+
Top-level statements (C# 9+) are not surfaced as named symbols.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install "git+https://github.com/qxbyte/codemap.git#subdirectory=plugins/codemap-csharp"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## File patterns
|
|
53
|
+
|
|
54
|
+
* `*.cs`, `*.csx`
|
|
55
|
+
|
|
56
|
+
## Limits
|
|
57
|
+
|
|
58
|
+
* File-scoped namespaces (``namespace App;``) and block-scoped namespaces
|
|
59
|
+
are both handled, but global usings are not emitted as symbols.
|
|
60
|
+
* Partial classes are emitted as separate symbols per file — the index
|
|
61
|
+
layer does not currently merge them.
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# codemap-csharp
|
|
2
|
+
|
|
3
|
+
> A C# 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-c-sharp`:
|
|
9
|
+
|
|
10
|
+
| AST node | Symbol kind |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `namespace_declaration` (incl. `qualified_name`) | namespace prefix (recursed into) |
|
|
13
|
+
| `class_declaration` | `class` |
|
|
14
|
+
| `interface_declaration` | `class` (`extra.csharp_kind=interface`) |
|
|
15
|
+
| `struct_declaration` | `class` (`extra.csharp_kind=struct`) |
|
|
16
|
+
| `record_declaration` | `class` (`extra.csharp_kind=record`) |
|
|
17
|
+
| `enum_declaration` | `class` (`extra.csharp_kind=enum`) |
|
|
18
|
+
| `delegate_declaration` | `class` (`extra.csharp_kind=delegate`) |
|
|
19
|
+
| `method_declaration` | `method` |
|
|
20
|
+
| `property_declaration` | `field` (`extra.csharp_kind=property`) |
|
|
21
|
+
| `field_declaration` → `variable_declarator` | `field` |
|
|
22
|
+
| `enum_member_declaration` | `field` |
|
|
23
|
+
|
|
24
|
+
Top-level statements (C# 9+) are not surfaced as named symbols.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install "git+https://github.com/qxbyte/codemap.git#subdirectory=plugins/codemap-csharp"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## File patterns
|
|
33
|
+
|
|
34
|
+
* `*.cs`, `*.csx`
|
|
35
|
+
|
|
36
|
+
## Limits
|
|
37
|
+
|
|
38
|
+
* File-scoped namespaces (``namespace App;``) and block-scoped namespaces
|
|
39
|
+
are both handled, but global usings are not emitted as symbols.
|
|
40
|
+
* Partial classes are emitted as separate symbols per file — the index
|
|
41
|
+
layer does not currently merge them.
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "codemap-csharp"
|
|
7
|
+
version = "0.1.0a1"
|
|
8
|
+
description = "C# 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", "csharp", "c-sharp", "dotnet", "indexer", "tree-sitter"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Programming Language :: C#",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Software Development",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"codemap-core>=0.1.0a1,<0.2",
|
|
22
|
+
"tree-sitter>=0.25",
|
|
23
|
+
"tree-sitter-c-sharp>=0.23",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = ["pytest>=8.0"]
|
|
28
|
+
|
|
29
|
+
[project.entry-points."codemap.indexers"]
|
|
30
|
+
csharp = "codemap_csharp:CSharpIndexer"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/qxbyte/codemap"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/codemap_csharp"]
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""C# indexer built on tree-sitter-c-sharp.
|
|
2
|
+
|
|
3
|
+
``namespace_declaration`` chains (including dotted ``A.B.C`` qualifiers)
|
|
4
|
+
are decomposed into a stack of namespace descriptors that prefix each
|
|
5
|
+
contained type's SymbolID.
|
|
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_c_sharp
|
|
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-csharp"
|
|
21
|
+
LANG = "csharp"
|
|
22
|
+
|
|
23
|
+
_CS_LANG = tree_sitter.Language(tree_sitter_c_sharp.language())
|
|
24
|
+
|
|
25
|
+
_TYPE_DECL_KINDS = {
|
|
26
|
+
"class_declaration": (None, "class"),
|
|
27
|
+
"interface_declaration": ("interface", "class"),
|
|
28
|
+
"struct_declaration": ("struct", "class"),
|
|
29
|
+
"record_declaration": ("record", "class"),
|
|
30
|
+
"enum_declaration": ("enum", "class"),
|
|
31
|
+
"delegate_declaration": ("delegate", "class"),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CSharpIndexer:
|
|
36
|
+
name: ClassVar[str] = "csharp"
|
|
37
|
+
version: ClassVar[str] = "0.1.0"
|
|
38
|
+
file_patterns: ClassVar[list[str]] = ["*.cs", "*.csx"]
|
|
39
|
+
languages: ClassVar[list[str]] = [LANG]
|
|
40
|
+
|
|
41
|
+
def supports(self, path: Path) -> bool:
|
|
42
|
+
return path.suffix in {".cs", ".csx"}
|
|
43
|
+
|
|
44
|
+
def index_file(
|
|
45
|
+
self,
|
|
46
|
+
path: Path,
|
|
47
|
+
source: bytes,
|
|
48
|
+
ctx: IndexContext,
|
|
49
|
+
) -> IndexResult:
|
|
50
|
+
try:
|
|
51
|
+
source.decode("utf-8")
|
|
52
|
+
except UnicodeDecodeError as exc:
|
|
53
|
+
return IndexResult(
|
|
54
|
+
diagnostics=[
|
|
55
|
+
Diagnostic(
|
|
56
|
+
severity="error",
|
|
57
|
+
file=ctx.relative_path,
|
|
58
|
+
code="CS002",
|
|
59
|
+
message=f"not valid UTF-8: {exc}",
|
|
60
|
+
producer=self.name,
|
|
61
|
+
)
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
parser = tree_sitter.Parser(_CS_LANG)
|
|
65
|
+
tree = parser.parse(source)
|
|
66
|
+
visitor = _Visitor(ctx.relative_path)
|
|
67
|
+
visitor.visit_root(tree.root_node)
|
|
68
|
+
diagnostics = list(visitor.diagnostics)
|
|
69
|
+
if tree.root_node.has_error:
|
|
70
|
+
diagnostics.append(
|
|
71
|
+
Diagnostic(
|
|
72
|
+
severity="warning",
|
|
73
|
+
file=ctx.relative_path,
|
|
74
|
+
range=Range(start_line=1, end_line=1),
|
|
75
|
+
code="CS001",
|
|
76
|
+
message="tree-sitter reported parse errors; symbols may be incomplete",
|
|
77
|
+
producer=self.name,
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
return IndexResult(
|
|
81
|
+
symbols=visitor.symbols,
|
|
82
|
+
edges=visitor.edges,
|
|
83
|
+
diagnostics=diagnostics,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class _Visitor:
|
|
88
|
+
def __init__(self, relative_path: PurePosixPath) -> None:
|
|
89
|
+
self.relative_path = relative_path
|
|
90
|
+
self.symbols: list[Symbol] = []
|
|
91
|
+
self.edges: list[Edge] = []
|
|
92
|
+
self.diagnostics: list[Diagnostic] = []
|
|
93
|
+
|
|
94
|
+
def visit_root(self, root: tree_sitter.Node) -> None:
|
|
95
|
+
for child in root.children:
|
|
96
|
+
self._visit(child, 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 == "namespace_declaration":
|
|
106
|
+
self._visit_namespace(node, scope=scope)
|
|
107
|
+
elif kind in _TYPE_DECL_KINDS:
|
|
108
|
+
csharp_kind, symbol_kind = _TYPE_DECL_KINDS[kind]
|
|
109
|
+
self._emit_type(
|
|
110
|
+
node,
|
|
111
|
+
scope=scope,
|
|
112
|
+
csharp_kind=csharp_kind,
|
|
113
|
+
symbol_kind=symbol_kind,
|
|
114
|
+
)
|
|
115
|
+
elif kind == "method_declaration":
|
|
116
|
+
self._emit_method(node, scope=scope)
|
|
117
|
+
elif kind == "property_declaration":
|
|
118
|
+
self._emit_property(node, scope=scope)
|
|
119
|
+
elif kind == "field_declaration":
|
|
120
|
+
self._emit_field_declaration(node, scope=scope)
|
|
121
|
+
elif kind == "enum_member_declaration":
|
|
122
|
+
self._emit_enum_member(node, scope=scope)
|
|
123
|
+
|
|
124
|
+
def _visit_namespace(
|
|
125
|
+
self,
|
|
126
|
+
node: tree_sitter.Node,
|
|
127
|
+
*,
|
|
128
|
+
scope: list[Descriptor],
|
|
129
|
+
) -> None:
|
|
130
|
+
new_scope = list(scope)
|
|
131
|
+
for child in node.children:
|
|
132
|
+
if child.type == "identifier":
|
|
133
|
+
new_scope.append(Descriptor(name=_node_text(child), kind=DescriptorKind.NAMESPACE))
|
|
134
|
+
elif child.type == "qualified_name":
|
|
135
|
+
new_scope.extend(
|
|
136
|
+
Descriptor(name=part, kind=DescriptorKind.NAMESPACE)
|
|
137
|
+
for part in _qualified_name_parts(child)
|
|
138
|
+
)
|
|
139
|
+
body = _first_child(node, "declaration_list")
|
|
140
|
+
if body is None:
|
|
141
|
+
return
|
|
142
|
+
for child in body.children:
|
|
143
|
+
self._visit(child, scope=new_scope)
|
|
144
|
+
|
|
145
|
+
def _emit_type(
|
|
146
|
+
self,
|
|
147
|
+
node: tree_sitter.Node,
|
|
148
|
+
*,
|
|
149
|
+
scope: list[Descriptor],
|
|
150
|
+
csharp_kind: str | None,
|
|
151
|
+
symbol_kind: str,
|
|
152
|
+
) -> None:
|
|
153
|
+
name = _first_child_text(node, "identifier")
|
|
154
|
+
if name is None:
|
|
155
|
+
return
|
|
156
|
+
type_desc = Descriptor(name=name, kind=DescriptorKind.TYPE)
|
|
157
|
+
descriptors = [*list(scope), type_desc]
|
|
158
|
+
extra: dict[str, str] = {}
|
|
159
|
+
if csharp_kind is not None:
|
|
160
|
+
extra["csharp_kind"] = csharp_kind
|
|
161
|
+
self.symbols.append(
|
|
162
|
+
Symbol(
|
|
163
|
+
id=self._make_id(descriptors),
|
|
164
|
+
kind=symbol_kind,
|
|
165
|
+
language=LANG,
|
|
166
|
+
file=self.relative_path,
|
|
167
|
+
range=_node_range(node),
|
|
168
|
+
extra=extra,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
# Recurse into nested members.
|
|
172
|
+
body = _first_child(node, "declaration_list") or _first_child(
|
|
173
|
+
node, "enum_member_declaration_list"
|
|
174
|
+
)
|
|
175
|
+
if body is None:
|
|
176
|
+
return
|
|
177
|
+
for child in body.children:
|
|
178
|
+
self._visit(child, scope=descriptors)
|
|
179
|
+
|
|
180
|
+
def _emit_method(
|
|
181
|
+
self,
|
|
182
|
+
node: tree_sitter.Node,
|
|
183
|
+
*,
|
|
184
|
+
scope: list[Descriptor],
|
|
185
|
+
) -> None:
|
|
186
|
+
name = _first_child_text(node, "identifier")
|
|
187
|
+
if name is None:
|
|
188
|
+
return
|
|
189
|
+
self.symbols.append(
|
|
190
|
+
Symbol(
|
|
191
|
+
id=self._make_id([*list(scope), Descriptor(name=name, kind=DescriptorKind.METHOD)]),
|
|
192
|
+
kind="method",
|
|
193
|
+
language=LANG,
|
|
194
|
+
file=self.relative_path,
|
|
195
|
+
range=_node_range(node),
|
|
196
|
+
signature=f"{name}()",
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _emit_property(
|
|
201
|
+
self,
|
|
202
|
+
node: tree_sitter.Node,
|
|
203
|
+
*,
|
|
204
|
+
scope: list[Descriptor],
|
|
205
|
+
) -> None:
|
|
206
|
+
name = _first_child_text(node, "identifier")
|
|
207
|
+
if name is None:
|
|
208
|
+
return
|
|
209
|
+
self.symbols.append(
|
|
210
|
+
Symbol(
|
|
211
|
+
id=self._make_id([*list(scope), Descriptor(name=name, kind=DescriptorKind.TERM)]),
|
|
212
|
+
kind="field",
|
|
213
|
+
language=LANG,
|
|
214
|
+
file=self.relative_path,
|
|
215
|
+
range=_node_range(node),
|
|
216
|
+
extra={"csharp_kind": "property"},
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def _emit_field_declaration(
|
|
221
|
+
self,
|
|
222
|
+
node: tree_sitter.Node,
|
|
223
|
+
*,
|
|
224
|
+
scope: list[Descriptor],
|
|
225
|
+
) -> None:
|
|
226
|
+
var_decl = _first_child(node, "variable_declaration")
|
|
227
|
+
if var_decl is None:
|
|
228
|
+
return
|
|
229
|
+
for child in var_decl.children:
|
|
230
|
+
if child.type == "variable_declarator":
|
|
231
|
+
name = _first_child_text(child, "identifier")
|
|
232
|
+
if name is None:
|
|
233
|
+
continue
|
|
234
|
+
self.symbols.append(
|
|
235
|
+
Symbol(
|
|
236
|
+
id=self._make_id(
|
|
237
|
+
[*list(scope), Descriptor(name=name, kind=DescriptorKind.TERM)]
|
|
238
|
+
),
|
|
239
|
+
kind="field",
|
|
240
|
+
language=LANG,
|
|
241
|
+
file=self.relative_path,
|
|
242
|
+
range=_node_range(node),
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def _emit_enum_member(
|
|
247
|
+
self,
|
|
248
|
+
node: tree_sitter.Node,
|
|
249
|
+
*,
|
|
250
|
+
scope: list[Descriptor],
|
|
251
|
+
) -> None:
|
|
252
|
+
name = _first_child_text(node, "identifier")
|
|
253
|
+
if name is None:
|
|
254
|
+
return
|
|
255
|
+
self.symbols.append(
|
|
256
|
+
Symbol(
|
|
257
|
+
id=self._make_id([*list(scope), Descriptor(name=name, kind=DescriptorKind.TERM)]),
|
|
258
|
+
kind="field",
|
|
259
|
+
language=LANG,
|
|
260
|
+
file=self.relative_path,
|
|
261
|
+
range=_node_range(node),
|
|
262
|
+
extra={"csharp_kind": "enum_member"},
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def _make_id(self, descriptors: list[Descriptor]) -> SymbolID:
|
|
267
|
+
full = list(_path_namespaces(self.relative_path))
|
|
268
|
+
full.extend(descriptors)
|
|
269
|
+
return SymbolID(scheme=SCHEME, descriptors=tuple(full))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ---------------------------------------------------------------------------
|
|
273
|
+
# Pure helpers
|
|
274
|
+
# ---------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _path_namespaces(path: PurePosixPath) -> list[Descriptor]:
|
|
278
|
+
return [Descriptor(name=part, kind=DescriptorKind.NAMESPACE) for part in path.parts]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _node_range(node: tree_sitter.Node) -> Range:
|
|
282
|
+
sr, sc = node.start_point
|
|
283
|
+
er, ec = node.end_point
|
|
284
|
+
return Range(
|
|
285
|
+
start_line=sr + 1,
|
|
286
|
+
start_col=sc,
|
|
287
|
+
end_line=max(er + 1, sr + 1),
|
|
288
|
+
end_col=ec,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _node_text(node: tree_sitter.Node) -> str:
|
|
293
|
+
return node.text.decode("utf-8") if node.text is not None else ""
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _first_child(node: tree_sitter.Node, kind: str) -> tree_sitter.Node | None:
|
|
297
|
+
for child in node.children:
|
|
298
|
+
if child.type == kind:
|
|
299
|
+
return child
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _first_child_text(node: tree_sitter.Node, kind: str) -> str | None:
|
|
304
|
+
found = _first_child(node, kind)
|
|
305
|
+
return _node_text(found) if found is not None else None
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _qualified_name_parts(node: tree_sitter.Node) -> list[str]:
|
|
309
|
+
parts: list[str] = []
|
|
310
|
+
for child in node.children:
|
|
311
|
+
if child.type == "identifier":
|
|
312
|
+
parts.append(_node_text(child))
|
|
313
|
+
elif child.type == "qualified_name":
|
|
314
|
+
parts.extend(_qualified_name_parts(child))
|
|
315
|
+
return parts
|
|
File without changes
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Unit tests for the C# indexer plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import textwrap
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
|
|
8
|
+
from codemap_csharp import CSharpIndexer
|
|
9
|
+
from codemap_csharp.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.cs") -> IndexResult:
|
|
16
|
+
code = textwrap.dedent(source).lstrip("\n")
|
|
17
|
+
return CSharpIndexer().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="csharp",
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_indexer_metadata() -> None:
|
|
29
|
+
ix = CSharpIndexer()
|
|
30
|
+
assert ix.name == "csharp"
|
|
31
|
+
assert ix.languages == ["csharp"]
|
|
32
|
+
assert ix.supports(Path("a.cs"))
|
|
33
|
+
assert ix.supports(Path("a.csx"))
|
|
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_simple_class_with_method_field_property() -> None:
|
|
44
|
+
r = _index(
|
|
45
|
+
"""
|
|
46
|
+
class Foo {
|
|
47
|
+
public int Counter;
|
|
48
|
+
public string Name { get; set; }
|
|
49
|
+
public void Bar() {}
|
|
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
|
+
field_names = {f.id.descriptors[-1].name for f in fields}
|
|
59
|
+
assert {"Counter", "Name"} <= field_names
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_interface_struct_record_enum_delegate() -> None:
|
|
63
|
+
r = _index(
|
|
64
|
+
"""
|
|
65
|
+
public interface IThing { void Do(); }
|
|
66
|
+
public struct Point { public int X; }
|
|
67
|
+
public record Person(string Name);
|
|
68
|
+
public enum Color { Red, Green }
|
|
69
|
+
public delegate void Handler();
|
|
70
|
+
"""
|
|
71
|
+
)
|
|
72
|
+
by_kind: dict[str, str] = {}
|
|
73
|
+
for s in r.symbols:
|
|
74
|
+
if s.kind == "class":
|
|
75
|
+
by_kind[s.id.descriptors[-1].name] = s.extra.get("csharp_kind", "class")
|
|
76
|
+
assert by_kind["IThing"] == "interface"
|
|
77
|
+
assert by_kind["Point"] == "struct"
|
|
78
|
+
assert by_kind["Person"] == "record"
|
|
79
|
+
assert by_kind["Color"] == "enum"
|
|
80
|
+
assert by_kind["Handler"] == "delegate"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_enum_members_emitted_as_fields() -> None:
|
|
84
|
+
r = _index("enum Color { Red, Green, Blue }")
|
|
85
|
+
fields = [s for s in r.symbols if s.kind == "field"]
|
|
86
|
+
assert {f.id.descriptors[-1].name for f in fields} == {"Red", "Green", "Blue"}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_block_namespace_prefixes_descriptors() -> None:
|
|
90
|
+
r = _index(
|
|
91
|
+
"""
|
|
92
|
+
namespace App {
|
|
93
|
+
class Foo {}
|
|
94
|
+
}
|
|
95
|
+
"""
|
|
96
|
+
)
|
|
97
|
+
foo = next(s for s in r.symbols if s.kind == "class")
|
|
98
|
+
names = [d.name for d in foo.id.descriptors]
|
|
99
|
+
assert "App" in names
|
|
100
|
+
assert "Foo" in names
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_qualified_namespace_is_split() -> None:
|
|
104
|
+
r = _index(
|
|
105
|
+
"""
|
|
106
|
+
namespace App.Core.Util {
|
|
107
|
+
class Helper {}
|
|
108
|
+
}
|
|
109
|
+
"""
|
|
110
|
+
)
|
|
111
|
+
helper = next(s for s in r.symbols if s.kind == "class")
|
|
112
|
+
names = [d.name for d in helper.id.descriptors]
|
|
113
|
+
# The three dotted components should each appear as a NAMESPACE descriptor.
|
|
114
|
+
assert names.index("App") < names.index("Core") < names.index("Util") < names.index("Helper")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_nested_class_descriptors() -> None:
|
|
118
|
+
r = _index(
|
|
119
|
+
"""
|
|
120
|
+
class Outer {
|
|
121
|
+
class Inner {
|
|
122
|
+
public int Field;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
"""
|
|
126
|
+
)
|
|
127
|
+
inner = next(s for s in r.symbols if s.kind == "class" and s.id.descriptors[-1].name == "Inner")
|
|
128
|
+
names = [d.name for d in inner.id.descriptors]
|
|
129
|
+
assert names.index("Outer") < names.index("Inner")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_multiple_field_declarators_on_one_line() -> None:
|
|
133
|
+
r = _index(
|
|
134
|
+
"""
|
|
135
|
+
class C {
|
|
136
|
+
public int A, B, Cee;
|
|
137
|
+
}
|
|
138
|
+
"""
|
|
139
|
+
)
|
|
140
|
+
fields = [s for s in r.symbols if s.kind == "field"]
|
|
141
|
+
assert {f.id.descriptors[-1].name for f in fields} == {"A", "B", "Cee"}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_property_marked_with_kind() -> None:
|
|
145
|
+
r = _index(
|
|
146
|
+
"""
|
|
147
|
+
class C {
|
|
148
|
+
public string Name { get; set; }
|
|
149
|
+
}
|
|
150
|
+
"""
|
|
151
|
+
)
|
|
152
|
+
name_prop = next(
|
|
153
|
+
s for s in r.symbols if s.kind == "field" and s.id.descriptors[-1].name == "Name"
|
|
154
|
+
)
|
|
155
|
+
assert name_prop.extra.get("csharp_kind") == "property"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_symbol_id_uses_path_namespaces() -> None:
|
|
159
|
+
r = _index("class C {}", path="src/Util/Helpers.cs")
|
|
160
|
+
cls = next(s for s in r.symbols if s.kind == "class")
|
|
161
|
+
assert str(cls.id) == "scip-csharp . . . src/Util/Helpers.cs/C#"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_empty_file_yields_no_symbols() -> None:
|
|
165
|
+
r = _index("")
|
|
166
|
+
assert r.symbols == []
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_invalid_utf8_yields_diagnostic() -> None:
|
|
170
|
+
ix = CSharpIndexer()
|
|
171
|
+
r = ix.index_file(
|
|
172
|
+
Path("Bad.cs"),
|
|
173
|
+
b"\xff\xfe class",
|
|
174
|
+
IndexContext(
|
|
175
|
+
project_root=Path("/tmp/proj"),
|
|
176
|
+
relative_path=PurePosixPath("Bad.cs"),
|
|
177
|
+
language="csharp",
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
assert r.symbols == []
|
|
181
|
+
assert r.diagnostics[0].code == "CS002"
|