codemap-php 0.1.0a1__py3-none-any.whl

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,8 @@
1
+ """PHP indexer plugin for CodeMap."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from codemap_php.indexer import PhpIndexer
6
+
7
+ __all__ = ["PhpIndexer"]
8
+ __version__ = "0.1.0"
codemap_php/indexer.py ADDED
@@ -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
@@ -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,6 @@
1
+ codemap_php/__init__.py,sha256=cQI-s7hKZEXaCfVwUU88zMBahsCtsa6BLvcbqGfzwak,166
2
+ codemap_php/indexer.py,sha256=kzqzlm5IOKvXqwPV_ZSugoALKqsIA1mW2Fp1NBOuSNw,10197
3
+ codemap_php-0.1.0a1.dist-info/METADATA,sha256=0_tH982u2h1PYWTckJ2fA1y3tViNelhZfZbTFoWJEMI,1823
4
+ codemap_php-0.1.0a1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
5
+ codemap_php-0.1.0a1.dist-info/entry_points.txt,sha256=Zud_JWFwg1IR9RBW9MtliC44q2J0TZP-jF3Bq_FjMUo,48
6
+ codemap_php-0.1.0a1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [codemap.indexers]
2
+ php = codemap_php:PhpIndexer