php-parser-py 0.1.1__tar.gz → 1.1.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.
- php_parser_py-1.1.0/PKG-INFO +279 -0
- php_parser_py-1.1.0/README.md +268 -0
- {php_parser_py-0.1.1 → php_parser_py-1.1.0}/pyproject.toml +1 -1
- php_parser_py-1.1.0/src/php_parser_py/__init__.py +138 -0
- php_parser_py-1.1.0/src/php_parser_py/_ast.py +357 -0
- php_parser_py-1.1.0/src/php_parser_py/_parser.py +389 -0
- php_parser_py-1.1.0/src/php_parser_py/_printer.py +109 -0
- php_parser_py-0.1.1/PKG-INFO +0 -190
- php_parser_py-0.1.1/README.md +0 -179
- php_parser_py-0.1.1/src/php_parser_py/__init__.py +0 -86
- php_parser_py-0.1.1/src/php_parser_py/_ast.py +0 -190
- php_parser_py-0.1.1/src/php_parser_py/_parser.py +0 -207
- php_parser_py-0.1.1/src/php_parser_py/_printer.py +0 -52
- {php_parser_py-0.1.1 → php_parser_py-1.1.0}/src/php_parser_py/_edge.py +0 -0
- {php_parser_py-0.1.1 → php_parser_py-1.1.0}/src/php_parser_py/_node.py +0 -0
- {php_parser_py-0.1.1 → php_parser_py-1.1.0}/src/php_parser_py/_resources.py +0 -0
- {php_parser_py-0.1.1 → php_parser_py-1.1.0}/src/php_parser_py/_runner.py +0 -0
- {php_parser_py-0.1.1 → php_parser_py-1.1.0}/src/php_parser_py/exceptions.py +0 -0
- {php_parser_py-0.1.1 → php_parser_py-1.1.0}/src/php_parser_py/resources/php-parser-4.19.4.zip +0 -0
- {php_parser_py-0.1.1 → php_parser_py-1.1.0}/src/php_parser_py/vendor/.extracted +0 -0
- {php_parser_py-0.1.1 → php_parser_py-1.1.0}/src/php_parser_py/vendor/LICENSE.txt +0 -0
- {php_parser_py-0.1.1 → php_parser_py-1.1.0}/src/php_parser_py/vendor/php-parser.phar +0 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: php-parser-py
|
|
3
|
+
Version: 1.1.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.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 Multiple Files (Project Structure)
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from php_parser_py import parse_project
|
|
81
|
+
|
|
82
|
+
# Parse multiple files into a single AST
|
|
83
|
+
ast = parse_project(["file1.php", "file2.php", "file3.php"])
|
|
84
|
+
|
|
85
|
+
# Access all files
|
|
86
|
+
for file_node in ast.files():
|
|
87
|
+
print(f"File: {file_node.get_property('filePath')}")
|
|
88
|
+
# Get statements in this file
|
|
89
|
+
for stmt in ast.succ(file_node):
|
|
90
|
+
print(f" Statement: {stmt.node_type}")
|
|
91
|
+
|
|
92
|
+
# Access project and file paths
|
|
93
|
+
ast = parse_project(["file1.php", "subdir/file2.php"])
|
|
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
|
+
|
|
102
|
+
### Traverse AST
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from php_parser_py import parse_file
|
|
106
|
+
|
|
107
|
+
ast = parse_file("example.php")
|
|
108
|
+
|
|
109
|
+
# Find all echo statements
|
|
110
|
+
for echo in ast.nodes(lambda n: n.node_type == "Stmt_Echo"):
|
|
111
|
+
print(f"Echo at line {echo.start_line}")
|
|
112
|
+
|
|
113
|
+
# Get children of a node (via PARENT_OF edges)
|
|
114
|
+
for child in ast.succ(some_node):
|
|
115
|
+
print(f"Child type: {child.node_type}")
|
|
116
|
+
|
|
117
|
+
# Get all descendants
|
|
118
|
+
for descendant in ast.descendants(some_node):
|
|
119
|
+
print(f"Descendant: {descendant.node_type}")
|
|
120
|
+
|
|
121
|
+
# Get file containing a node
|
|
122
|
+
stmt_node = ast.first_node(lambda n: n.node_type == "Stmt_Function")
|
|
123
|
+
file_node = ast.get_file(stmt_node.id)
|
|
124
|
+
if file_node:
|
|
125
|
+
print(f"Function is in: {file_node.get_property('filePath')}")
|
|
126
|
+
|
|
127
|
+
# Access project structure
|
|
128
|
+
project = ast.project_node
|
|
129
|
+
files = ast.files()
|
|
130
|
+
for file_node in files:
|
|
131
|
+
print(f"File: {file_node.get_property('filePath')}")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Generate PHP Code
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from php_parser_py import parse_file, parse_project, PrettyPrinter
|
|
138
|
+
|
|
139
|
+
# Parse single file
|
|
140
|
+
ast = parse_file("example.php")
|
|
141
|
+
|
|
142
|
+
# Generate code (returns dict mapping file paths to code)
|
|
143
|
+
printer = PrettyPrinter()
|
|
144
|
+
generated = printer.print(ast)
|
|
145
|
+
for file_path, code in generated.items():
|
|
146
|
+
print(f"File: {file_path}")
|
|
147
|
+
print(code)
|
|
148
|
+
|
|
149
|
+
# Parse multiple files
|
|
150
|
+
ast = parse_project(["file1.php", "file2.php"])
|
|
151
|
+
|
|
152
|
+
# Generate code for all files
|
|
153
|
+
generated = printer.print(ast)
|
|
154
|
+
# Returns: {"file1.php": "...", "file2.php": "..."}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Advanced Usage
|
|
158
|
+
|
|
159
|
+
### Custom PHP Binary
|
|
160
|
+
|
|
161
|
+
If you need to use a specific PHP version or custom binary:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from php_parser_py import Parser
|
|
165
|
+
from pathlib import Path
|
|
166
|
+
|
|
167
|
+
# Use local PHP binary
|
|
168
|
+
parser = Parser(php_binary_path=Path("/usr/local/bin/php"))
|
|
169
|
+
ast = parser.parse("<?php echo 'hello';")
|
|
170
|
+
|
|
171
|
+
# Or download from remote URL
|
|
172
|
+
parser = Parser(php_binary_url="https://example.com/php-8.4.zip")
|
|
173
|
+
ast = parser.parse("<?php echo 'hello';")
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## API Reference
|
|
177
|
+
|
|
178
|
+
### Main Functions
|
|
179
|
+
|
|
180
|
+
- **`parse_code(code: str) -> list[Node]`**: Parse PHP code string into raw statement nodes (no project/file structure)
|
|
181
|
+
- **`parse(code: str) -> AST`**: Parse PHP code string and return AST with project/file structure (backward compatibility)
|
|
182
|
+
- **`parse_file(path: str) -> AST`**: Parse a single PHP file and return AST with project/file structure
|
|
183
|
+
- **`parse_project(paths: list[str]) -> AST`**: Parse multiple PHP files into a single AST with project structure
|
|
184
|
+
|
|
185
|
+
### Classes
|
|
186
|
+
|
|
187
|
+
- **`AST`**: Represents the complete AST as a graph
|
|
188
|
+
- `node(id)`: Get node by ID
|
|
189
|
+
- `nodes(predicate)`: Iterate nodes matching predicate
|
|
190
|
+
- `first_node(predicate)`: Get first matching node
|
|
191
|
+
- `succ(node, predicate)`: Get successor nodes (children via PARENT_OF edges)
|
|
192
|
+
- `prev(node, predicate)`: Get predecessor nodes (parents via PARENT_OF edges)
|
|
193
|
+
- `descendants(node, predicate)`: Get all descendants via BFS
|
|
194
|
+
- `ancestors(node, predicate)`: Get all ancestors via BFS
|
|
195
|
+
- `root_node`: Property returning the root/project node
|
|
196
|
+
- `project_node`: Property returning the project node (alias for root_node)
|
|
197
|
+
- `files()`: Get all file nodes in the project
|
|
198
|
+
- `get_file(node_id)`: Get the file node containing the given node
|
|
199
|
+
- `to_json()`: Reconstruct PHP-Parser JSON
|
|
200
|
+
|
|
201
|
+
- **`Node`**: Wraps a single AST node
|
|
202
|
+
- `id`: Node identifier
|
|
203
|
+
- `node_type`: Node type (e.g., "Stmt_Function") - returns `nodeType` property
|
|
204
|
+
- `all_properties`: All node properties as dict
|
|
205
|
+
- `get_property(*names)`: Get property value
|
|
206
|
+
- `start_line`, `end_line`: Line number properties
|
|
207
|
+
- `__getitem__(key)`: Dict-like access (e.g., `node["name"]`)
|
|
208
|
+
- `get(key, default)`: Get property with default value
|
|
209
|
+
|
|
210
|
+
- **`Parser`**: Parses PHP code
|
|
211
|
+
- `__init__(php_binary_path=None, php_binary_url=None)`: Initialize parser
|
|
212
|
+
- `php_binary_path`: Optional path to local PHP binary
|
|
213
|
+
- `php_binary_url`: Optional URL to download PHP binary from
|
|
214
|
+
- `parse_code(code)`: Parse code string into list of nodes
|
|
215
|
+
- `parse(code)`: Parse code string into AST (backward compatibility)
|
|
216
|
+
- `parse_file(path)`: Parse file into AST with project/file structure
|
|
217
|
+
- `parse_project(paths)`: Parse multiple files into AST with project structure
|
|
218
|
+
|
|
219
|
+
- **`PrettyPrinter`**: Generates PHP code
|
|
220
|
+
- `__init__(php_binary_path=None, php_binary_url=None)`: Initialize printer
|
|
221
|
+
- `php_binary_path`: Optional path to local PHP binary
|
|
222
|
+
- `php_binary_url`: Optional URL to download PHP binary from
|
|
223
|
+
- `print(ast) -> dict[str, str]`: Generate code from AST, returns dictionary mapping file paths to generated code
|
|
224
|
+
- Returns: `{file_path: code_string, ...}` for each file in the AST
|
|
225
|
+
- If AST has no file structure, returns `{"": code_string}`
|
|
226
|
+
|
|
227
|
+
### Exceptions
|
|
228
|
+
|
|
229
|
+
- **`ParseError`**: Raised when PHP-Parser reports syntax error
|
|
230
|
+
- **`RunnerError`**: Raised when PHP execution fails
|
|
231
|
+
|
|
232
|
+
## AST Structure
|
|
233
|
+
|
|
234
|
+
The library supports three parsing modes:
|
|
235
|
+
|
|
236
|
+
1. **Code Parsing** (`parse_code`): Returns flat list of statement nodes
|
|
237
|
+
- Node IDs: `node_1`, `node_2`, ...
|
|
238
|
+
- No project/file structure
|
|
239
|
+
|
|
240
|
+
2. **File Parsing** (`parse_file`): Creates project → file → statements hierarchy
|
|
241
|
+
- Project node: `"project"` (fixed ID)
|
|
242
|
+
- File node: 8-character hex hash (e.g., `"a1b2c3d4"`)
|
|
243
|
+
- Statement nodes: `{hex}_1`, `{hex}_2`, ...
|
|
244
|
+
|
|
245
|
+
3. **Project Parsing** (`parse_project`): Creates project → multiple files → statements hierarchy
|
|
246
|
+
- Single project node: `"project"` (fixed ID)
|
|
247
|
+
- Multiple file nodes: Each with unique hex hash
|
|
248
|
+
- Statement nodes: Prefixed with their file's hash
|
|
249
|
+
|
|
250
|
+
## Architecture
|
|
251
|
+
|
|
252
|
+
The library follows a delegation-based architecture:
|
|
253
|
+
|
|
254
|
+
1. **PHP-Parser** handles all parsing and code generation
|
|
255
|
+
2. **cpg2py Storage** stores the AST as a graph
|
|
256
|
+
3. **Python classes** provide convenient access to the graph
|
|
257
|
+
|
|
258
|
+
This design ensures:
|
|
259
|
+
- No need to maintain PHP node type definitions in Python
|
|
260
|
+
- Automatic compatibility with PHP-Parser updates
|
|
261
|
+
- Full access to PHP-Parser's features
|
|
262
|
+
- Support for multi-file project analysis with merge capabilities
|
|
263
|
+
|
|
264
|
+
## Requirements
|
|
265
|
+
|
|
266
|
+
- Python >= 3.11
|
|
267
|
+
- cpg2py
|
|
268
|
+
- static-php-py (bundled PHP binaries)
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
MIT License - see LICENSE file for details.
|
|
273
|
+
|
|
274
|
+
## Documentation
|
|
275
|
+
|
|
276
|
+
For detailed design documentation, see:
|
|
277
|
+
- [Design Document](docs/design.md)
|
|
278
|
+
- [Idea Document](docs/idea.md)
|
|
279
|
+
- [API Demo](docs/api_demo.ipynb)
|
|
@@ -0,0 +1,268 @@
|
|
|
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 Multiple Files (Project Structure)
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from php_parser_py import parse_project
|
|
70
|
+
|
|
71
|
+
# Parse multiple files into a single AST
|
|
72
|
+
ast = parse_project(["file1.php", "file2.php", "file3.php"])
|
|
73
|
+
|
|
74
|
+
# Access all files
|
|
75
|
+
for file_node in ast.files():
|
|
76
|
+
print(f"File: {file_node.get_property('filePath')}")
|
|
77
|
+
# Get statements in this file
|
|
78
|
+
for stmt in ast.succ(file_node):
|
|
79
|
+
print(f" Statement: {stmt.node_type}")
|
|
80
|
+
|
|
81
|
+
# Access project and file paths
|
|
82
|
+
ast = parse_project(["file1.php", "subdir/file2.php"])
|
|
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
|
+
|
|
91
|
+
### Traverse AST
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from php_parser_py import parse_file
|
|
95
|
+
|
|
96
|
+
ast = parse_file("example.php")
|
|
97
|
+
|
|
98
|
+
# Find all echo statements
|
|
99
|
+
for echo in ast.nodes(lambda n: n.node_type == "Stmt_Echo"):
|
|
100
|
+
print(f"Echo at line {echo.start_line}")
|
|
101
|
+
|
|
102
|
+
# Get children of a node (via PARENT_OF edges)
|
|
103
|
+
for child in ast.succ(some_node):
|
|
104
|
+
print(f"Child type: {child.node_type}")
|
|
105
|
+
|
|
106
|
+
# Get all descendants
|
|
107
|
+
for descendant in ast.descendants(some_node):
|
|
108
|
+
print(f"Descendant: {descendant.node_type}")
|
|
109
|
+
|
|
110
|
+
# Get file containing a node
|
|
111
|
+
stmt_node = ast.first_node(lambda n: n.node_type == "Stmt_Function")
|
|
112
|
+
file_node = ast.get_file(stmt_node.id)
|
|
113
|
+
if file_node:
|
|
114
|
+
print(f"Function is in: {file_node.get_property('filePath')}")
|
|
115
|
+
|
|
116
|
+
# Access project structure
|
|
117
|
+
project = ast.project_node
|
|
118
|
+
files = ast.files()
|
|
119
|
+
for file_node in files:
|
|
120
|
+
print(f"File: {file_node.get_property('filePath')}")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Generate PHP Code
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from php_parser_py import parse_file, parse_project, PrettyPrinter
|
|
127
|
+
|
|
128
|
+
# Parse single file
|
|
129
|
+
ast = parse_file("example.php")
|
|
130
|
+
|
|
131
|
+
# Generate code (returns dict mapping file paths to code)
|
|
132
|
+
printer = PrettyPrinter()
|
|
133
|
+
generated = printer.print(ast)
|
|
134
|
+
for file_path, code in generated.items():
|
|
135
|
+
print(f"File: {file_path}")
|
|
136
|
+
print(code)
|
|
137
|
+
|
|
138
|
+
# Parse multiple files
|
|
139
|
+
ast = parse_project(["file1.php", "file2.php"])
|
|
140
|
+
|
|
141
|
+
# Generate code for all files
|
|
142
|
+
generated = printer.print(ast)
|
|
143
|
+
# Returns: {"file1.php": "...", "file2.php": "..."}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Advanced Usage
|
|
147
|
+
|
|
148
|
+
### Custom PHP Binary
|
|
149
|
+
|
|
150
|
+
If you need to use a specific PHP version or custom binary:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from php_parser_py import Parser
|
|
154
|
+
from pathlib import Path
|
|
155
|
+
|
|
156
|
+
# Use local PHP binary
|
|
157
|
+
parser = Parser(php_binary_path=Path("/usr/local/bin/php"))
|
|
158
|
+
ast = parser.parse("<?php echo 'hello';")
|
|
159
|
+
|
|
160
|
+
# Or download from remote URL
|
|
161
|
+
parser = Parser(php_binary_url="https://example.com/php-8.4.zip")
|
|
162
|
+
ast = parser.parse("<?php echo 'hello';")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## API Reference
|
|
166
|
+
|
|
167
|
+
### Main Functions
|
|
168
|
+
|
|
169
|
+
- **`parse_code(code: str) -> list[Node]`**: Parse PHP code string into raw statement nodes (no project/file structure)
|
|
170
|
+
- **`parse(code: str) -> AST`**: Parse PHP code string and return AST with project/file structure (backward compatibility)
|
|
171
|
+
- **`parse_file(path: str) -> AST`**: Parse a single PHP file and return AST with project/file structure
|
|
172
|
+
- **`parse_project(paths: list[str]) -> AST`**: Parse multiple PHP files into a single AST with project structure
|
|
173
|
+
|
|
174
|
+
### Classes
|
|
175
|
+
|
|
176
|
+
- **`AST`**: Represents the complete AST as a graph
|
|
177
|
+
- `node(id)`: Get node by ID
|
|
178
|
+
- `nodes(predicate)`: Iterate nodes matching predicate
|
|
179
|
+
- `first_node(predicate)`: Get first matching node
|
|
180
|
+
- `succ(node, predicate)`: Get successor nodes (children via PARENT_OF edges)
|
|
181
|
+
- `prev(node, predicate)`: Get predecessor nodes (parents via PARENT_OF edges)
|
|
182
|
+
- `descendants(node, predicate)`: Get all descendants via BFS
|
|
183
|
+
- `ancestors(node, predicate)`: Get all ancestors via BFS
|
|
184
|
+
- `root_node`: Property returning the root/project node
|
|
185
|
+
- `project_node`: Property returning the project node (alias for root_node)
|
|
186
|
+
- `files()`: Get all file nodes in the project
|
|
187
|
+
- `get_file(node_id)`: Get the file node containing the given node
|
|
188
|
+
- `to_json()`: Reconstruct PHP-Parser JSON
|
|
189
|
+
|
|
190
|
+
- **`Node`**: Wraps a single AST node
|
|
191
|
+
- `id`: Node identifier
|
|
192
|
+
- `node_type`: Node type (e.g., "Stmt_Function") - returns `nodeType` property
|
|
193
|
+
- `all_properties`: All node properties as dict
|
|
194
|
+
- `get_property(*names)`: Get property value
|
|
195
|
+
- `start_line`, `end_line`: Line number properties
|
|
196
|
+
- `__getitem__(key)`: Dict-like access (e.g., `node["name"]`)
|
|
197
|
+
- `get(key, default)`: Get property with default value
|
|
198
|
+
|
|
199
|
+
- **`Parser`**: Parses PHP code
|
|
200
|
+
- `__init__(php_binary_path=None, php_binary_url=None)`: Initialize parser
|
|
201
|
+
- `php_binary_path`: Optional path to local PHP binary
|
|
202
|
+
- `php_binary_url`: Optional URL to download PHP binary from
|
|
203
|
+
- `parse_code(code)`: Parse code string into list of nodes
|
|
204
|
+
- `parse(code)`: Parse code string into AST (backward compatibility)
|
|
205
|
+
- `parse_file(path)`: Parse file into AST with project/file structure
|
|
206
|
+
- `parse_project(paths)`: Parse multiple files into AST with project structure
|
|
207
|
+
|
|
208
|
+
- **`PrettyPrinter`**: Generates PHP code
|
|
209
|
+
- `__init__(php_binary_path=None, php_binary_url=None)`: Initialize printer
|
|
210
|
+
- `php_binary_path`: Optional path to local PHP binary
|
|
211
|
+
- `php_binary_url`: Optional URL to download PHP binary from
|
|
212
|
+
- `print(ast) -> dict[str, str]`: Generate code from AST, returns dictionary mapping file paths to generated code
|
|
213
|
+
- Returns: `{file_path: code_string, ...}` for each file in the AST
|
|
214
|
+
- If AST has no file structure, returns `{"": code_string}`
|
|
215
|
+
|
|
216
|
+
### Exceptions
|
|
217
|
+
|
|
218
|
+
- **`ParseError`**: Raised when PHP-Parser reports syntax error
|
|
219
|
+
- **`RunnerError`**: Raised when PHP execution fails
|
|
220
|
+
|
|
221
|
+
## AST Structure
|
|
222
|
+
|
|
223
|
+
The library supports three parsing modes:
|
|
224
|
+
|
|
225
|
+
1. **Code Parsing** (`parse_code`): Returns flat list of statement nodes
|
|
226
|
+
- Node IDs: `node_1`, `node_2`, ...
|
|
227
|
+
- No project/file structure
|
|
228
|
+
|
|
229
|
+
2. **File Parsing** (`parse_file`): Creates project → file → statements hierarchy
|
|
230
|
+
- Project node: `"project"` (fixed ID)
|
|
231
|
+
- File node: 8-character hex hash (e.g., `"a1b2c3d4"`)
|
|
232
|
+
- Statement nodes: `{hex}_1`, `{hex}_2`, ...
|
|
233
|
+
|
|
234
|
+
3. **Project Parsing** (`parse_project`): Creates project → multiple files → statements hierarchy
|
|
235
|
+
- Single project node: `"project"` (fixed ID)
|
|
236
|
+
- Multiple file nodes: Each with unique hex hash
|
|
237
|
+
- Statement nodes: Prefixed with their file's hash
|
|
238
|
+
|
|
239
|
+
## Architecture
|
|
240
|
+
|
|
241
|
+
The library follows a delegation-based architecture:
|
|
242
|
+
|
|
243
|
+
1. **PHP-Parser** handles all parsing and code generation
|
|
244
|
+
2. **cpg2py Storage** stores the AST as a graph
|
|
245
|
+
3. **Python classes** provide convenient access to the graph
|
|
246
|
+
|
|
247
|
+
This design ensures:
|
|
248
|
+
- No need to maintain PHP node type definitions in Python
|
|
249
|
+
- Automatic compatibility with PHP-Parser updates
|
|
250
|
+
- Full access to PHP-Parser's features
|
|
251
|
+
- Support for multi-file project analysis with merge capabilities
|
|
252
|
+
|
|
253
|
+
## Requirements
|
|
254
|
+
|
|
255
|
+
- Python >= 3.11
|
|
256
|
+
- cpg2py
|
|
257
|
+
- static-php-py (bundled PHP binaries)
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
MIT License - see LICENSE file for details.
|
|
262
|
+
|
|
263
|
+
## Documentation
|
|
264
|
+
|
|
265
|
+
For detailed design documentation, see:
|
|
266
|
+
- [Design Document](docs/design.md)
|
|
267
|
+
- [Idea Document](docs/idea.md)
|
|
268
|
+
- [API Demo](docs/api_demo.ipynb)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
php-parser-py: Python wrapper for PHP-Parser using cpg2py.
|
|
3
|
+
|
|
4
|
+
This package provides a Python interface to PHP-Parser, enabling AST parsing
|
|
5
|
+
and manipulation of PHP code.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from php_parser_py._parser import Parser as _Parser
|
|
9
|
+
from php_parser_py._printer import PrettyPrinter as _PrettyPrinter
|
|
10
|
+
from php_parser_py._resources import ensure_php_parser_extracted
|
|
11
|
+
|
|
12
|
+
# Ensure PHP-Parser is extracted on first import
|
|
13
|
+
ensure_php_parser_extracted()
|
|
14
|
+
|
|
15
|
+
# Import and expose public classes directly
|
|
16
|
+
from php_parser_py._ast import AST
|
|
17
|
+
from php_parser_py._node import Node
|
|
18
|
+
from php_parser_py._edge import Edge
|
|
19
|
+
from php_parser_py.exceptions import ParseError, RunnerError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Parser(_Parser):
|
|
23
|
+
"""Parses PHP source code using PHP-Parser.
|
|
24
|
+
|
|
25
|
+
Invokes PHP-Parser to parse PHP code and converts the resulting JSON
|
|
26
|
+
to cpg2py Storage format for graph-based analysis.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PrettyPrinter(_PrettyPrinter):
|
|
31
|
+
"""Converts AST to PHP code using PHP-Parser's PrettyPrinter.
|
|
32
|
+
|
|
33
|
+
Reconstructs JSON from AST graph and invokes PHP-Parser to generate
|
|
34
|
+
formatted PHP source code.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_code(code: str) -> list[Node]:
|
|
39
|
+
"""Parse PHP code string into a list of top-level statement nodes.
|
|
40
|
+
|
|
41
|
+
Does not create project or file nodes. Returns raw statement nodes.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
code: PHP source code string.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
List of Node instances representing top-level statements.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ParseError: If PHP-Parser reports syntax error.
|
|
51
|
+
"""
|
|
52
|
+
parser = Parser()
|
|
53
|
+
return parser.parse_code(code)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def parse(code: str) -> AST:
|
|
57
|
+
"""Parse PHP code string into an AST (backward compatibility).
|
|
58
|
+
|
|
59
|
+
This is a convenience function that creates a temporary project/file structure.
|
|
60
|
+
For code-only parsing without project structure, use parse_code() instead.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
code: PHP source code string.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
AST instance with project -> file -> statements hierarchy.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ParseError: If PHP-Parser reports syntax error.
|
|
70
|
+
"""
|
|
71
|
+
# Create a temporary file-like structure for backward compatibility
|
|
72
|
+
import tempfile
|
|
73
|
+
from pathlib import Path
|
|
74
|
+
|
|
75
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.php', delete=False) as f:
|
|
76
|
+
f.write(code)
|
|
77
|
+
temp_path = f.name
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
return parse_file(temp_path)
|
|
81
|
+
finally:
|
|
82
|
+
Path(temp_path).unlink()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def parse_file(path: str) -> AST:
|
|
86
|
+
"""Read and parse a single PHP file into an AST.
|
|
87
|
+
|
|
88
|
+
Creates a project node and a file node. The file node contains
|
|
89
|
+
all statements from the file.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
path: File path string.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
AST instance with project -> file -> statements hierarchy.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
ParseError: If PHP-Parser reports syntax error.
|
|
99
|
+
FileNotFoundError: If file does not exist.
|
|
100
|
+
"""
|
|
101
|
+
parser = Parser()
|
|
102
|
+
return parser.parse_file(path)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def parse_project(paths: list[str], project_path: str | None = None) -> AST:
|
|
106
|
+
"""Parse multiple PHP files into an AST.
|
|
107
|
+
|
|
108
|
+
Creates a project node and multiple file nodes. Each file node
|
|
109
|
+
contains statements from its corresponding file.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
paths: List of file path strings.
|
|
113
|
+
project_path: Optional project root path. If not provided, computed as common parent.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
AST instance with project -> files -> statements hierarchy.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
ParseError: If any file has syntax errors.
|
|
120
|
+
FileNotFoundError: If any file does not exist.
|
|
121
|
+
"""
|
|
122
|
+
parser = Parser()
|
|
123
|
+
return parser.parse_project(paths, project_path=project_path)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
__version__ = "0.1.0"
|
|
127
|
+
__all__ = [
|
|
128
|
+
"AST",
|
|
129
|
+
"Node",
|
|
130
|
+
"Edge",
|
|
131
|
+
"Parser",
|
|
132
|
+
"PrettyPrinter",
|
|
133
|
+
"ParseError",
|
|
134
|
+
"RunnerError",
|
|
135
|
+
"parse_code",
|
|
136
|
+
"parse_file",
|
|
137
|
+
"parse_project",
|
|
138
|
+
]
|