php-parser-py 0.1.1__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-0.1.1/PKG-INFO +190 -0
- php_parser_py-0.1.1/README.md +179 -0
- php_parser_py-0.1.1/pyproject.toml +29 -0
- php_parser_py-0.1.1/src/php_parser_py/__init__.py +86 -0
- php_parser_py-0.1.1/src/php_parser_py/_ast.py +190 -0
- php_parser_py-0.1.1/src/php_parser_py/_edge.py +130 -0
- php_parser_py-0.1.1/src/php_parser_py/_node.py +208 -0
- php_parser_py-0.1.1/src/php_parser_py/_parser.py +207 -0
- php_parser_py-0.1.1/src/php_parser_py/_printer.py +52 -0
- php_parser_py-0.1.1/src/php_parser_py/_resources.py +157 -0
- php_parser_py-0.1.1/src/php_parser_py/_runner.py +239 -0
- php_parser_py-0.1.1/src/php_parser_py/exceptions.py +44 -0
- php_parser_py-0.1.1/src/php_parser_py/resources/php-parser-4.19.4.zip +0 -0
- php_parser_py-0.1.1/src/php_parser_py/vendor/.extracted +1 -0
- php_parser_py-0.1.1/src/php_parser_py/vendor/LICENSE.txt +29 -0
- php_parser_py-0.1.1/src/php_parser_py/vendor/php-parser.phar +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: php-parser-py
|
|
3
|
+
Version: 0.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
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from php_parser_py import parse
|
|
43
|
+
|
|
44
|
+
# Parse PHP code
|
|
45
|
+
code = """<?php
|
|
46
|
+
function greet($name) {
|
|
47
|
+
echo "Hello, $name!";
|
|
48
|
+
}
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
ast = parse(code)
|
|
52
|
+
|
|
53
|
+
# Query nodes by type
|
|
54
|
+
for func in ast.nodes(lambda n: n.label == "Stmt_Function"):
|
|
55
|
+
name = func.get_property("name")
|
|
56
|
+
print(f"Found function: {name}")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Parse PHP File
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from php_parser_py import parse_file
|
|
63
|
+
|
|
64
|
+
ast = parse_file("example.php")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Traverse AST
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# Find all echo statements
|
|
71
|
+
for echo in ast.nodes(lambda n: n.label == "Stmt_Echo"):
|
|
72
|
+
print(f"Echo at line {echo.get_property('startLine')}")
|
|
73
|
+
|
|
74
|
+
# Get children of a node
|
|
75
|
+
for child in ast.children(some_node):
|
|
76
|
+
print(f"Child type: {child.label}")
|
|
77
|
+
|
|
78
|
+
# Get all descendants
|
|
79
|
+
for descendant in ast.descendants(some_node):
|
|
80
|
+
print(f"Descendant: {descendant.label}")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Generate PHP Code
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from php_parser_py import parse, PrettyPrinter
|
|
87
|
+
|
|
88
|
+
# Parse code
|
|
89
|
+
ast = parse("<?php echo 'hello';")
|
|
90
|
+
|
|
91
|
+
# Modify AST (example: change properties)
|
|
92
|
+
# ... modify nodes as needed ...
|
|
93
|
+
|
|
94
|
+
# Generate code
|
|
95
|
+
printer = PrettyPrinter()
|
|
96
|
+
generated_code = printer.print(ast)
|
|
97
|
+
print(generated_code)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Advanced Usage
|
|
101
|
+
|
|
102
|
+
### Custom PHP Binary
|
|
103
|
+
|
|
104
|
+
If you need to use a specific PHP version or custom binary:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from php_parser_py import Parser
|
|
108
|
+
from pathlib import Path
|
|
109
|
+
|
|
110
|
+
# Use local PHP binary
|
|
111
|
+
parser = Parser(php_binary_path=Path("/usr/local/bin/php"))
|
|
112
|
+
ast = parser.parse("<?php echo 'hello';")
|
|
113
|
+
|
|
114
|
+
# Or download from remote URL
|
|
115
|
+
parser = Parser(php_binary_url="https://example.com/php-8.4.zip")
|
|
116
|
+
ast = parser.parse("<?php echo 'hello';")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## API Reference
|
|
120
|
+
|
|
121
|
+
### Main Functions
|
|
122
|
+
|
|
123
|
+
- **`parse(code: str) -> AST`**: Parse PHP code string and return AST
|
|
124
|
+
- **`parse_file(path: str) -> AST`**: Parse PHP file and return AST
|
|
125
|
+
|
|
126
|
+
### Classes
|
|
127
|
+
|
|
128
|
+
- **`AST`**: Represents the complete AST as a graph
|
|
129
|
+
- `node(id)`: Get node by ID
|
|
130
|
+
- `nodes(predicate)`: Iterate nodes matching predicate
|
|
131
|
+
- `first_node(predicate)`: Get first matching node
|
|
132
|
+
- `children(node)`: Get child nodes
|
|
133
|
+
- `descendants(node)`: Get all descendants
|
|
134
|
+
- `to_json()`: Reconstruct PHP-Parser JSON
|
|
135
|
+
|
|
136
|
+
- **`Node`**: Wraps a single AST node
|
|
137
|
+
- `id`: Node identifier
|
|
138
|
+
- `label`: Node type (e.g., "Stmt_Function")
|
|
139
|
+
- `properties`: All node properties
|
|
140
|
+
- `get_property(*names)`: Get property value
|
|
141
|
+
|
|
142
|
+
- **`Parser`**: Parses PHP code
|
|
143
|
+
- `__init__(php_binary_path=None, php_binary_url=None)`: Initialize parser
|
|
144
|
+
- `php_binary_path`: Optional path to local PHP binary
|
|
145
|
+
- `php_binary_url`: Optional URL to download PHP binary from
|
|
146
|
+
- `parse(code, prefix="")`: Parse code string
|
|
147
|
+
- `prefix`: Optional prefix for node IDs (useful for multi-file parsing)
|
|
148
|
+
- `parse_file(path, prefix=None)`: Parse file
|
|
149
|
+
- `prefix`: Optional prefix (auto-generated from file path if not provided)
|
|
150
|
+
|
|
151
|
+
- **`PrettyPrinter`**: Generates PHP code
|
|
152
|
+
- `__init__(php_binary_path=None, php_binary_url=None)`: Initialize printer
|
|
153
|
+
- `php_binary_path`: Optional path to local PHP binary
|
|
154
|
+
- `php_binary_url`: Optional URL to download PHP binary from
|
|
155
|
+
- `print(ast)`: Generate code from AST
|
|
156
|
+
|
|
157
|
+
### Exceptions
|
|
158
|
+
|
|
159
|
+
- **`ParseError`**: Raised when PHP-Parser reports syntax error
|
|
160
|
+
- **`RunnerError`**: Raised when PHP execution fails
|
|
161
|
+
|
|
162
|
+
## Architecture
|
|
163
|
+
|
|
164
|
+
The library follows a delegation-based architecture:
|
|
165
|
+
|
|
166
|
+
1. **PHP-Parser** handles all parsing and code generation
|
|
167
|
+
2. **cpg2py Storage** stores the AST as a graph
|
|
168
|
+
3. **Python classes** provide convenient access to the graph
|
|
169
|
+
|
|
170
|
+
This design ensures:
|
|
171
|
+
- No need to maintain PHP node type definitions in Python
|
|
172
|
+
- Automatic compatibility with PHP-Parser updates
|
|
173
|
+
- Full access to PHP-Parser's features
|
|
174
|
+
|
|
175
|
+
## Requirements
|
|
176
|
+
|
|
177
|
+
- Python >= 3.11
|
|
178
|
+
- cpg2py
|
|
179
|
+
- static-php-py (bundled PHP binaries)
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT License - see LICENSE file for details.
|
|
184
|
+
|
|
185
|
+
## Documentation
|
|
186
|
+
|
|
187
|
+
For detailed design documentation, see:
|
|
188
|
+
- [Design Document](docs/design.md)
|
|
189
|
+
- [Idea Document](docs/idea.md)
|
|
190
|
+
- [API Demo](docs/api_demo.ipynb)
|
|
@@ -0,0 +1,179 @@
|
|
|
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
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from php_parser_py import parse
|
|
32
|
+
|
|
33
|
+
# Parse PHP code
|
|
34
|
+
code = """<?php
|
|
35
|
+
function greet($name) {
|
|
36
|
+
echo "Hello, $name!";
|
|
37
|
+
}
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
ast = parse(code)
|
|
41
|
+
|
|
42
|
+
# Query nodes by type
|
|
43
|
+
for func in ast.nodes(lambda n: n.label == "Stmt_Function"):
|
|
44
|
+
name = func.get_property("name")
|
|
45
|
+
print(f"Found function: {name}")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Parse PHP File
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from php_parser_py import parse_file
|
|
52
|
+
|
|
53
|
+
ast = parse_file("example.php")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Traverse AST
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
# Find all echo statements
|
|
60
|
+
for echo in ast.nodes(lambda n: n.label == "Stmt_Echo"):
|
|
61
|
+
print(f"Echo at line {echo.get_property('startLine')}")
|
|
62
|
+
|
|
63
|
+
# Get children of a node
|
|
64
|
+
for child in ast.children(some_node):
|
|
65
|
+
print(f"Child type: {child.label}")
|
|
66
|
+
|
|
67
|
+
# Get all descendants
|
|
68
|
+
for descendant in ast.descendants(some_node):
|
|
69
|
+
print(f"Descendant: {descendant.label}")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Generate PHP Code
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from php_parser_py import parse, PrettyPrinter
|
|
76
|
+
|
|
77
|
+
# Parse code
|
|
78
|
+
ast = parse("<?php echo 'hello';")
|
|
79
|
+
|
|
80
|
+
# Modify AST (example: change properties)
|
|
81
|
+
# ... modify nodes as needed ...
|
|
82
|
+
|
|
83
|
+
# Generate code
|
|
84
|
+
printer = PrettyPrinter()
|
|
85
|
+
generated_code = printer.print(ast)
|
|
86
|
+
print(generated_code)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Advanced Usage
|
|
90
|
+
|
|
91
|
+
### Custom PHP Binary
|
|
92
|
+
|
|
93
|
+
If you need to use a specific PHP version or custom binary:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from php_parser_py import Parser
|
|
97
|
+
from pathlib import Path
|
|
98
|
+
|
|
99
|
+
# Use local PHP binary
|
|
100
|
+
parser = Parser(php_binary_path=Path("/usr/local/bin/php"))
|
|
101
|
+
ast = parser.parse("<?php echo 'hello';")
|
|
102
|
+
|
|
103
|
+
# Or download from remote URL
|
|
104
|
+
parser = Parser(php_binary_url="https://example.com/php-8.4.zip")
|
|
105
|
+
ast = parser.parse("<?php echo 'hello';")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## API Reference
|
|
109
|
+
|
|
110
|
+
### Main Functions
|
|
111
|
+
|
|
112
|
+
- **`parse(code: str) -> AST`**: Parse PHP code string and return AST
|
|
113
|
+
- **`parse_file(path: str) -> AST`**: Parse PHP file and return AST
|
|
114
|
+
|
|
115
|
+
### Classes
|
|
116
|
+
|
|
117
|
+
- **`AST`**: Represents the complete AST as a graph
|
|
118
|
+
- `node(id)`: Get node by ID
|
|
119
|
+
- `nodes(predicate)`: Iterate nodes matching predicate
|
|
120
|
+
- `first_node(predicate)`: Get first matching node
|
|
121
|
+
- `children(node)`: Get child nodes
|
|
122
|
+
- `descendants(node)`: Get all descendants
|
|
123
|
+
- `to_json()`: Reconstruct PHP-Parser JSON
|
|
124
|
+
|
|
125
|
+
- **`Node`**: Wraps a single AST node
|
|
126
|
+
- `id`: Node identifier
|
|
127
|
+
- `label`: Node type (e.g., "Stmt_Function")
|
|
128
|
+
- `properties`: All node properties
|
|
129
|
+
- `get_property(*names)`: Get property value
|
|
130
|
+
|
|
131
|
+
- **`Parser`**: Parses PHP code
|
|
132
|
+
- `__init__(php_binary_path=None, php_binary_url=None)`: Initialize parser
|
|
133
|
+
- `php_binary_path`: Optional path to local PHP binary
|
|
134
|
+
- `php_binary_url`: Optional URL to download PHP binary from
|
|
135
|
+
- `parse(code, prefix="")`: Parse code string
|
|
136
|
+
- `prefix`: Optional prefix for node IDs (useful for multi-file parsing)
|
|
137
|
+
- `parse_file(path, prefix=None)`: Parse file
|
|
138
|
+
- `prefix`: Optional prefix (auto-generated from file path if not provided)
|
|
139
|
+
|
|
140
|
+
- **`PrettyPrinter`**: Generates PHP code
|
|
141
|
+
- `__init__(php_binary_path=None, php_binary_url=None)`: Initialize printer
|
|
142
|
+
- `php_binary_path`: Optional path to local PHP binary
|
|
143
|
+
- `php_binary_url`: Optional URL to download PHP binary from
|
|
144
|
+
- `print(ast)`: Generate code from AST
|
|
145
|
+
|
|
146
|
+
### Exceptions
|
|
147
|
+
|
|
148
|
+
- **`ParseError`**: Raised when PHP-Parser reports syntax error
|
|
149
|
+
- **`RunnerError`**: Raised when PHP execution fails
|
|
150
|
+
|
|
151
|
+
## Architecture
|
|
152
|
+
|
|
153
|
+
The library follows a delegation-based architecture:
|
|
154
|
+
|
|
155
|
+
1. **PHP-Parser** handles all parsing and code generation
|
|
156
|
+
2. **cpg2py Storage** stores the AST as a graph
|
|
157
|
+
3. **Python classes** provide convenient access to the graph
|
|
158
|
+
|
|
159
|
+
This design ensures:
|
|
160
|
+
- No need to maintain PHP node type definitions in Python
|
|
161
|
+
- Automatic compatibility with PHP-Parser updates
|
|
162
|
+
- Full access to PHP-Parser's features
|
|
163
|
+
|
|
164
|
+
## Requirements
|
|
165
|
+
|
|
166
|
+
- Python >= 3.11
|
|
167
|
+
- cpg2py
|
|
168
|
+
- static-php-py (bundled PHP binaries)
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT License - see LICENSE file for details.
|
|
173
|
+
|
|
174
|
+
## Documentation
|
|
175
|
+
|
|
176
|
+
For detailed design documentation, see:
|
|
177
|
+
- [Design Document](docs/design.md)
|
|
178
|
+
- [Idea Document](docs/idea.md)
|
|
179
|
+
- [API Demo](docs/api_demo.ipynb)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "php-parser-py"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Python wrapper for PHP-Parser using cpg2py"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "samhsu-dev", email = "yxu166@jh.edu" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"cpg2py>=1.1.0",
|
|
12
|
+
"static-php-py",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["uv_build>=0.9.26,<0.10.0"]
|
|
17
|
+
build-backend = "uv_build"
|
|
18
|
+
|
|
19
|
+
[tool.uv]
|
|
20
|
+
package = true
|
|
21
|
+
|
|
22
|
+
[tool.uv.build-backend]
|
|
23
|
+
# Include resources directory in package data
|
|
24
|
+
source-include = ["resources/*.zip"]
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"pytest>=9.0.2",
|
|
29
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
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: str) -> AST:
|
|
39
|
+
"""Parse PHP code and return AST.
|
|
40
|
+
|
|
41
|
+
Convenience function that creates a Parser instance and parses the code.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
code: PHP source code string.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
AST instance containing parsed code structure.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ParseError: If PHP-Parser reports syntax error.
|
|
51
|
+
"""
|
|
52
|
+
parser = Parser()
|
|
53
|
+
return parser.parse(code)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def parse_file(path: str) -> AST:
|
|
57
|
+
"""Read and parse PHP file.
|
|
58
|
+
|
|
59
|
+
Convenience function that creates a Parser instance and parses the file.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
path: File path string.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
AST instance containing parsed code structure.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ParseError: If PHP-Parser reports syntax error.
|
|
69
|
+
FileNotFoundError: If file does not exist.
|
|
70
|
+
"""
|
|
71
|
+
parser = Parser()
|
|
72
|
+
return parser.parse_file(path)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
__version__ = "0.1.0"
|
|
76
|
+
__all__ = [
|
|
77
|
+
"AST",
|
|
78
|
+
"Node",
|
|
79
|
+
"Edge",
|
|
80
|
+
"Parser",
|
|
81
|
+
"PrettyPrinter",
|
|
82
|
+
"ParseError",
|
|
83
|
+
"RunnerError",
|
|
84
|
+
"parse",
|
|
85
|
+
"parse_file",
|
|
86
|
+
]
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""AST representation for PHP code using cpg2py's graph framework."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Callable, Iterable, Optional
|
|
8
|
+
|
|
9
|
+
from cpg2py import AbcGraphQuerier, Storage
|
|
10
|
+
|
|
11
|
+
from php_parser_py._edge import Edge
|
|
12
|
+
from php_parser_py._node import Node
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AST(AbcGraphQuerier[Node, Edge]):
|
|
18
|
+
"""Represents a PHP Abstract Syntax Tree.
|
|
19
|
+
|
|
20
|
+
This class extends cpg2py's AbcGraphQuerier with generic type parameters
|
|
21
|
+
specifying that it works with Node and Edge types.
|
|
22
|
+
|
|
23
|
+
The AST is stored as a graph in cpg2py Storage, where:
|
|
24
|
+
- Nodes represent PHP-Parser AST nodes (statements, expressions, etc.)
|
|
25
|
+
- Edges represent parent-child relationships with field names
|
|
26
|
+
|
|
27
|
+
Type Parameters:
|
|
28
|
+
Node: The concrete node type for PHP AST nodes
|
|
29
|
+
Edge: The concrete edge type for parent-child relationships
|
|
30
|
+
|
|
31
|
+
Provides graph traversal methods and JSON reconstruction
|
|
32
|
+
for code generation.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
_storage: cpg2py Storage containing AST nodes and edges.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, storage: Storage) -> None:
|
|
39
|
+
"""Initialize AST with populated storage.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
storage: cpg2py Storage containing AST nodes and edges.
|
|
43
|
+
"""
|
|
44
|
+
super().__init__(storage)
|
|
45
|
+
self._storage = storage
|
|
46
|
+
|
|
47
|
+
def node(self, whose_id_is: str) -> Optional[Node]:
|
|
48
|
+
"""Return node wrapper by ID.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
whose_id_is: Node ID string.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Node instance or None if not found.
|
|
55
|
+
"""
|
|
56
|
+
if not self._storage.contains_node(whose_id_is):
|
|
57
|
+
return None
|
|
58
|
+
return Node(self._storage, whose_id_is)
|
|
59
|
+
|
|
60
|
+
def edge(self, fid: str, tid: str, eid: str) -> Optional[Edge]:
|
|
61
|
+
"""Return edge wrapper by IDs.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
fid: From node ID.
|
|
65
|
+
tid: To node ID.
|
|
66
|
+
eid: Edge type.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Edge instance or None if not found.
|
|
70
|
+
"""
|
|
71
|
+
edge_id = (fid, tid, eid)
|
|
72
|
+
if not self._storage.contains_edge(edge_id):
|
|
73
|
+
return None
|
|
74
|
+
return Edge(self._storage, fid, tid, eid)
|
|
75
|
+
|
|
76
|
+
def to_json(self) -> str:
|
|
77
|
+
"""Reconstruct PHP-Parser JSON from Storage for code generation.
|
|
78
|
+
|
|
79
|
+
Traverses the graph to rebuild the nested JSON structure that matches
|
|
80
|
+
PHP-Parser's original output format. This enables lossless round-trip
|
|
81
|
+
from parsing to code generation.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
JSON string compatible with PHP-Parser's JsonDecoder.
|
|
85
|
+
"""
|
|
86
|
+
root_nodes = self._find_root_nodes()
|
|
87
|
+
result = [self._reconstruct_node(nid) for nid in root_nodes]
|
|
88
|
+
return json.dumps(result)
|
|
89
|
+
|
|
90
|
+
def _find_root_nodes(self) -> list[str]:
|
|
91
|
+
"""Find nodes with no incoming PARENT_OF edges."""
|
|
92
|
+
all_nodes = set(self._storage.get_nodes())
|
|
93
|
+
child_nodes = set()
|
|
94
|
+
|
|
95
|
+
for edge_id in self._storage.get_edges():
|
|
96
|
+
_, to_id, edge_type = edge_id
|
|
97
|
+
if edge_type == "PARENT_OF":
|
|
98
|
+
child_nodes.add(to_id)
|
|
99
|
+
|
|
100
|
+
root_nodes = all_nodes - child_nodes
|
|
101
|
+
return sorted(root_nodes)
|
|
102
|
+
|
|
103
|
+
def _reconstruct_node(self, nid: str) -> dict[str, Any]:
|
|
104
|
+
"""Recursively reconstruct JSON object for a node."""
|
|
105
|
+
props = self._storage.get_node_props(nid)
|
|
106
|
+
if props is None:
|
|
107
|
+
return {}
|
|
108
|
+
|
|
109
|
+
result: dict[str, Any] = {}
|
|
110
|
+
node_type = props.get("nodeType")
|
|
111
|
+
if node_type:
|
|
112
|
+
result["nodeType"] = node_type
|
|
113
|
+
|
|
114
|
+
# Collect attribute fields
|
|
115
|
+
attributes = {}
|
|
116
|
+
child_fields: dict[str, Any] = {}
|
|
117
|
+
|
|
118
|
+
for key, value in props.items():
|
|
119
|
+
if key == "nodeType":
|
|
120
|
+
continue
|
|
121
|
+
# Attributes are typically line numbers and metadata
|
|
122
|
+
if key in (
|
|
123
|
+
"startLine",
|
|
124
|
+
"endLine",
|
|
125
|
+
"startFilePos",
|
|
126
|
+
"endFilePos",
|
|
127
|
+
"startTokenPos",
|
|
128
|
+
"endTokenPos",
|
|
129
|
+
"kind",
|
|
130
|
+
"comments",
|
|
131
|
+
):
|
|
132
|
+
attributes[key] = value
|
|
133
|
+
else:
|
|
134
|
+
# All other fields (scalars, arrays, etc.) go directly in result
|
|
135
|
+
# Child nodes will be added separately via edges
|
|
136
|
+
result[key] = value
|
|
137
|
+
|
|
138
|
+
if attributes:
|
|
139
|
+
result["attributes"] = attributes
|
|
140
|
+
|
|
141
|
+
# Reconstruct child nodes from PARENT_OF edges
|
|
142
|
+
outgoing_edges = [
|
|
143
|
+
eid
|
|
144
|
+
for eid in self._storage.get_edges()
|
|
145
|
+
if eid[0] == nid and eid[2] == "PARENT_OF"
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
for from_id, to_id, edge_type in outgoing_edges:
|
|
149
|
+
edge_props = self._storage.get_edge_props((from_id, to_id, edge_type))
|
|
150
|
+
if edge_props is None:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
field_name = edge_props.get("field")
|
|
154
|
+
if field_name is None:
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
child_json = self._reconstruct_node(to_id)
|
|
158
|
+
index = edge_props.get("index")
|
|
159
|
+
|
|
160
|
+
if index is not None:
|
|
161
|
+
# Array field
|
|
162
|
+
if field_name not in child_fields:
|
|
163
|
+
child_fields[field_name] = {}
|
|
164
|
+
child_fields[field_name][index] = child_json
|
|
165
|
+
else:
|
|
166
|
+
# Single object field
|
|
167
|
+
result[field_name] = child_json
|
|
168
|
+
|
|
169
|
+
# Convert indexed dictionaries to arrays
|
|
170
|
+
for field_name, indexed_children in child_fields.items():
|
|
171
|
+
if isinstance(indexed_children, dict):
|
|
172
|
+
# Sort by index and create array
|
|
173
|
+
max_index = max(indexed_children.keys())
|
|
174
|
+
array = [None] * (max_index + 1)
|
|
175
|
+
for idx, child in indexed_children.items():
|
|
176
|
+
array[idx] = child
|
|
177
|
+
result[field_name] = array
|
|
178
|
+
|
|
179
|
+
# Add default values for common optional fields that PHP-Parser expects
|
|
180
|
+
# These prevent "null given, array expected" errors in PrettyPrinter
|
|
181
|
+
# attrGroups is required for many node types in PHP 8+
|
|
182
|
+
if node_type and ("attrGroups" not in result):
|
|
183
|
+
# Most node types that can have attributes need attrGroups
|
|
184
|
+
if (node_type.startswith("Stmt_") or
|
|
185
|
+
node_type == "Param" or
|
|
186
|
+
node_type.startswith("Expr_Closure") or
|
|
187
|
+
node_type.startswith("Expr_ArrowFunction")):
|
|
188
|
+
result["attrGroups"] = []
|
|
189
|
+
|
|
190
|
+
return result
|