codemap-php 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_php-0.1.0a1/.gitignore +43 -0
- codemap_php-0.1.0a1/PKG-INFO +65 -0
- codemap_php-0.1.0a1/README.md +45 -0
- codemap_php-0.1.0a1/pyproject.toml +36 -0
- codemap_php-0.1.0a1/src/codemap_php/__init__.py +8 -0
- codemap_php-0.1.0a1/src/codemap_php/indexer.py +293 -0
- codemap_php-0.1.0a1/tests/__init__.py +0 -0
- codemap_php-0.1.0a1/tests/test_indexer.py +169 -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-php
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: PHP indexer plugin for CodeMap
|
|
5
|
+
Project-URL: Homepage, https://github.com/qxbyte/codemap
|
|
6
|
+
Author: CodeMap Contributors
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: codemap,indexer,php,tree-sitter
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: PHP
|
|
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-php>=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-php
|
|
22
|
+
|
|
23
|
+
> A PHP 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-php`:
|
|
29
|
+
|
|
30
|
+
| AST node | Symbol kind |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `class_declaration` | `class` (with `extra.php_kind=class`) |
|
|
33
|
+
| `interface_declaration` | `class` (with `extra.php_kind=interface`) |
|
|
34
|
+
| `trait_declaration` | `class` (with `extra.php_kind=trait`) |
|
|
35
|
+
| `enum_declaration` | `class` (with `extra.php_kind=enum`) |
|
|
36
|
+
| `method_declaration` (inside type) | `method` |
|
|
37
|
+
| `function_definition` (top level) | `function` |
|
|
38
|
+
| `property_declaration` (inside type) | `field` |
|
|
39
|
+
| `const_declaration` (top level) | `variable` |
|
|
40
|
+
| `const_declaration` (inside type) | `field` |
|
|
41
|
+
|
|
42
|
+
`namespace_definition` is captured as `extra.namespace` on every
|
|
43
|
+
symbol-producing type.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install "git+https://github.com/qxbyte/codemap.git#subdirectory=plugins/codemap-php"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## SymbolID encoding
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
scip-php . . . src/App/User.php/User#hello().
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Limits
|
|
58
|
+
|
|
59
|
+
* Use statements aren't yet expanded into namespace-resolved edges.
|
|
60
|
+
* PHPDoc annotations are not parsed.
|
|
61
|
+
* Anonymous classes are skipped.
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# codemap-php
|
|
2
|
+
|
|
3
|
+
> A PHP 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-php`:
|
|
9
|
+
|
|
10
|
+
| AST node | Symbol kind |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `class_declaration` | `class` (with `extra.php_kind=class`) |
|
|
13
|
+
| `interface_declaration` | `class` (with `extra.php_kind=interface`) |
|
|
14
|
+
| `trait_declaration` | `class` (with `extra.php_kind=trait`) |
|
|
15
|
+
| `enum_declaration` | `class` (with `extra.php_kind=enum`) |
|
|
16
|
+
| `method_declaration` (inside type) | `method` |
|
|
17
|
+
| `function_definition` (top level) | `function` |
|
|
18
|
+
| `property_declaration` (inside type) | `field` |
|
|
19
|
+
| `const_declaration` (top level) | `variable` |
|
|
20
|
+
| `const_declaration` (inside type) | `field` |
|
|
21
|
+
|
|
22
|
+
`namespace_definition` is captured as `extra.namespace` on every
|
|
23
|
+
symbol-producing type.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install "git+https://github.com/qxbyte/codemap.git#subdirectory=plugins/codemap-php"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## SymbolID encoding
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
scip-php . . . src/App/User.php/User#hello().
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Limits
|
|
38
|
+
|
|
39
|
+
* Use statements aren't yet expanded into namespace-resolved edges.
|
|
40
|
+
* PHPDoc annotations are not parsed.
|
|
41
|
+
* Anonymous classes are skipped.
|
|
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-php"
|
|
7
|
+
version = "0.1.0a1"
|
|
8
|
+
description = "PHP 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", "php", "indexer", "tree-sitter"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: PHP",
|
|
18
|
+
"Topic :: Software Development",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"codemap-core>=0.1.0a1,<0.2",
|
|
22
|
+
"tree-sitter>=0.25",
|
|
23
|
+
"tree-sitter-php>=0.23",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = ["pytest>=8.0"]
|
|
28
|
+
|
|
29
|
+
[project.entry-points."codemap.indexers"]
|
|
30
|
+
php = "codemap_php:PhpIndexer"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/qxbyte/codemap"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/codemap_php"]
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""PHP indexer built on tree-sitter-php.
|
|
2
|
+
|
|
3
|
+
Handles all four PHP type declarations (``class`` / ``interface`` /
|
|
4
|
+
``trait`` / ``enum``) with their respective bodies, plus free
|
|
5
|
+
``function_definition`` and module-level ``const_declaration``.
|
|
6
|
+
``namespace_definition`` is honoured as ``extra.namespace`` on every
|
|
7
|
+
type symbol; nested types share the same single namespace (PHP doesn't
|
|
8
|
+
allow nested classes the way Java does, but the tree-sitter grammar
|
|
9
|
+
permits it).
|
|
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_php
|
|
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-php"
|
|
25
|
+
LANG = "php"
|
|
26
|
+
|
|
27
|
+
_PHP_LANG = tree_sitter.Language(tree_sitter_php.language_php())
|
|
28
|
+
|
|
29
|
+
_TYPE_DECLS: dict[str, str] = {
|
|
30
|
+
"class_declaration": "class",
|
|
31
|
+
"interface_declaration": "interface",
|
|
32
|
+
"trait_declaration": "trait",
|
|
33
|
+
"enum_declaration": "enum",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PhpIndexer:
|
|
38
|
+
name: ClassVar[str] = "php"
|
|
39
|
+
version: ClassVar[str] = "0.1.0"
|
|
40
|
+
file_patterns: ClassVar[list[str]] = ["*.php"]
|
|
41
|
+
languages: ClassVar[list[str]] = [LANG]
|
|
42
|
+
|
|
43
|
+
def supports(self, path: Path) -> bool:
|
|
44
|
+
return path.suffix == ".php"
|
|
45
|
+
|
|
46
|
+
def index_file(
|
|
47
|
+
self,
|
|
48
|
+
path: Path,
|
|
49
|
+
source: bytes,
|
|
50
|
+
ctx: IndexContext,
|
|
51
|
+
) -> IndexResult:
|
|
52
|
+
try:
|
|
53
|
+
source.decode("utf-8")
|
|
54
|
+
except UnicodeDecodeError as exc:
|
|
55
|
+
return IndexResult(
|
|
56
|
+
diagnostics=[
|
|
57
|
+
Diagnostic(
|
|
58
|
+
severity="error",
|
|
59
|
+
file=ctx.relative_path,
|
|
60
|
+
code="PHP002",
|
|
61
|
+
message=f"not valid UTF-8: {exc}",
|
|
62
|
+
producer=self.name,
|
|
63
|
+
)
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
parser = tree_sitter.Parser(_PHP_LANG)
|
|
67
|
+
tree = parser.parse(source)
|
|
68
|
+
visitor = _Visitor(ctx.relative_path)
|
|
69
|
+
visitor.visit(tree.root_node)
|
|
70
|
+
diagnostics = list(visitor.diagnostics)
|
|
71
|
+
if tree.root_node.has_error:
|
|
72
|
+
diagnostics.append(
|
|
73
|
+
Diagnostic(
|
|
74
|
+
severity="warning",
|
|
75
|
+
file=ctx.relative_path,
|
|
76
|
+
range=Range(start_line=1, end_line=1),
|
|
77
|
+
code="PHP001",
|
|
78
|
+
message="tree-sitter reported parse errors; symbols may be incomplete",
|
|
79
|
+
producer=self.name,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
return IndexResult(
|
|
83
|
+
symbols=visitor.symbols,
|
|
84
|
+
edges=visitor.edges,
|
|
85
|
+
diagnostics=diagnostics,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class _Visitor:
|
|
90
|
+
def __init__(self, relative_path: PurePosixPath) -> None:
|
|
91
|
+
self.relative_path = relative_path
|
|
92
|
+
self.symbols: list[Symbol] = []
|
|
93
|
+
self.edges: list[Edge] = []
|
|
94
|
+
self.diagnostics: list[Diagnostic] = []
|
|
95
|
+
self._type_stack: list[str] = []
|
|
96
|
+
self._namespace: str = ""
|
|
97
|
+
|
|
98
|
+
def visit(self, node: tree_sitter.Node) -> None:
|
|
99
|
+
kind = node.type
|
|
100
|
+
if kind == "namespace_definition":
|
|
101
|
+
self._set_namespace(node)
|
|
102
|
+
return
|
|
103
|
+
if kind in _TYPE_DECLS:
|
|
104
|
+
self._visit_type(node, php_kind=_TYPE_DECLS[kind])
|
|
105
|
+
return
|
|
106
|
+
if kind == "function_definition":
|
|
107
|
+
self._visit_function(node)
|
|
108
|
+
return
|
|
109
|
+
if kind == "method_declaration" and self._type_stack:
|
|
110
|
+
self._visit_method(node)
|
|
111
|
+
return
|
|
112
|
+
if kind == "property_declaration" and self._type_stack:
|
|
113
|
+
self._visit_property(node)
|
|
114
|
+
return
|
|
115
|
+
if kind == "const_declaration":
|
|
116
|
+
self._visit_const(node)
|
|
117
|
+
return
|
|
118
|
+
for child in node.children:
|
|
119
|
+
self.visit(child)
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------- namespace
|
|
122
|
+
|
|
123
|
+
def _set_namespace(self, node: tree_sitter.Node) -> None:
|
|
124
|
+
for child in node.children:
|
|
125
|
+
if child.type == "namespace_name":
|
|
126
|
+
self._namespace = _node_text(child)
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
# ------------------------------------------------------- types
|
|
130
|
+
|
|
131
|
+
def _visit_type(self, node: tree_sitter.Node, *, php_kind: str) -> None:
|
|
132
|
+
name = _name_child(node)
|
|
133
|
+
if name is None:
|
|
134
|
+
return
|
|
135
|
+
sid = self._make_id(name, kind=DescriptorKind.TYPE)
|
|
136
|
+
extra: dict[str, str] = {"php_kind": php_kind}
|
|
137
|
+
if self._namespace:
|
|
138
|
+
extra["namespace"] = self._namespace
|
|
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 = _declaration_list(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
|
+
if self._type_stack:
|
|
163
|
+
# Function defined inside a class body (rare but legal in some
|
|
164
|
+
# grammars) — treat as method.
|
|
165
|
+
self._visit_method(node)
|
|
166
|
+
return
|
|
167
|
+
name = _name_child(node)
|
|
168
|
+
if name is None:
|
|
169
|
+
return
|
|
170
|
+
sid = self._make_id(name, kind=DescriptorKind.METHOD)
|
|
171
|
+
self.symbols.append(
|
|
172
|
+
Symbol(
|
|
173
|
+
id=sid,
|
|
174
|
+
kind="function",
|
|
175
|
+
language=LANG,
|
|
176
|
+
file=self.relative_path,
|
|
177
|
+
range=_node_range(node),
|
|
178
|
+
signature=f"function {name}()",
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def _visit_method(self, node: tree_sitter.Node) -> None:
|
|
183
|
+
name = _name_child(node)
|
|
184
|
+
if name is None:
|
|
185
|
+
return
|
|
186
|
+
sid = self._make_id(name, kind=DescriptorKind.METHOD)
|
|
187
|
+
self.symbols.append(
|
|
188
|
+
Symbol(
|
|
189
|
+
id=sid,
|
|
190
|
+
kind="method",
|
|
191
|
+
language=LANG,
|
|
192
|
+
file=self.relative_path,
|
|
193
|
+
range=_node_range(node),
|
|
194
|
+
signature=f"function {name}()",
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# --------------------------------------------------- properties
|
|
199
|
+
|
|
200
|
+
def _visit_property(self, node: tree_sitter.Node) -> None:
|
|
201
|
+
# property_declaration > property_element > variable_name
|
|
202
|
+
for child in node.children:
|
|
203
|
+
if child.type == "property_element":
|
|
204
|
+
for grand in child.children:
|
|
205
|
+
if grand.type == "variable_name":
|
|
206
|
+
# variable_name has a single name child (without '$')
|
|
207
|
+
for great in grand.children:
|
|
208
|
+
if great.type == "name":
|
|
209
|
+
name = _node_text(great)
|
|
210
|
+
if not name:
|
|
211
|
+
return
|
|
212
|
+
sid = self._make_id(name, kind=DescriptorKind.TERM)
|
|
213
|
+
self.symbols.append(
|
|
214
|
+
Symbol(
|
|
215
|
+
id=sid,
|
|
216
|
+
kind="field",
|
|
217
|
+
language=LANG,
|
|
218
|
+
file=self.relative_path,
|
|
219
|
+
range=_node_range(node),
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
# ------------------------------------------------ const decls
|
|
225
|
+
|
|
226
|
+
def _visit_const(self, node: tree_sitter.Node) -> None:
|
|
227
|
+
# const_declaration > const_element > name
|
|
228
|
+
for child in node.children:
|
|
229
|
+
if child.type != "const_element":
|
|
230
|
+
continue
|
|
231
|
+
for grand in child.children:
|
|
232
|
+
if grand.type == "name":
|
|
233
|
+
name = _node_text(grand)
|
|
234
|
+
if not name:
|
|
235
|
+
return
|
|
236
|
+
sym_kind: str = "field" if self._type_stack else "variable"
|
|
237
|
+
sid = self._make_id(name, kind=DescriptorKind.TERM)
|
|
238
|
+
self.symbols.append(
|
|
239
|
+
Symbol(
|
|
240
|
+
id=sid,
|
|
241
|
+
kind=sym_kind, # type: ignore[arg-type]
|
|
242
|
+
language=LANG,
|
|
243
|
+
file=self.relative_path,
|
|
244
|
+
range=_node_range(node),
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
# -------------------------------------------------------- helpers
|
|
250
|
+
|
|
251
|
+
def _make_id(self, name: str, *, kind: DescriptorKind) -> SymbolID:
|
|
252
|
+
descriptors = list(_path_namespaces(self.relative_path))
|
|
253
|
+
descriptors.extend(Descriptor(name=t, kind=DescriptorKind.TYPE) for t in self._type_stack)
|
|
254
|
+
descriptors.append(Descriptor(name=name, kind=kind))
|
|
255
|
+
return SymbolID(scheme=SCHEME, descriptors=tuple(descriptors))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
# Pure helpers
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _path_namespaces(path: PurePosixPath) -> list[Descriptor]:
|
|
264
|
+
return [Descriptor(name=part, kind=DescriptorKind.NAMESPACE) for part in path.parts]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _node_range(node: tree_sitter.Node) -> Range:
|
|
268
|
+
sr, sc = node.start_point
|
|
269
|
+
er, ec = node.end_point
|
|
270
|
+
return Range(
|
|
271
|
+
start_line=sr + 1,
|
|
272
|
+
start_col=sc,
|
|
273
|
+
end_line=max(er + 1, sr + 1),
|
|
274
|
+
end_col=ec,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _node_text(node: tree_sitter.Node) -> str:
|
|
279
|
+
return node.text.decode("utf-8") if node.text is not None else ""
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _name_child(node: tree_sitter.Node) -> str | None:
|
|
283
|
+
for child in node.children:
|
|
284
|
+
if child.type == "name":
|
|
285
|
+
return _node_text(child)
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _declaration_list(node: tree_sitter.Node) -> tree_sitter.Node | None:
|
|
290
|
+
for child in node.children:
|
|
291
|
+
if child.type in {"declaration_list", "enum_declaration_list"}:
|
|
292
|
+
return child
|
|
293
|
+
return None
|
|
File without changes
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Unit tests for the PHP indexer plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import textwrap
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
|
|
8
|
+
from codemap_php import PhpIndexer
|
|
9
|
+
from codemap_php.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/User.php") -> IndexResult:
|
|
16
|
+
code = textwrap.dedent(source).lstrip("\n")
|
|
17
|
+
return PhpIndexer().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="php",
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_indexer_metadata() -> None:
|
|
29
|
+
ix = PhpIndexer()
|
|
30
|
+
assert ix.name == "php"
|
|
31
|
+
assert ix.languages == ["php"]
|
|
32
|
+
assert ix.supports(Path("a.php"))
|
|
33
|
+
assert not ix.supports(Path("a.py"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_scheme_is_consistent() -> None:
|
|
37
|
+
r = _index(
|
|
38
|
+
"""
|
|
39
|
+
<?php
|
|
40
|
+
class A {}
|
|
41
|
+
function f() {}
|
|
42
|
+
const X = 1;
|
|
43
|
+
"""
|
|
44
|
+
)
|
|
45
|
+
for s in r.symbols:
|
|
46
|
+
assert str(s.id).startswith(f"{SCHEME} ")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_class_declaration() -> None:
|
|
50
|
+
r = _index(
|
|
51
|
+
"""
|
|
52
|
+
<?php
|
|
53
|
+
class User {
|
|
54
|
+
public string $name;
|
|
55
|
+
public function hello(): string { return $this->name; }
|
|
56
|
+
}
|
|
57
|
+
"""
|
|
58
|
+
)
|
|
59
|
+
classes = [s for s in r.symbols if s.kind == "class"]
|
|
60
|
+
assert classes[0].extra.get("php_kind") == "class"
|
|
61
|
+
methods = [s for s in r.symbols if s.kind == "method"]
|
|
62
|
+
assert any("User#hello()." in str(m.id) for m in methods)
|
|
63
|
+
fields = [s for s in r.symbols if s.kind == "field"]
|
|
64
|
+
assert any("User#name." in str(f.id) for f in fields)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_interface_declaration() -> None:
|
|
68
|
+
r = _index(
|
|
69
|
+
"""
|
|
70
|
+
<?php
|
|
71
|
+
interface Greeter {
|
|
72
|
+
public function hello(): string;
|
|
73
|
+
}
|
|
74
|
+
"""
|
|
75
|
+
)
|
|
76
|
+
cls = next(s for s in r.symbols if s.kind == "class")
|
|
77
|
+
assert cls.extra.get("php_kind") == "interface"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_trait_declaration() -> None:
|
|
81
|
+
r = _index(
|
|
82
|
+
"""
|
|
83
|
+
<?php
|
|
84
|
+
trait Loggable {
|
|
85
|
+
public function log() {}
|
|
86
|
+
}
|
|
87
|
+
"""
|
|
88
|
+
)
|
|
89
|
+
cls = next(s for s in r.symbols if s.kind == "class")
|
|
90
|
+
assert cls.extra.get("php_kind") == "trait"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_namespace_captured_in_extra() -> None:
|
|
94
|
+
r = _index(
|
|
95
|
+
"""
|
|
96
|
+
<?php
|
|
97
|
+
namespace App\\Models;
|
|
98
|
+
class User {}
|
|
99
|
+
"""
|
|
100
|
+
)
|
|
101
|
+
cls = next(s for s in r.symbols if s.kind == "class")
|
|
102
|
+
assert "App" in cls.extra.get("namespace", "")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_free_function() -> None:
|
|
106
|
+
r = _index("<?php\nfunction helper(int $x): int { return $x + 1; }")
|
|
107
|
+
funcs = [s for s in r.symbols if s.kind == "function"]
|
|
108
|
+
assert len(funcs) == 1
|
|
109
|
+
assert "helper" in str(funcs[0].id)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_top_level_const_is_variable() -> None:
|
|
113
|
+
r = _index("<?php\nconst MAX = 10;")
|
|
114
|
+
vars_ = [s for s in r.symbols if s.kind == "variable"]
|
|
115
|
+
assert len(vars_) == 1
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_const_inside_class_is_field() -> None:
|
|
119
|
+
r = _index(
|
|
120
|
+
"""
|
|
121
|
+
<?php
|
|
122
|
+
class A {
|
|
123
|
+
const DEFAULT = 1;
|
|
124
|
+
}
|
|
125
|
+
"""
|
|
126
|
+
)
|
|
127
|
+
fields = [s for s in r.symbols if s.kind == "field"]
|
|
128
|
+
assert any("DEFAULT" in str(f.id) for f in fields)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_static_method_recorded() -> None:
|
|
132
|
+
r = _index(
|
|
133
|
+
"""
|
|
134
|
+
<?php
|
|
135
|
+
class User {
|
|
136
|
+
public static function create(string $name): User {
|
|
137
|
+
return new User();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
"""
|
|
141
|
+
)
|
|
142
|
+
methods = [s for s in r.symbols if s.kind == "method"]
|
|
143
|
+
assert any("create" in str(m.id) for m in methods)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_symbol_id_uses_path_namespaces() -> None:
|
|
147
|
+
r = _index("<?php\nclass A {}", path="src/App/A.php")
|
|
148
|
+
cls = next(s for s in r.symbols if s.kind == "class")
|
|
149
|
+
assert str(cls.id) == "scip-php . . . src/App/A.php/A#"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_empty_file_yields_no_symbols() -> None:
|
|
153
|
+
r = _index("")
|
|
154
|
+
assert r.symbols == []
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_invalid_utf8_yields_diagnostic() -> None:
|
|
158
|
+
ix = PhpIndexer()
|
|
159
|
+
r = ix.index_file(
|
|
160
|
+
Path("bad.php"),
|
|
161
|
+
b"\xff\xfe<?php",
|
|
162
|
+
IndexContext(
|
|
163
|
+
project_root=Path("/tmp/proj"),
|
|
164
|
+
relative_path=PurePosixPath("bad.php"),
|
|
165
|
+
language="php",
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
assert r.symbols == []
|
|
169
|
+
assert r.diagnostics[0].code == "PHP002"
|