Skip to content

Commit a98dc21

Browse files
jgravelleclaude
andcommitted
Add PHP language support
Adds PHP as a supported language via a new LanguageSpec backed by tree-sitter-php (already bundled in tree-sitter-language-pack). Supports functions, classes, methods, interfaces, traits, enums, constants, return types, and PHPDoc comments. PHP 8 #[Attribute] decorator syntax is also handled. Updates LANGUAGE_SUPPORT.md, README.md, and SPEC.md to document the new language. Adds test fixture and test_parse_php() to the language test suite. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 97c15b4 commit a98dc21

6 files changed

Lines changed: 174 additions & 4 deletions

File tree

LANGUAGE_SUPPORT.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
| Go | `.go` | tree-sitter-go | function, method, type, constant || `//` comments | No class hierarchy (language limitation) |
1111
| Rust | `.rs` | tree-sitter-rust | function, type (struct/enum/trait), impl, constant | `#[attr]` | `///` and `//!` comments | Macro-generated symbols are not visible to the parser |
1212
| Java | `.java` | tree-sitter-java | method, class, type (interface/enum), constant | `@Annotation` | `/** */` Javadoc | Deep inner-class nesting may be flattened |
13+
| PHP | `.php` | tree-sitter-php | function, class, method, type (interface/trait/enum), constant | `#[Attribute]` | `/** */` PHPDoc | PHP 8+ attributes supported; language-file `<?php` tag required |
1314

1415
---
1516

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ All tool responses include a `_meta` envelope with timing and metadata.
190190
| Go | `.go` | function, method, type, constant |
191191
| Rust | `.rs` | function, type, impl, constant |
192192
| Java | `.java` | method, class, type, constant |
193+
| PHP | `.php` | function, class, method, type, constant |
193194

194195
See **LANGUAGE_SUPPORT.md** for full semantics.
195196

SPEC.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ class Symbol:
166166
name: str # Symbol name
167167
qualified_name: str # Dot-separated with parent context
168168
kind: str # function | class | method | constant | type
169-
language: str # python | javascript | typescript | go | rust | java
169+
language: str # python | javascript | typescript | go | rust | java | php
170170
signature: str # Full signature line(s)
171171
content_hash: str = "" # SHA-256 of source bytes (drift detection)
172172
docstring: str = ""
@@ -212,7 +212,7 @@ Recursive directory walk with the full security pipeline.
212212

213213
### Filtering Pipeline (Both Paths)
214214

215-
1. **Extension filter** — must be in `LANGUAGE_EXTENSIONS` (.py, .js, .jsx, .ts, .tsx, .go, .rs, .java)
215+
1. **Extension filter** — must be in `LANGUAGE_EXTENSIONS` (.py, .js, .jsx, .ts, .tsx, .go, .rs, .java, .php)
216216
2. **Skip patterns**`node_modules/`, `vendor/`, `.git/`, `build/`, `dist/`, lock files, minified files, etc.
217217
3. **`.gitignore`** — respected via the `pathspec` library
218218
4. **Secret detection**`.env`, `*.pem`, `*.key`, `*.p12`, credentials files excluded

src/jcodemunch_mcp/parser/languages.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class LanguageSpec:
5353
".go": "go",
5454
".rs": "rust",
5555
".java": "java",
56+
".php": "php",
5657
}
5758

5859

@@ -241,6 +242,41 @@ class LanguageSpec:
241242
)
242243

243244

245+
# PHP specification
246+
PHP_SPEC = LanguageSpec(
247+
ts_language="php",
248+
symbol_node_types={
249+
"function_definition": "function",
250+
"class_declaration": "class",
251+
"method_declaration": "method",
252+
"interface_declaration": "type",
253+
"trait_declaration": "type",
254+
"enum_declaration": "type",
255+
},
256+
name_fields={
257+
"function_definition": "name",
258+
"class_declaration": "name",
259+
"method_declaration": "name",
260+
"interface_declaration": "name",
261+
"trait_declaration": "name",
262+
"enum_declaration": "name",
263+
},
264+
param_fields={
265+
"function_definition": "parameters",
266+
"method_declaration": "parameters",
267+
},
268+
return_type_fields={
269+
"function_definition": "return_type",
270+
"method_declaration": "return_type",
271+
},
272+
docstring_strategy="preceding_comment",
273+
decorator_node_type="attribute", # PHP 8 #[Attribute] syntax
274+
container_node_types=["class_declaration", "trait_declaration", "interface_declaration"],
275+
constant_patterns=["const_declaration"],
276+
type_patterns=["interface_declaration", "trait_declaration", "enum_declaration"],
277+
)
278+
279+
244280
# Language registry
245281
LANGUAGE_REGISTRY = {
246282
"python": PYTHON_SPEC,
@@ -249,4 +285,5 @@ class LanguageSpec:
249285
"go": GO_SPEC,
250286
"rust": RUST_SPEC,
251287
"java": JAVA_SPEC,
288+
"php": PHP_SPEC,
252289
}

tests/fixtures/php/sample.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
const MAX_RETRIES = 3;
4+
5+
/**
6+
* Authenticate a user token.
7+
*/
8+
function authenticate(string $token): bool
9+
{
10+
return strlen($token) > 0;
11+
}
12+
13+
/**
14+
* Manages user operations.
15+
*/
16+
class UserService
17+
{
18+
/**
19+
* Get a user by ID.
20+
*/
21+
public function getUser(int $userId): array
22+
{
23+
return ['id' => $userId];
24+
}
25+
26+
/**
27+
* Create a new user.
28+
*/
29+
public static function create(string $name): self
30+
{
31+
return new self();
32+
}
33+
}
34+
35+
interface Authenticatable
36+
{
37+
public function authenticate(string $token): bool;
38+
}
39+
40+
trait Timestampable
41+
{
42+
public function getCreatedAt(): string
43+
{
44+
return date('Y-m-d');
45+
}
46+
}
47+
48+
enum Status
49+
{
50+
case Active;
51+
case Inactive;
52+
case Pending;
53+
}

tests/test_languages.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,91 @@ def test_parse_rust():
171171
def test_parse_java():
172172
"""Test Java parsing."""
173173
symbols = parse_file(JAVA_SOURCE, "Calculator.java", "java")
174-
174+
175175
# Should have class, method, interface
176176
calc = next((s for s in symbols if s.name == "Calculator"), None)
177177
assert calc is not None
178178
assert calc.kind == "class"
179-
179+
180180
add = next((s for s in symbols if s.name == "add"), None)
181181
assert add is not None
182182
assert add.kind == "method"
183183

184+
185+
PHP_SOURCE = '''<?php
186+
187+
const MAX_RETRIES = 3;
188+
189+
/**
190+
* Authenticate a user token.
191+
*/
192+
function authenticate(string $token): bool
193+
{
194+
return strlen($token) > 0;
195+
}
196+
197+
/**
198+
* Manages user operations.
199+
*/
200+
class UserService
201+
{
202+
/**
203+
* Get a user by ID.
204+
*/
205+
public function getUser(int $userId): array
206+
{
207+
return ['id' => $userId];
208+
}
209+
}
210+
211+
interface Authenticatable
212+
{
213+
public function authenticate(string $token): bool;
214+
}
215+
216+
trait Timestampable
217+
{
218+
public function getCreatedAt(): string
219+
{
220+
return date(\'Y-m-d\');
221+
}
222+
}
223+
224+
enum Status
225+
{
226+
case Active;
227+
case Inactive;
228+
}
229+
'''
230+
231+
232+
def test_parse_php():
233+
"""Test PHP parsing."""
234+
symbols = parse_file(PHP_SOURCE, "service.php", "php")
235+
236+
func = next((s for s in symbols if s.name == "authenticate"), None)
237+
assert func is not None
238+
assert func.kind == "function"
239+
assert "Authenticate a user token" in func.docstring
240+
241+
cls = next((s for s in symbols if s.name == "UserService"), None)
242+
assert cls is not None
243+
assert cls.kind == "class"
244+
245+
method = next((s for s in symbols if s.name == "getUser"), None)
246+
assert method is not None
247+
assert method.kind == "method"
248+
assert "Get a user by ID" in method.docstring
249+
250+
interface = next((s for s in symbols if s.name == "Authenticatable"), None)
251+
assert interface is not None
252+
assert interface.kind == "type"
253+
254+
trait = next((s for s in symbols if s.name == "Timestampable"), None)
255+
assert trait is not None
256+
assert trait.kind == "type"
257+
258+
enum = next((s for s in symbols if s.name == "Status"), None)
259+
assert enum is not None
260+
assert enum.kind == "type"
261+

0 commit comments

Comments
 (0)