php-parser-py 1.1.1__tar.gz → 1.2.0__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,61 @@
1
+ Metadata-Version: 2.3
2
+ Name: php-parser-py
3
+ Version: 1.2.0
4
+ Summary: Python wrapper for PHP-Parser using cpg2py
5
+ Author: samhsu-dev
6
+ Author-email: samhsu-dev <yxu166@jh.edu>
7
+ Requires-Dist: cpg2py>=1.2.0
8
+ Requires-Dist: static-php-py
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+
12
+ # php-parser-py
13
+
14
+ Python wrapper for [nikic/PHP-Parser](https://github.com/nikic/PHP-Parser) built on the [cpg2py](https://github.com/jhu-seclab/cpg2py) graph framework.
15
+
16
+ - **Automated**: Uses [static-php-cli](https://github.com/static-php-cli/static-php-cli) binaries (no local PHP required).
17
+ - **Graph-Based**: AST nodes are stored in a queryable graph database.
18
+ - **Accurate**: Full support for PHP-Parser 4.19.4 AST structure.
19
+
20
+ ## Quick Start
21
+ ```bash
22
+ pip install php-parser-py
23
+ ```
24
+
25
+ ```python
26
+ from php_parser_py import parse_file, parse_project
27
+
28
+ # 1. Parse
29
+ ast = parse_file("src/User.php")
30
+ # ast = parse_project("src/")
31
+
32
+ # 2. Traverse
33
+ files = ast.file_nodes()
34
+
35
+ # Find specific nodes
36
+ functions = ast.nodes(lambda n: n.node_type == "Stmt_Function")
37
+
38
+ # Get properties (scalars)
39
+ for func in functions:
40
+ print(f"Function: {func['namespacedName']}")
41
+
42
+ # Get child nodes (structure)
43
+ for func in functions:
44
+ # Use helper method or traverse edges
45
+ params = [c for c in ast.succ(func) if c.node_type == "Param"]
46
+ ```
47
+
48
+ ## Documentation
49
+
50
+ - **[Architecture & Design](docs/design.md)**
51
+ Detailed design documentation covering internal architecture, class specifications, and design rationale.
52
+
53
+ - **[AST Structure Reference](docs/libs/php_parser_ast.md)**
54
+ Complete table of all node types, subnodes (children), and properties. Verified against PHP-Parser source.
55
+
56
+ - **[Graph Traversal Guide](docs/libs/cpg2py_traversal.md)**
57
+ How to navigate the graph (`succ`, `prev`), access properties, and modify the graph (`set_property`).
58
+
59
+ ## Requirements
60
+ - Python ≥ 3.11
61
+ - (Optional) Custom PHP binary can be configured via `Parser` constructor.
@@ -0,0 +1,50 @@
1
+ # php-parser-py
2
+
3
+ Python wrapper for [nikic/PHP-Parser](https://github.com/nikic/PHP-Parser) built on the [cpg2py](https://github.com/jhu-seclab/cpg2py) graph framework.
4
+
5
+ - **Automated**: Uses [static-php-cli](https://github.com/static-php-cli/static-php-cli) binaries (no local PHP required).
6
+ - **Graph-Based**: AST nodes are stored in a queryable graph database.
7
+ - **Accurate**: Full support for PHP-Parser 4.19.4 AST structure.
8
+
9
+ ## Quick Start
10
+ ```bash
11
+ pip install php-parser-py
12
+ ```
13
+
14
+ ```python
15
+ from php_parser_py import parse_file, parse_project
16
+
17
+ # 1. Parse
18
+ ast = parse_file("src/User.php")
19
+ # ast = parse_project("src/")
20
+
21
+ # 2. Traverse
22
+ files = ast.file_nodes()
23
+
24
+ # Find specific nodes
25
+ functions = ast.nodes(lambda n: n.node_type == "Stmt_Function")
26
+
27
+ # Get properties (scalars)
28
+ for func in functions:
29
+ print(f"Function: {func['namespacedName']}")
30
+
31
+ # Get child nodes (structure)
32
+ for func in functions:
33
+ # Use helper method or traverse edges
34
+ params = [c for c in ast.succ(func) if c.node_type == "Param"]
35
+ ```
36
+
37
+ ## Documentation
38
+
39
+ - **[Architecture & Design](docs/design.md)**
40
+ Detailed design documentation covering internal architecture, class specifications, and design rationale.
41
+
42
+ - **[AST Structure Reference](docs/libs/php_parser_ast.md)**
43
+ Complete table of all node types, subnodes (children), and properties. Verified against PHP-Parser source.
44
+
45
+ - **[Graph Traversal Guide](docs/libs/cpg2py_traversal.md)**
46
+ How to navigate the graph (`succ`, `prev`), access properties, and modify the graph (`set_property`).
47
+
48
+ ## Requirements
49
+ - Python ≥ 3.11
50
+ - (Optional) Custom PHP binary can be configured via `Parser` constructor.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "php-parser-py"
3
- version = "1.1.1"
3
+ version = "1.2.0"
4
4
  description = "Python wrapper for PHP-Parser using cpg2py"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -8,7 +8,7 @@ authors = [
8
8
  ]
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
11
- "cpg2py>=1.1.0",
11
+ "cpg2py>=1.2.0",
12
12
  "static-php-py",
13
13
  ]
14
14
 
@@ -77,35 +77,25 @@ class AST(AbcGraphQuerier[Node, Edge]):
77
77
  return None
78
78
  return Edge(self._storage, fid, tid, eid)
79
79
 
80
- @property
81
- def root_node(self) -> Optional[Node]:
82
- """Return the root project node of the AST.
80
+ def project_node(self) -> Optional[Node]:
81
+ """Return the project node.
83
82
 
84
- The root node is always the project node with ID "project".
83
+ The project node is the root of the AST with ID "project".
85
84
 
86
85
  Returns:
87
- Root Node instance or None if root node doesn't exist.
86
+ Project Node instance or None if not found.
88
87
  """
89
88
  if not self._storage.contains_node(self._root_node_id):
90
89
  return None
91
90
  return Node(self._storage, self._root_node_id)
92
91
 
93
- @property
94
- def project_node(self) -> Optional[Node]:
95
- """Return the project node (alias for root_node).
96
-
97
- Returns:
98
- Project Node instance or None if not found.
99
- """
100
- return self.root_node
101
-
102
- def files(self) -> list[Node]:
92
+ def file_nodes(self) -> list[Node]:
103
93
  """Return all file nodes in the project.
104
94
 
105
95
  Returns:
106
96
  List of File Node instances.
107
97
  """
108
- if not self.root_node:
98
+ if not self.project_node():
109
99
  return []
110
100
 
111
101
  file_nodes = []
@@ -204,7 +194,7 @@ class AST(AbcGraphQuerier[Node, Edge]):
204
194
  top_level_nodes = self._get_file_statements(file_hash)
205
195
  else:
206
196
  # Try to export from file structure first
207
- file_nodes = self.files()
197
+ file_nodes = self.file_nodes()
208
198
  if file_nodes:
209
199
  # Export all files (flattened)
210
200
  top_level_nodes = []
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  from typing import Any, Callable, Optional
7
7
 
8
8
  from cpg2py import Storage
9
+ from static_php_py import PHP
9
10
 
10
11
  from php_parser_py._ast import AST
11
12
  from php_parser_py._exceptions import ParseError, RunnerError
@@ -27,21 +28,13 @@ class Parser:
27
28
  _runner: Runner instance for PHP-Parser invocation.
28
29
  """
29
30
 
30
- def __init__(
31
- self,
32
- php_binary_path: Optional[Path] = None,
33
- php_binary_url: Optional[str] = None,
34
- ) -> None:
31
+ def __init__(self, php: Optional[PHP] = None) -> None:
35
32
  """Initialize Parser with Runner.
36
33
 
37
34
  Args:
38
- php_binary_path: Optional path to local PHP binary.
39
- php_binary_url: Optional URL to download PHP binary from.
35
+ php: Optional PHP instance. If not provided, uses builtin PHP.
40
36
  """
41
- self._runner = Runner(
42
- php_binary_path=php_binary_path,
43
- php_binary_url=php_binary_url,
44
- )
37
+ self._runner = Runner(php=php)
45
38
 
46
39
  def parse_code(self, code: str) -> list[Node]:
47
40
  """Parse PHP code string into a list of top-level statement nodes.
@@ -1,9 +1,10 @@
1
1
  """PrettyPrinter class for PHP code generation."""
2
2
 
3
3
  import logging
4
- from pathlib import Path
5
4
  from typing import Optional
6
5
 
6
+ from static_php_py import PHP
7
+
7
8
  from php_parser_py._ast import AST
8
9
  from php_parser_py._runner import Runner
9
10
 
@@ -22,18 +23,15 @@ class PrettyPrinter:
22
23
 
23
24
  def __init__(
24
25
  self,
25
- php_binary_path: Optional[Path] = None,
26
- php_binary_url: Optional[str] = None,
26
+ php: Optional[PHP] = None,
27
27
  ) -> None:
28
28
  """Initialize PrettyPrinter with Runner.
29
29
 
30
30
  Args:
31
- php_binary_path: Optional path to local PHP binary.
32
- php_binary_url: Optional URL to download PHP binary from.
31
+ php: Optional PHP instance. If not provided, uses builtin PHP.
33
32
  """
34
33
  self._runner = Runner(
35
- php_binary_path=php_binary_path,
36
- php_binary_url=php_binary_url,
34
+ php=php,
37
35
  )
38
36
 
39
37
  def print(self, ast: AST) -> dict[str, str]:
@@ -50,7 +48,7 @@ class PrettyPrinter:
50
48
  Raises:
51
49
  RunnerError: If PHP-Parser execution fails.
52
50
  """
53
- file_nodes = ast.files()
51
+ file_nodes = ast.file_nodes()
54
52
 
55
53
  if not file_nodes:
56
54
  # No file structure - export all statements as a single code block
@@ -98,7 +96,7 @@ class PrettyPrinter:
98
96
  RunnerError: If PHP-Parser execution fails.
99
97
  """
100
98
  # Find file node by its relative path property
101
- for file_node in ast.files():
99
+ for file_node in ast.file_nodes():
102
100
  if file_node.get_property("path") == relative_path:
103
101
  file_hash = file_node.id
104
102
  json_str = ast.to_json(file_hash=file_hash)
@@ -3,7 +3,6 @@
3
3
  import json
4
4
  import logging
5
5
  import subprocess
6
- from pathlib import Path
7
6
  from typing import Any, Optional
8
7
 
9
8
  from static_php_py import PHP
@@ -26,41 +25,26 @@ class Runner:
26
25
  _vendor_dir: Path to directory containing PHP-Parser PHAR.
27
26
  """
28
27
 
29
- def __init__(
30
- self,
31
- php_binary_path: Optional[Path] = None,
32
- php_binary_url: Optional[str] = None,
33
- ) -> None:
34
- """Initialize Runner with PHP binary and PHP-Parser paths.
28
+ def __init__(self, php: Optional[PHP] = None) -> None:
29
+ """Initialize Runner with PHP binary wrapper.
35
30
 
36
31
  Args:
37
- php_binary_path: Optional path to local PHP binary.
38
- php_binary_url: Optional URL to download PHP binary from.
32
+ php: Optional PHP instance. If not provided, uses builtin PHP.
39
33
 
40
34
  Raises:
41
- RunnerError: If PHP binary or PHP-Parser cannot be located.
35
+ RunnerError: If PHP binary cannot be located.
42
36
  """
43
37
  self._vendor_dir = ensure_php_parser_extracted()
44
38
 
45
- # Determine PHP binary source with priority: custom path > custom URL > built-in
46
39
  try:
47
- if php_binary_path is not None:
48
- # Use custom local PHP binary
49
- php = PHP.local(php_binary_path)
50
- elif php_binary_url is not None:
51
- # Download PHP binary from custom URL
52
- php = PHP.remote(php_binary_url)
53
- else:
54
- # Use built-in PHP binary from static-php-py
55
- php = PHP.builtin()
56
-
57
- self._php_binary = php.path()
58
-
40
+ self._php = php if php is not None else PHP.builtin()
41
+ self._php_binary = self._php.path()
42
+
59
43
  except BinaryNotFoundError as e:
60
44
  raise RunnerError(
61
45
  f"PHP binary not found: {e}. "
62
46
  "The built-in PHP binary may not be available for your platform. "
63
- "Please provide a custom PHP binary path or URL.",
47
+ "Please provide a custom PHP instance.",
64
48
  exit_code=1,
65
49
  ) from e
66
50
  except DownloadError as e:
@@ -1,285 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: php-parser-py
3
- Version: 1.1.1
4
- Summary: Python wrapper for PHP-Parser using cpg2py
5
- Author: samhsu-dev
6
- Author-email: samhsu-dev <yxu166@jh.edu>
7
- Requires-Dist: cpg2py>=1.1.0
8
- Requires-Dist: static-php-py
9
- Requires-Python: >=3.11
10
- Description-Content-Type: text/markdown
11
-
12
- # php-parser-py
13
-
14
- Python wrapper for PHP-Parser using cpg2py's graph framework.
15
-
16
- ## Overview
17
-
18
- `php-parser-py` provides a Python interface to [PHP-Parser](https://github.com/nikic/PHP-Parser), enabling Abstract Syntax Tree (AST) parsing and manipulation of PHP code. The library integrates with [cpg2py](https://github.com/jhu-seclab/cpg2py)'s graph framework for powerful graph-based code analysis.
19
-
20
- ## Features
21
-
22
- - **Native PHP-Parser Integration**: Delegates all parsing and code generation to PHP-Parser for maximum compatibility
23
- - **Zero-Configuration PHP**: Bundled PHP binaries via static-php-py - no system PHP installation required
24
- - **Graph-Based Analysis**: Uses cpg2py's graph framework for AST traversal and querying
25
- - **Dynamic Node Types**: No hardcoded PHP node types - all types come from PHP-Parser's JSON output
26
- - **Lossless Round-Trip**: Parse → Modify → Generate code with full fidelity
27
- - **Cross-Platform Support**: Pre-built PHP 8.4 binaries for Linux (x86_64/aarch64), macOS (x86_64/arm64), and Windows (x64)
28
-
29
- ## Installation
30
-
31
- ```bash
32
- pip install php-parser-py
33
- ```
34
-
35
- **Note**: Platform-specific wheels include pre-built PHP 8.4 binaries. No separate PHP installation required!
36
-
37
- ## Quick Start
38
-
39
- ### Parse PHP Code (Without Project Structure)
40
-
41
- ```python
42
- from php_parser_py import parse_code
43
-
44
- # Parse PHP code into raw statement nodes
45
- code = """<?php
46
- function greet($name) {
47
- echo "Hello, $name!";
48
- }
49
- """
50
-
51
- nodes = parse_code(code)
52
- # Returns list of Node instances (no project/file structure)
53
-
54
- for node in nodes:
55
- print(f"Node type: {node.node_type}")
56
- ```
57
-
58
- ### Parse PHP File (With Project Structure)
59
-
60
- ```python
61
- from php_parser_py import parse_file
62
-
63
- # Parse a single file with project/file structure
64
- ast = parse_file("example.php")
65
-
66
- # Access project and file nodes
67
- project = ast.project_node
68
- files = ast.files()
69
- print(f"Project has {len(files)} file(s)")
70
-
71
- # Get file containing a specific node
72
- stmt_node = ast.first_node(lambda n: n.node_type == "Stmt_Function")
73
- file_node = ast.get_file(stmt_node.id)
74
- print(f"Function is in file: {file_node.get_property('filePath')}")
75
- ```
76
-
77
- ### Parse Project Directory
78
-
79
- ```python
80
- from php_parser_py import parse_project
81
- from pathlib import Path
82
-
83
- # Parse all PHP files in a project directory (recursively)
84
- ast = parse_project("/path/to/project")
85
-
86
- # Access all files
87
- for file_node in ast.files():
88
- print(f"File: {file_node.get_property('filePath')}")
89
- # Get statements in this file
90
- for stmt in ast.succ(file_node):
91
- print(f" Statement: {stmt.node_type}")
92
-
93
- # Access project and file paths
94
- project = ast.project_node
95
- print(f"Project root: {project.get_property('path')}")
96
-
97
- for file_node in ast.files():
98
- print(f"File relative path: {file_node.get_property('path')}")
99
- print(f"File absolute path: {file_node.get_property('filePath')}")
100
-
101
- # Use custom file filter (e.g., include .php and .phtml files)
102
- ast = parse_project(
103
- "/path/to/project",
104
- file_filter=lambda p: p.suffix in ['.php', '.phtml']
105
- )
106
- ```
107
-
108
- ### Traverse AST
109
-
110
- ```python
111
- from php_parser_py import parse_file
112
-
113
- ast = parse_file("example.php")
114
-
115
- # Find all echo statements
116
- for echo in ast.nodes(lambda n: n.node_type == "Stmt_Echo"):
117
- print(f"Echo at line {echo.start_line}")
118
-
119
- # Get children of a node (via PARENT_OF edges)
120
- for child in ast.succ(some_node):
121
- print(f"Child type: {child.node_type}")
122
-
123
- # Get all descendants
124
- for descendant in ast.descendants(some_node):
125
- print(f"Descendant: {descendant.node_type}")
126
-
127
- # Get file containing a node
128
- stmt_node = ast.first_node(lambda n: n.node_type == "Stmt_Function")
129
- file_node = ast.get_file(stmt_node.id)
130
- if file_node:
131
- print(f"Function is in: {file_node.get_property('filePath')}")
132
-
133
- # Access project structure
134
- project = ast.project_node
135
- files = ast.files()
136
- for file_node in files:
137
- print(f"File: {file_node.get_property('filePath')}")
138
- ```
139
-
140
- ### Generate PHP Code
141
-
142
- ```python
143
- from php_parser_py import parse_file, parse_project, PrettyPrinter
144
-
145
- # Parse single file
146
- ast = parse_file("example.php")
147
-
148
- # Generate code (returns dict mapping file paths to code)
149
- printer = PrettyPrinter()
150
- generated = printer.print(ast)
151
- for file_path, code in generated.items():
152
- print(f"File: {file_path}")
153
- print(code)
154
-
155
- # Parse project directory (recursively finds all PHP files)
156
- ast = parse_project("/path/to/project")
157
-
158
- # Generate code for all files
159
- generated = printer.print(ast)
160
- # Returns: {"src/file1.php": "...", "src/file2.php": "..."}
161
- ```
162
-
163
- ## Advanced Usage
164
-
165
- ### Custom PHP Binary
166
-
167
- If you need to use a specific PHP version or custom binary:
168
-
169
- ```python
170
- from php_parser_py import Parser
171
- from pathlib import Path
172
-
173
- # Use local PHP binary
174
- parser = Parser(php_binary_path=Path("/usr/local/bin/php"))
175
- ast = parser.parse("<?php echo 'hello';")
176
-
177
- # Or download from remote URL
178
- parser = Parser(php_binary_url="https://example.com/php-8.4.zip")
179
- ast = parser.parse("<?php echo 'hello';")
180
- ```
181
-
182
- ## API Reference
183
-
184
- ### Main Functions
185
-
186
- - **`parse_code(code: str) -> list[Node]`**: Parse PHP code string into raw statement nodes (no project/file structure)
187
- - **`parse(code: str) -> AST`**: Parse PHP code string and return AST with project/file structure (backward compatibility)
188
- - **`parse_file(path: str) -> AST`**: Parse a single PHP file and return AST with project/file structure
189
- - **`parse_project(project_path: str, file_filter: Callable[[Path], bool] = lambda p: p.suffix == '.php') -> AST`**: Parse all PHP files in a project directory into a single AST with project structure. `file_filter` function to customize which files are included (defaults to `.php` suffix check)
190
-
191
- ### Classes
192
-
193
- - **`AST`**: Represents the complete AST as a graph
194
- - `node(id)`: Get node by ID
195
- - `nodes(predicate)`: Iterate nodes matching predicate
196
- - `first_node(predicate)`: Get first matching node
197
- - `succ(node, predicate)`: Get successor nodes (children via PARENT_OF edges)
198
- - `prev(node, predicate)`: Get predecessor nodes (parents via PARENT_OF edges)
199
- - `descendants(node, predicate)`: Get all descendants via BFS
200
- - `ancestors(node, predicate)`: Get all ancestors via BFS
201
- - `root_node`: Property returning the root/project node
202
- - `project_node`: Property returning the project node (alias for root_node)
203
- - `files()`: Get all file nodes in the project
204
- - `get_file(node_id)`: Get the file node containing the given node
205
- - `to_json()`: Reconstruct PHP-Parser JSON
206
-
207
- - **`Node`**: Wraps a single AST node
208
- - `id`: Node identifier
209
- - `node_type`: Node type (e.g., "Stmt_Function") - returns `nodeType` property
210
- - `all_properties`: All node properties as dict
211
- - `get_property(*names)`: Get property value
212
- - `start_line`, `end_line`: Line number properties
213
- - `__getitem__(key)`: Dict-like access (e.g., `node["name"]`)
214
- - `get(key, default)`: Get property with default value
215
-
216
- - **`Parser`**: Parses PHP code
217
- - `__init__(php_binary_path=None, php_binary_url=None)`: Initialize parser
218
- - `php_binary_path`: Optional path to local PHP binary
219
- - `php_binary_url`: Optional URL to download PHP binary from
220
- - `parse_code(code)`: Parse code string into list of nodes
221
- - `parse(code)`: Parse code string into AST (backward compatibility)
222
- - `parse_file(path)`: Parse file into AST with project/file structure
223
- - `parse_project(project_path)`: Parse project directory into AST with project structure
224
-
225
- - **`PrettyPrinter`**: Generates PHP code
226
- - `__init__(php_binary_path=None, php_binary_url=None)`: Initialize printer
227
- - `php_binary_path`: Optional path to local PHP binary
228
- - `php_binary_url`: Optional URL to download PHP binary from
229
- - `print(ast) -> dict[str, str]`: Generate code from AST, returns dictionary mapping file paths to generated code
230
- - Returns: `{file_path: code_string, ...}` for each file in the AST
231
- - If AST has no file structure, returns `{"": code_string}`
232
-
233
- ### Exceptions
234
-
235
- - **`ParseError`**: Raised when PHP-Parser reports syntax error
236
- - **`RunnerError`**: Raised when PHP execution fails
237
-
238
- ## AST Structure
239
-
240
- The library supports three parsing modes:
241
-
242
- 1. **Code Parsing** (`parse_code`): Returns flat list of statement nodes
243
- - Node IDs: `node_1`, `node_2`, ...
244
- - No project/file structure
245
-
246
- 2. **File Parsing** (`parse_file`): Creates project → file → statements hierarchy
247
- - Project node: `"project"` (fixed ID)
248
- - File node: 8-character hex hash (e.g., `"a1b2c3d4"`)
249
- - Statement nodes: `{hex}_1`, `{hex}_2`, ...
250
-
251
- 3. **Project Parsing** (`parse_project`): Creates project → multiple files → statements hierarchy
252
- - Single project node: `"project"` (fixed ID)
253
- - Multiple file nodes: Each with unique hex hash
254
- - Statement nodes: Prefixed with their file's hash
255
-
256
- ## Architecture
257
-
258
- The library follows a delegation-based architecture:
259
-
260
- 1. **PHP-Parser** handles all parsing and code generation
261
- 2. **cpg2py Storage** stores the AST as a graph
262
- 3. **Python classes** provide convenient access to the graph
263
-
264
- This design ensures:
265
- - No need to maintain PHP node type definitions in Python
266
- - Automatic compatibility with PHP-Parser updates
267
- - Full access to PHP-Parser's features
268
- - Support for multi-file project analysis with merge capabilities
269
-
270
- ## Requirements
271
-
272
- - Python >= 3.11
273
- - cpg2py
274
- - static-php-py (bundled PHP binaries)
275
-
276
- ## License
277
-
278
- MIT License - see LICENSE file for details.
279
-
280
- ## Documentation
281
-
282
- For detailed design documentation, see:
283
- - [Design Document](docs/design.md)
284
- - [Idea Document](docs/idea.md)
285
- - [API Demo](docs/api_demo.ipynb)
@@ -1,274 +0,0 @@
1
- # php-parser-py
2
-
3
- Python wrapper for PHP-Parser using cpg2py's graph framework.
4
-
5
- ## Overview
6
-
7
- `php-parser-py` provides a Python interface to [PHP-Parser](https://github.com/nikic/PHP-Parser), enabling Abstract Syntax Tree (AST) parsing and manipulation of PHP code. The library integrates with [cpg2py](https://github.com/jhu-seclab/cpg2py)'s graph framework for powerful graph-based code analysis.
8
-
9
- ## Features
10
-
11
- - **Native PHP-Parser Integration**: Delegates all parsing and code generation to PHP-Parser for maximum compatibility
12
- - **Zero-Configuration PHP**: Bundled PHP binaries via static-php-py - no system PHP installation required
13
- - **Graph-Based Analysis**: Uses cpg2py's graph framework for AST traversal and querying
14
- - **Dynamic Node Types**: No hardcoded PHP node types - all types come from PHP-Parser's JSON output
15
- - **Lossless Round-Trip**: Parse → Modify → Generate code with full fidelity
16
- - **Cross-Platform Support**: Pre-built PHP 8.4 binaries for Linux (x86_64/aarch64), macOS (x86_64/arm64), and Windows (x64)
17
-
18
- ## Installation
19
-
20
- ```bash
21
- pip install php-parser-py
22
- ```
23
-
24
- **Note**: Platform-specific wheels include pre-built PHP 8.4 binaries. No separate PHP installation required!
25
-
26
- ## Quick Start
27
-
28
- ### Parse PHP Code (Without Project Structure)
29
-
30
- ```python
31
- from php_parser_py import parse_code
32
-
33
- # Parse PHP code into raw statement nodes
34
- code = """<?php
35
- function greet($name) {
36
- echo "Hello, $name!";
37
- }
38
- """
39
-
40
- nodes = parse_code(code)
41
- # Returns list of Node instances (no project/file structure)
42
-
43
- for node in nodes:
44
- print(f"Node type: {node.node_type}")
45
- ```
46
-
47
- ### Parse PHP File (With Project Structure)
48
-
49
- ```python
50
- from php_parser_py import parse_file
51
-
52
- # Parse a single file with project/file structure
53
- ast = parse_file("example.php")
54
-
55
- # Access project and file nodes
56
- project = ast.project_node
57
- files = ast.files()
58
- print(f"Project has {len(files)} file(s)")
59
-
60
- # Get file containing a specific node
61
- stmt_node = ast.first_node(lambda n: n.node_type == "Stmt_Function")
62
- file_node = ast.get_file(stmt_node.id)
63
- print(f"Function is in file: {file_node.get_property('filePath')}")
64
- ```
65
-
66
- ### Parse Project Directory
67
-
68
- ```python
69
- from php_parser_py import parse_project
70
- from pathlib import Path
71
-
72
- # Parse all PHP files in a project directory (recursively)
73
- ast = parse_project("/path/to/project")
74
-
75
- # Access all files
76
- for file_node in ast.files():
77
- print(f"File: {file_node.get_property('filePath')}")
78
- # Get statements in this file
79
- for stmt in ast.succ(file_node):
80
- print(f" Statement: {stmt.node_type}")
81
-
82
- # Access project and file paths
83
- project = ast.project_node
84
- print(f"Project root: {project.get_property('path')}")
85
-
86
- for file_node in ast.files():
87
- print(f"File relative path: {file_node.get_property('path')}")
88
- print(f"File absolute path: {file_node.get_property('filePath')}")
89
-
90
- # Use custom file filter (e.g., include .php and .phtml files)
91
- ast = parse_project(
92
- "/path/to/project",
93
- file_filter=lambda p: p.suffix in ['.php', '.phtml']
94
- )
95
- ```
96
-
97
- ### Traverse AST
98
-
99
- ```python
100
- from php_parser_py import parse_file
101
-
102
- ast = parse_file("example.php")
103
-
104
- # Find all echo statements
105
- for echo in ast.nodes(lambda n: n.node_type == "Stmt_Echo"):
106
- print(f"Echo at line {echo.start_line}")
107
-
108
- # Get children of a node (via PARENT_OF edges)
109
- for child in ast.succ(some_node):
110
- print(f"Child type: {child.node_type}")
111
-
112
- # Get all descendants
113
- for descendant in ast.descendants(some_node):
114
- print(f"Descendant: {descendant.node_type}")
115
-
116
- # Get file containing a node
117
- stmt_node = ast.first_node(lambda n: n.node_type == "Stmt_Function")
118
- file_node = ast.get_file(stmt_node.id)
119
- if file_node:
120
- print(f"Function is in: {file_node.get_property('filePath')}")
121
-
122
- # Access project structure
123
- project = ast.project_node
124
- files = ast.files()
125
- for file_node in files:
126
- print(f"File: {file_node.get_property('filePath')}")
127
- ```
128
-
129
- ### Generate PHP Code
130
-
131
- ```python
132
- from php_parser_py import parse_file, parse_project, PrettyPrinter
133
-
134
- # Parse single file
135
- ast = parse_file("example.php")
136
-
137
- # Generate code (returns dict mapping file paths to code)
138
- printer = PrettyPrinter()
139
- generated = printer.print(ast)
140
- for file_path, code in generated.items():
141
- print(f"File: {file_path}")
142
- print(code)
143
-
144
- # Parse project directory (recursively finds all PHP files)
145
- ast = parse_project("/path/to/project")
146
-
147
- # Generate code for all files
148
- generated = printer.print(ast)
149
- # Returns: {"src/file1.php": "...", "src/file2.php": "..."}
150
- ```
151
-
152
- ## Advanced Usage
153
-
154
- ### Custom PHP Binary
155
-
156
- If you need to use a specific PHP version or custom binary:
157
-
158
- ```python
159
- from php_parser_py import Parser
160
- from pathlib import Path
161
-
162
- # Use local PHP binary
163
- parser = Parser(php_binary_path=Path("/usr/local/bin/php"))
164
- ast = parser.parse("<?php echo 'hello';")
165
-
166
- # Or download from remote URL
167
- parser = Parser(php_binary_url="https://example.com/php-8.4.zip")
168
- ast = parser.parse("<?php echo 'hello';")
169
- ```
170
-
171
- ## API Reference
172
-
173
- ### Main Functions
174
-
175
- - **`parse_code(code: str) -> list[Node]`**: Parse PHP code string into raw statement nodes (no project/file structure)
176
- - **`parse(code: str) -> AST`**: Parse PHP code string and return AST with project/file structure (backward compatibility)
177
- - **`parse_file(path: str) -> AST`**: Parse a single PHP file and return AST with project/file structure
178
- - **`parse_project(project_path: str, file_filter: Callable[[Path], bool] = lambda p: p.suffix == '.php') -> AST`**: Parse all PHP files in a project directory into a single AST with project structure. `file_filter` function to customize which files are included (defaults to `.php` suffix check)
179
-
180
- ### Classes
181
-
182
- - **`AST`**: Represents the complete AST as a graph
183
- - `node(id)`: Get node by ID
184
- - `nodes(predicate)`: Iterate nodes matching predicate
185
- - `first_node(predicate)`: Get first matching node
186
- - `succ(node, predicate)`: Get successor nodes (children via PARENT_OF edges)
187
- - `prev(node, predicate)`: Get predecessor nodes (parents via PARENT_OF edges)
188
- - `descendants(node, predicate)`: Get all descendants via BFS
189
- - `ancestors(node, predicate)`: Get all ancestors via BFS
190
- - `root_node`: Property returning the root/project node
191
- - `project_node`: Property returning the project node (alias for root_node)
192
- - `files()`: Get all file nodes in the project
193
- - `get_file(node_id)`: Get the file node containing the given node
194
- - `to_json()`: Reconstruct PHP-Parser JSON
195
-
196
- - **`Node`**: Wraps a single AST node
197
- - `id`: Node identifier
198
- - `node_type`: Node type (e.g., "Stmt_Function") - returns `nodeType` property
199
- - `all_properties`: All node properties as dict
200
- - `get_property(*names)`: Get property value
201
- - `start_line`, `end_line`: Line number properties
202
- - `__getitem__(key)`: Dict-like access (e.g., `node["name"]`)
203
- - `get(key, default)`: Get property with default value
204
-
205
- - **`Parser`**: Parses PHP code
206
- - `__init__(php_binary_path=None, php_binary_url=None)`: Initialize parser
207
- - `php_binary_path`: Optional path to local PHP binary
208
- - `php_binary_url`: Optional URL to download PHP binary from
209
- - `parse_code(code)`: Parse code string into list of nodes
210
- - `parse(code)`: Parse code string into AST (backward compatibility)
211
- - `parse_file(path)`: Parse file into AST with project/file structure
212
- - `parse_project(project_path)`: Parse project directory into AST with project structure
213
-
214
- - **`PrettyPrinter`**: Generates PHP code
215
- - `__init__(php_binary_path=None, php_binary_url=None)`: Initialize printer
216
- - `php_binary_path`: Optional path to local PHP binary
217
- - `php_binary_url`: Optional URL to download PHP binary from
218
- - `print(ast) -> dict[str, str]`: Generate code from AST, returns dictionary mapping file paths to generated code
219
- - Returns: `{file_path: code_string, ...}` for each file in the AST
220
- - If AST has no file structure, returns `{"": code_string}`
221
-
222
- ### Exceptions
223
-
224
- - **`ParseError`**: Raised when PHP-Parser reports syntax error
225
- - **`RunnerError`**: Raised when PHP execution fails
226
-
227
- ## AST Structure
228
-
229
- The library supports three parsing modes:
230
-
231
- 1. **Code Parsing** (`parse_code`): Returns flat list of statement nodes
232
- - Node IDs: `node_1`, `node_2`, ...
233
- - No project/file structure
234
-
235
- 2. **File Parsing** (`parse_file`): Creates project → file → statements hierarchy
236
- - Project node: `"project"` (fixed ID)
237
- - File node: 8-character hex hash (e.g., `"a1b2c3d4"`)
238
- - Statement nodes: `{hex}_1`, `{hex}_2`, ...
239
-
240
- 3. **Project Parsing** (`parse_project`): Creates project → multiple files → statements hierarchy
241
- - Single project node: `"project"` (fixed ID)
242
- - Multiple file nodes: Each with unique hex hash
243
- - Statement nodes: Prefixed with their file's hash
244
-
245
- ## Architecture
246
-
247
- The library follows a delegation-based architecture:
248
-
249
- 1. **PHP-Parser** handles all parsing and code generation
250
- 2. **cpg2py Storage** stores the AST as a graph
251
- 3. **Python classes** provide convenient access to the graph
252
-
253
- This design ensures:
254
- - No need to maintain PHP node type definitions in Python
255
- - Automatic compatibility with PHP-Parser updates
256
- - Full access to PHP-Parser's features
257
- - Support for multi-file project analysis with merge capabilities
258
-
259
- ## Requirements
260
-
261
- - Python >= 3.11
262
- - cpg2py
263
- - static-php-py (bundled PHP binaries)
264
-
265
- ## License
266
-
267
- MIT License - see LICENSE file for details.
268
-
269
- ## Documentation
270
-
271
- For detailed design documentation, see:
272
- - [Design Document](docs/design.md)
273
- - [Idea Document](docs/idea.md)
274
- - [API Demo](docs/api_demo.ipynb)