php-parser-py 1.1.0__tar.gz → 1.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-1.1.0 → php_parser_py-1.1.1}/PKG-INFO +16 -10
- {php_parser_py-1.1.0 → php_parser_py-1.1.1}/README.md +15 -9
- {php_parser_py-1.1.0 → php_parser_py-1.1.1}/pyproject.toml +4 -1
- {php_parser_py-1.1.0 → php_parser_py-1.1.1}/src/php_parser_py/__init__.py +21 -39
- {php_parser_py-1.1.0 → php_parser_py-1.1.1}/src/php_parser_py/_ast.py +23 -9
- {php_parser_py-1.1.0 → php_parser_py-1.1.1}/src/php_parser_py/_edge.py +3 -1
- {php_parser_py-1.1.0 → php_parser_py-1.1.1}/src/php_parser_py/_parser.py +80 -91
- {php_parser_py-1.1.0 → php_parser_py-1.1.1}/src/php_parser_py/_printer.py +2 -4
- {php_parser_py-1.1.0 → php_parser_py-1.1.1}/src/php_parser_py/_resources.py +1 -1
- {php_parser_py-1.1.0 → php_parser_py-1.1.1}/src/php_parser_py/_runner.py +4 -8
- /php_parser_py-1.1.0/src/php_parser_py/exceptions.py → /php_parser_py-1.1.1/src/php_parser_py/_exceptions.py +0 -0
- {php_parser_py-1.1.0 → php_parser_py-1.1.1}/src/php_parser_py/_node.py +0 -0
- {php_parser_py-1.1.0 → php_parser_py-1.1.1}/src/php_parser_py/resources/php-parser-4.19.4.zip +0 -0
- {php_parser_py-1.1.0 → php_parser_py-1.1.1}/src/php_parser_py/vendor/.extracted +0 -0
- {php_parser_py-1.1.0 → php_parser_py-1.1.1}/src/php_parser_py/vendor/LICENSE.txt +0 -0
- {php_parser_py-1.1.0 → php_parser_py-1.1.1}/src/php_parser_py/vendor/php-parser.phar +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: php-parser-py
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: Python wrapper for PHP-Parser using cpg2py
|
|
5
5
|
Author: samhsu-dev
|
|
6
6
|
Author-email: samhsu-dev <yxu166@jh.edu>
|
|
@@ -74,13 +74,14 @@ file_node = ast.get_file(stmt_node.id)
|
|
|
74
74
|
print(f"Function is in file: {file_node.get_property('filePath')}")
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
### Parse
|
|
77
|
+
### Parse Project Directory
|
|
78
78
|
|
|
79
79
|
```python
|
|
80
80
|
from php_parser_py import parse_project
|
|
81
|
+
from pathlib import Path
|
|
81
82
|
|
|
82
|
-
# Parse
|
|
83
|
-
ast = parse_project(
|
|
83
|
+
# Parse all PHP files in a project directory (recursively)
|
|
84
|
+
ast = parse_project("/path/to/project")
|
|
84
85
|
|
|
85
86
|
# Access all files
|
|
86
87
|
for file_node in ast.files():
|
|
@@ -90,13 +91,18 @@ for file_node in ast.files():
|
|
|
90
91
|
print(f" Statement: {stmt.node_type}")
|
|
91
92
|
|
|
92
93
|
# Access project and file paths
|
|
93
|
-
ast = parse_project(["file1.php", "subdir/file2.php"])
|
|
94
94
|
project = ast.project_node
|
|
95
95
|
print(f"Project root: {project.get_property('path')}")
|
|
96
96
|
|
|
97
97
|
for file_node in ast.files():
|
|
98
98
|
print(f"File relative path: {file_node.get_property('path')}")
|
|
99
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
|
+
)
|
|
100
106
|
```
|
|
101
107
|
|
|
102
108
|
### Traverse AST
|
|
@@ -146,12 +152,12 @@ for file_path, code in generated.items():
|
|
|
146
152
|
print(f"File: {file_path}")
|
|
147
153
|
print(code)
|
|
148
154
|
|
|
149
|
-
# Parse
|
|
150
|
-
ast = parse_project(
|
|
155
|
+
# Parse project directory (recursively finds all PHP files)
|
|
156
|
+
ast = parse_project("/path/to/project")
|
|
151
157
|
|
|
152
158
|
# Generate code for all files
|
|
153
159
|
generated = printer.print(ast)
|
|
154
|
-
# Returns: {"file1.php": "...", "file2.php": "..."}
|
|
160
|
+
# Returns: {"src/file1.php": "...", "src/file2.php": "..."}
|
|
155
161
|
```
|
|
156
162
|
|
|
157
163
|
## Advanced Usage
|
|
@@ -180,7 +186,7 @@ ast = parser.parse("<?php echo 'hello';")
|
|
|
180
186
|
- **`parse_code(code: str) -> list[Node]`**: Parse PHP code string into raw statement nodes (no project/file structure)
|
|
181
187
|
- **`parse(code: str) -> AST`**: Parse PHP code string and return AST with project/file structure (backward compatibility)
|
|
182
188
|
- **`parse_file(path: str) -> AST`**: Parse a single PHP file and return AST with project/file structure
|
|
183
|
-
- **`parse_project(
|
|
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)
|
|
184
190
|
|
|
185
191
|
### Classes
|
|
186
192
|
|
|
@@ -214,7 +220,7 @@ ast = parser.parse("<?php echo 'hello';")
|
|
|
214
220
|
- `parse_code(code)`: Parse code string into list of nodes
|
|
215
221
|
- `parse(code)`: Parse code string into AST (backward compatibility)
|
|
216
222
|
- `parse_file(path)`: Parse file into AST with project/file structure
|
|
217
|
-
- `parse_project(
|
|
223
|
+
- `parse_project(project_path)`: Parse project directory into AST with project structure
|
|
218
224
|
|
|
219
225
|
- **`PrettyPrinter`**: Generates PHP code
|
|
220
226
|
- `__init__(php_binary_path=None, php_binary_url=None)`: Initialize printer
|
|
@@ -63,13 +63,14 @@ file_node = ast.get_file(stmt_node.id)
|
|
|
63
63
|
print(f"Function is in file: {file_node.get_property('filePath')}")
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
### Parse
|
|
66
|
+
### Parse Project Directory
|
|
67
67
|
|
|
68
68
|
```python
|
|
69
69
|
from php_parser_py import parse_project
|
|
70
|
+
from pathlib import Path
|
|
70
71
|
|
|
71
|
-
# Parse
|
|
72
|
-
ast = parse_project(
|
|
72
|
+
# Parse all PHP files in a project directory (recursively)
|
|
73
|
+
ast = parse_project("/path/to/project")
|
|
73
74
|
|
|
74
75
|
# Access all files
|
|
75
76
|
for file_node in ast.files():
|
|
@@ -79,13 +80,18 @@ for file_node in ast.files():
|
|
|
79
80
|
print(f" Statement: {stmt.node_type}")
|
|
80
81
|
|
|
81
82
|
# Access project and file paths
|
|
82
|
-
ast = parse_project(["file1.php", "subdir/file2.php"])
|
|
83
83
|
project = ast.project_node
|
|
84
84
|
print(f"Project root: {project.get_property('path')}")
|
|
85
85
|
|
|
86
86
|
for file_node in ast.files():
|
|
87
87
|
print(f"File relative path: {file_node.get_property('path')}")
|
|
88
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
|
+
)
|
|
89
95
|
```
|
|
90
96
|
|
|
91
97
|
### Traverse AST
|
|
@@ -135,12 +141,12 @@ for file_path, code in generated.items():
|
|
|
135
141
|
print(f"File: {file_path}")
|
|
136
142
|
print(code)
|
|
137
143
|
|
|
138
|
-
# Parse
|
|
139
|
-
ast = parse_project(
|
|
144
|
+
# Parse project directory (recursively finds all PHP files)
|
|
145
|
+
ast = parse_project("/path/to/project")
|
|
140
146
|
|
|
141
147
|
# Generate code for all files
|
|
142
148
|
generated = printer.print(ast)
|
|
143
|
-
# Returns: {"file1.php": "...", "file2.php": "..."}
|
|
149
|
+
# Returns: {"src/file1.php": "...", "src/file2.php": "..."}
|
|
144
150
|
```
|
|
145
151
|
|
|
146
152
|
## Advanced Usage
|
|
@@ -169,7 +175,7 @@ ast = parser.parse("<?php echo 'hello';")
|
|
|
169
175
|
- **`parse_code(code: str) -> list[Node]`**: Parse PHP code string into raw statement nodes (no project/file structure)
|
|
170
176
|
- **`parse(code: str) -> AST`**: Parse PHP code string and return AST with project/file structure (backward compatibility)
|
|
171
177
|
- **`parse_file(path: str) -> AST`**: Parse a single PHP file and return AST with project/file structure
|
|
172
|
-
- **`parse_project(
|
|
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)
|
|
173
179
|
|
|
174
180
|
### Classes
|
|
175
181
|
|
|
@@ -203,7 +209,7 @@ ast = parser.parse("<?php echo 'hello';")
|
|
|
203
209
|
- `parse_code(code)`: Parse code string into list of nodes
|
|
204
210
|
- `parse(code)`: Parse code string into AST (backward compatibility)
|
|
205
211
|
- `parse_file(path)`: Parse file into AST with project/file structure
|
|
206
|
-
- `parse_project(
|
|
212
|
+
- `parse_project(project_path)`: Parse project directory into AST with project structure
|
|
207
213
|
|
|
208
214
|
- **`PrettyPrinter`**: Generates PHP code
|
|
209
215
|
- `__init__(php_binary_path=None, php_binary_url=None)`: Initialize printer
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "php-parser-py"
|
|
3
|
-
version = "1.1.
|
|
3
|
+
version = "1.1.1"
|
|
4
4
|
description = "Python wrapper for PHP-Parser using cpg2py"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -25,5 +25,8 @@ source-include = ["resources/*.zip"]
|
|
|
25
25
|
|
|
26
26
|
[dependency-groups]
|
|
27
27
|
dev = [
|
|
28
|
+
"black>=26.1.0",
|
|
29
|
+
"isort>=7.0.0",
|
|
30
|
+
"pylint>=4.0.4",
|
|
28
31
|
"pytest>=9.0.2",
|
|
29
32
|
]
|
|
@@ -5,6 +5,12 @@ This package provides a Python interface to PHP-Parser, enabling AST parsing
|
|
|
5
5
|
and manipulation of PHP code.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Callable
|
|
13
|
+
|
|
8
14
|
from php_parser_py._parser import Parser as _Parser
|
|
9
15
|
from php_parser_py._printer import PrettyPrinter as _PrettyPrinter
|
|
10
16
|
from php_parser_py._resources import ensure_php_parser_extracted
|
|
@@ -14,9 +20,9 @@ ensure_php_parser_extracted()
|
|
|
14
20
|
|
|
15
21
|
# Import and expose public classes directly
|
|
16
22
|
from php_parser_py._ast import AST
|
|
17
|
-
from php_parser_py._node import Node
|
|
18
23
|
from php_parser_py._edge import Edge
|
|
19
|
-
from php_parser_py.
|
|
24
|
+
from php_parser_py._exceptions import ParseError, RunnerError
|
|
25
|
+
from php_parser_py._node import Node
|
|
20
26
|
|
|
21
27
|
|
|
22
28
|
class Parser(_Parser):
|
|
@@ -53,35 +59,6 @@ def parse_code(code: str) -> list[Node]:
|
|
|
53
59
|
return parser.parse_code(code)
|
|
54
60
|
|
|
55
61
|
|
|
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
62
|
def parse_file(path: str) -> AST:
|
|
86
63
|
"""Read and parse a single PHP file into an AST.
|
|
87
64
|
|
|
@@ -102,25 +79,30 @@ def parse_file(path: str) -> AST:
|
|
|
102
79
|
return parser.parse_file(path)
|
|
103
80
|
|
|
104
81
|
|
|
105
|
-
def parse_project(
|
|
106
|
-
|
|
82
|
+
def parse_project(
|
|
83
|
+
project_path: str,
|
|
84
|
+
file_filter: "Callable[[Path], bool]" = lambda p: p.suffix == ".php", # type: ignore
|
|
85
|
+
) -> AST:
|
|
86
|
+
"""Parse all PHP files in a project directory into an AST.
|
|
107
87
|
|
|
108
|
-
|
|
109
|
-
|
|
88
|
+
Recursively traverses the project directory to find all PHP files,
|
|
89
|
+
then parses them into a single AST with project and file nodes.
|
|
110
90
|
|
|
111
91
|
Args:
|
|
112
|
-
|
|
113
|
-
|
|
92
|
+
project_path: Project root directory path.
|
|
93
|
+
file_filter: Function to filter files. Takes a Path and returns
|
|
94
|
+
True if the file should be parsed. Defaults to checking for `.php` suffix.
|
|
114
95
|
|
|
115
96
|
Returns:
|
|
116
97
|
AST instance with project -> files -> statements hierarchy.
|
|
117
98
|
|
|
118
99
|
Raises:
|
|
119
100
|
ParseError: If any file has syntax errors.
|
|
120
|
-
FileNotFoundError: If
|
|
101
|
+
FileNotFoundError: If project directory does not exist.
|
|
102
|
+
ValueError: If project_path is not a directory.
|
|
121
103
|
"""
|
|
122
104
|
parser = Parser()
|
|
123
|
-
return parser.parse_project(
|
|
105
|
+
return parser.parse_project(project_path, file_filter=file_filter)
|
|
124
106
|
|
|
125
107
|
|
|
126
108
|
__version__ = "0.1.0"
|
|
@@ -119,7 +119,9 @@ class AST(AbcGraphQuerier[Node, Edge]):
|
|
|
119
119
|
# Sort by file path for consistent ordering
|
|
120
120
|
return sorted(file_nodes, key=lambda n: n.get_property("filePath", ""))
|
|
121
121
|
|
|
122
|
-
def get_file(
|
|
122
|
+
def get_file(
|
|
123
|
+
self, node_id: str
|
|
124
|
+
) -> Optional[Node]: # pylint: disable=too-many-return-statements
|
|
123
125
|
"""Get the file node that contains the given node.
|
|
124
126
|
|
|
125
127
|
Traverses upward from the given node via PARENT_OF edges until
|
|
@@ -215,7 +217,7 @@ class AST(AbcGraphQuerier[Node, Edge]):
|
|
|
215
217
|
nodes_with_parents = set()
|
|
216
218
|
|
|
217
219
|
for edge_id in self._storage.get_edges():
|
|
218
|
-
|
|
220
|
+
_, to_id, edge_type = edge_id
|
|
219
221
|
if edge_type == "PARENT_OF":
|
|
220
222
|
nodes_with_parents.add(to_id)
|
|
221
223
|
|
|
@@ -253,10 +255,20 @@ class AST(AbcGraphQuerier[Node, Edge]):
|
|
|
253
255
|
|
|
254
256
|
return sorted(statement_nodes, key=get_index)
|
|
255
257
|
|
|
256
|
-
def _reconstruct_node(
|
|
257
|
-
|
|
258
|
+
def _reconstruct_node( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
|
|
259
|
+
self, nid: str
|
|
260
|
+
) -> dict[str, Any]:
|
|
261
|
+
"""Recursively reconstruct JSON object for a node.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
nid: Node ID to reconstruct.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Dictionary representing the node in PHP-Parser JSON format.
|
|
268
|
+
"""
|
|
258
269
|
props = self._storage.get_node_props(nid)
|
|
259
270
|
if props is None:
|
|
271
|
+
logger.warning("Node %s not found in storage, returning empty dict", nid)
|
|
260
272
|
return {}
|
|
261
273
|
|
|
262
274
|
result: dict[str, Any] = {}
|
|
@@ -348,10 +360,12 @@ class AST(AbcGraphQuerier[Node, Edge]):
|
|
|
348
360
|
|
|
349
361
|
# Add default values for common optional fields that PHP-Parser expects
|
|
350
362
|
if node_type and ("attrGroups" not in result):
|
|
351
|
-
if (
|
|
352
|
-
node_type
|
|
353
|
-
node_type
|
|
354
|
-
node_type.startswith("
|
|
363
|
+
if (
|
|
364
|
+
node_type.startswith("Stmt_")
|
|
365
|
+
or node_type == "Param"
|
|
366
|
+
or node_type.startswith("Expr_Closure")
|
|
367
|
+
or node_type.startswith("Expr_ArrowFunction")
|
|
368
|
+
):
|
|
355
369
|
result["attrGroups"] = []
|
|
356
|
-
|
|
370
|
+
|
|
357
371
|
return result
|
|
@@ -20,7 +20,9 @@ class Edge(AbcEdgeQuerier):
|
|
|
20
20
|
_edge_id: Tuple of (from_id, to_id, edge_type).
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
-
def __init__(
|
|
23
|
+
def __init__(
|
|
24
|
+
self, graph: Storage, f_nid: str, t_nid: str, e_type: str = "PARENT_OF"
|
|
25
|
+
) -> None:
|
|
24
26
|
"""Initialize Edge with storage and edge identifiers.
|
|
25
27
|
|
|
26
28
|
Args:
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import logging
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Any, Optional
|
|
6
|
+
from typing import Any, Callable, Optional
|
|
7
7
|
|
|
8
8
|
from cpg2py import Storage
|
|
9
9
|
|
|
10
10
|
from php_parser_py._ast import AST
|
|
11
|
+
from php_parser_py._exceptions import ParseError, RunnerError
|
|
11
12
|
from php_parser_py._node import Node
|
|
12
13
|
from php_parser_py._runner import Runner
|
|
13
|
-
from php_parser_py.exceptions import ParseError, RunnerError
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
16
16
|
|
|
@@ -21,7 +21,7 @@ class Parser:
|
|
|
21
21
|
Provides three parsing methods:
|
|
22
22
|
- parse_code: Parse code string, returns list of top-level statement nodes
|
|
23
23
|
- parse_file: Parse single file, returns AST with project and file nodes
|
|
24
|
-
- parse_project: Parse
|
|
24
|
+
- parse_project: Parse project directory, returns AST with project and all PHP files
|
|
25
25
|
|
|
26
26
|
Attributes:
|
|
27
27
|
_runner: Runner instance for PHP-Parser invocation.
|
|
@@ -33,7 +33,7 @@ class Parser:
|
|
|
33
33
|
php_binary_url: Optional[str] = None,
|
|
34
34
|
) -> None:
|
|
35
35
|
"""Initialize Parser with Runner.
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
Args:
|
|
38
38
|
php_binary_path: Optional path to local PHP binary.
|
|
39
39
|
php_binary_url: Optional URL to download PHP binary from.
|
|
@@ -43,35 +43,6 @@ class Parser:
|
|
|
43
43
|
php_binary_url=php_binary_url,
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
-
def parse(self, code: str) -> AST:
|
|
47
|
-
"""Parse PHP code string into an AST (backward compatibility).
|
|
48
|
-
|
|
49
|
-
Creates a temporary project/file structure. For code-only parsing
|
|
50
|
-
without project structure, use parse_code() instead.
|
|
51
|
-
|
|
52
|
-
Args:
|
|
53
|
-
code: PHP source code string.
|
|
54
|
-
|
|
55
|
-
Returns:
|
|
56
|
-
AST instance with project -> file -> statements hierarchy.
|
|
57
|
-
|
|
58
|
-
Raises:
|
|
59
|
-
ParseError: If code has syntax errors.
|
|
60
|
-
RunnerError: If PHP execution fails.
|
|
61
|
-
"""
|
|
62
|
-
import tempfile
|
|
63
|
-
from pathlib import Path
|
|
64
|
-
|
|
65
|
-
# Create temporary file
|
|
66
|
-
with tempfile.NamedTemporaryFile(mode='w', suffix='.php', delete=False) as f:
|
|
67
|
-
f.write(code)
|
|
68
|
-
temp_path = f.name
|
|
69
|
-
|
|
70
|
-
try:
|
|
71
|
-
return self.parse_file(temp_path)
|
|
72
|
-
finally:
|
|
73
|
-
Path(temp_path).unlink()
|
|
74
|
-
|
|
75
46
|
def parse_code(self, code: str) -> list[Node]:
|
|
76
47
|
"""Parse PHP code string into a list of top-level statement nodes.
|
|
77
48
|
|
|
@@ -94,7 +65,7 @@ class Parser:
|
|
|
94
65
|
if "Syntax error" in str(e):
|
|
95
66
|
raise ParseError("Syntax error in PHP code", line=1) from e
|
|
96
67
|
raise
|
|
97
|
-
|
|
68
|
+
|
|
98
69
|
# Normalize to list
|
|
99
70
|
if not isinstance(json_data, list):
|
|
100
71
|
json_data = [json_data]
|
|
@@ -105,7 +76,9 @@ class Parser:
|
|
|
105
76
|
node_counter = [1] # Use list to allow mutation in recursive calls
|
|
106
77
|
|
|
107
78
|
for item in json_data:
|
|
108
|
-
node_id = self._process_node(
|
|
79
|
+
node_id = self._process_node(
|
|
80
|
+
storage, item, None, None, None, node_counter, ""
|
|
81
|
+
)
|
|
109
82
|
if node_id:
|
|
110
83
|
node_ids.append(node_id)
|
|
111
84
|
|
|
@@ -140,7 +113,7 @@ class Parser:
|
|
|
140
113
|
file_hash = hashlib.md5(str(file_path).encode()).hexdigest()[:8]
|
|
141
114
|
|
|
142
115
|
code = file_path.read_text(encoding="utf-8")
|
|
143
|
-
|
|
116
|
+
|
|
144
117
|
try:
|
|
145
118
|
json_data = self._runner.parse(code)
|
|
146
119
|
except RunnerError as e:
|
|
@@ -153,63 +126,69 @@ class Parser:
|
|
|
153
126
|
json_data = [json_data]
|
|
154
127
|
|
|
155
128
|
storage = self._create_project_structure(
|
|
156
|
-
[(file_path, json_data)],
|
|
129
|
+
[(file_path, json_data)],
|
|
157
130
|
[(file_path, file_hash)],
|
|
158
|
-
project_path=project_path
|
|
131
|
+
project_path=project_path,
|
|
159
132
|
)
|
|
160
133
|
return AST(storage, root_node_id="project")
|
|
161
134
|
|
|
162
|
-
def parse_project(
|
|
163
|
-
|
|
135
|
+
def parse_project(
|
|
136
|
+
self,
|
|
137
|
+
project_path: str,
|
|
138
|
+
file_filter: Callable[[Path], bool] = lambda p: p.suffix == ".php",
|
|
139
|
+
) -> AST:
|
|
140
|
+
"""Parse all PHP files in a project directory into an AST.
|
|
164
141
|
|
|
165
|
-
|
|
142
|
+
Recursively traverses the project directory to find all PHP files,
|
|
143
|
+
then parses them into a single AST with project and file nodes.
|
|
166
144
|
Each file node has ID based on its path hash.
|
|
167
|
-
Project path is the common parent directory of all files, or the provided project_path.
|
|
168
145
|
|
|
169
146
|
Args:
|
|
170
|
-
|
|
171
|
-
|
|
147
|
+
project_path: Project root directory path.
|
|
148
|
+
file_filter: Function to filter files. Takes a Path and returns
|
|
149
|
+
True if the file should be parsed. Defaults to checking for `.php` suffix.
|
|
172
150
|
|
|
173
151
|
Returns:
|
|
174
152
|
AST instance with project -> files -> statements hierarchy.
|
|
175
153
|
|
|
176
154
|
Raises:
|
|
177
155
|
ParseError: If any file has syntax errors.
|
|
178
|
-
FileNotFoundError: If
|
|
156
|
+
FileNotFoundError: If project directory does not exist.
|
|
157
|
+
ValueError: If project_path is not a directory.
|
|
179
158
|
"""
|
|
159
|
+
project_path_obj = Path(project_path).resolve()
|
|
160
|
+
|
|
161
|
+
if not project_path_obj.exists():
|
|
162
|
+
raise FileNotFoundError(f"Project directory not found: {project_path}")
|
|
163
|
+
|
|
164
|
+
if not project_path_obj.is_dir():
|
|
165
|
+
raise ValueError(f"Project path is not a directory: {project_path}")
|
|
166
|
+
|
|
167
|
+
# Find all files recursively and filter
|
|
168
|
+
all_files = project_path_obj.rglob("*")
|
|
169
|
+
php_files = [f for f in all_files if f.is_file() and file_filter(f)]
|
|
170
|
+
|
|
171
|
+
if not php_files:
|
|
172
|
+
logger.warning(f"No PHP files found in project directory: {project_path}")
|
|
173
|
+
# Create empty project structure
|
|
174
|
+
storage = Storage()
|
|
175
|
+
project_id = "project"
|
|
176
|
+
storage.add_node(project_id)
|
|
177
|
+
storage.set_node_props(
|
|
178
|
+
project_id,
|
|
179
|
+
{
|
|
180
|
+
"nodeType": "Project",
|
|
181
|
+
"label": "Project",
|
|
182
|
+
"path": str(project_path_obj),
|
|
183
|
+
},
|
|
184
|
+
)
|
|
185
|
+
return AST(storage, root_node_id="project")
|
|
186
|
+
|
|
187
|
+
# Prepare file information
|
|
180
188
|
file_infos = []
|
|
181
|
-
|
|
182
|
-
for path in paths:
|
|
183
|
-
file_path = Path(path).resolve()
|
|
184
|
-
if not file_path.exists():
|
|
185
|
-
raise FileNotFoundError(f"File not found: {path}")
|
|
189
|
+
for file_path in php_files:
|
|
186
190
|
file_hash = hashlib.md5(str(file_path).encode()).hexdigest()[:8]
|
|
187
191
|
file_infos.append((file_path, file_hash))
|
|
188
|
-
resolved_paths.append(file_path)
|
|
189
|
-
|
|
190
|
-
# Determine project path
|
|
191
|
-
if project_path:
|
|
192
|
-
project_path = Path(project_path).resolve()
|
|
193
|
-
else:
|
|
194
|
-
# Compute common parent directory
|
|
195
|
-
if len(resolved_paths) == 1:
|
|
196
|
-
project_path = resolved_paths[0].parent
|
|
197
|
-
else:
|
|
198
|
-
# Find common parent
|
|
199
|
-
common_parts = None
|
|
200
|
-
for file_path in resolved_paths:
|
|
201
|
-
parts = file_path.parts
|
|
202
|
-
if common_parts is None:
|
|
203
|
-
common_parts = parts
|
|
204
|
-
else:
|
|
205
|
-
# Find common prefix
|
|
206
|
-
min_len = min(len(common_parts), len(parts))
|
|
207
|
-
common_parts = [p for i, p in enumerate(common_parts[:min_len]) if parts[i] == p]
|
|
208
|
-
if common_parts:
|
|
209
|
-
project_path = Path(*common_parts)
|
|
210
|
-
else:
|
|
211
|
-
# Fallback to first file's parent
|
|
212
|
-
project_path = resolved_paths[0].parent
|
|
213
192
|
|
|
214
193
|
# Parse all files
|
|
215
194
|
all_json_data = []
|
|
@@ -225,14 +204,16 @@ class Parser:
|
|
|
225
204
|
raise ParseError(f"Syntax error in {file_path}", line=1) from e
|
|
226
205
|
raise
|
|
227
206
|
|
|
228
|
-
storage = self._create_project_structure(
|
|
207
|
+
storage = self._create_project_structure(
|
|
208
|
+
all_json_data, file_infos, project_path=project_path_obj
|
|
209
|
+
)
|
|
229
210
|
return AST(storage, root_node_id="project")
|
|
230
211
|
|
|
231
212
|
def _create_project_structure(
|
|
232
213
|
self,
|
|
233
214
|
files_data: list[tuple[Path, Any]],
|
|
234
215
|
file_infos: list[tuple[Path, str]],
|
|
235
|
-
project_path: Path
|
|
216
|
+
project_path: Path,
|
|
236
217
|
) -> Storage:
|
|
237
218
|
"""Create AST structure with project -> files -> statements hierarchy.
|
|
238
219
|
|
|
@@ -249,11 +230,10 @@ class Parser:
|
|
|
249
230
|
# Create project node (fixed ID: "project")
|
|
250
231
|
project_id = "project"
|
|
251
232
|
storage.add_node(project_id)
|
|
252
|
-
storage.set_node_props(
|
|
253
|
-
|
|
254
|
-
"label": "Project",
|
|
255
|
-
|
|
256
|
-
})
|
|
233
|
+
storage.set_node_props(
|
|
234
|
+
project_id,
|
|
235
|
+
{"nodeType": "Project", "label": "Project", "path": str(project_path)},
|
|
236
|
+
)
|
|
257
237
|
|
|
258
238
|
# Create file nodes and process their statements
|
|
259
239
|
for (file_path, file_hash), (_, json_data) in zip(file_infos, files_data):
|
|
@@ -267,12 +247,15 @@ class Parser:
|
|
|
267
247
|
# Create file node (ID: hex hash)
|
|
268
248
|
file_id = file_hash
|
|
269
249
|
storage.add_node(file_id)
|
|
270
|
-
storage.set_node_props(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
250
|
+
storage.set_node_props(
|
|
251
|
+
file_id,
|
|
252
|
+
{
|
|
253
|
+
"nodeType": "File",
|
|
254
|
+
"label": "File",
|
|
255
|
+
"filePath": str(file_path), # Absolute path
|
|
256
|
+
"path": str(relative_path), # Relative path from project root
|
|
257
|
+
},
|
|
258
|
+
)
|
|
276
259
|
|
|
277
260
|
# Link file to project
|
|
278
261
|
edge_id = (project_id, file_id, "PARENT_OF")
|
|
@@ -291,7 +274,7 @@ class Parser:
|
|
|
291
274
|
|
|
292
275
|
return storage
|
|
293
276
|
|
|
294
|
-
def _process_node(
|
|
277
|
+
def _process_node( # pylint: disable=too-many-arguments,too-many-locals,too-many-branches
|
|
295
278
|
self,
|
|
296
279
|
storage: Storage,
|
|
297
280
|
node_data: Any,
|
|
@@ -299,7 +282,7 @@ class Parser:
|
|
|
299
282
|
field_name: str | None,
|
|
300
283
|
index: int | None,
|
|
301
284
|
node_counter: list[int],
|
|
302
|
-
prefix: str
|
|
285
|
+
prefix: str,
|
|
303
286
|
) -> str | None:
|
|
304
287
|
"""Process a single node and its children recursively.
|
|
305
288
|
|
|
@@ -383,7 +366,13 @@ class Parser:
|
|
|
383
366
|
else:
|
|
384
367
|
# Single child object
|
|
385
368
|
self._process_node(
|
|
386
|
-
storage,
|
|
369
|
+
storage,
|
|
370
|
+
field_value,
|
|
371
|
+
node_id,
|
|
372
|
+
field_name,
|
|
373
|
+
None,
|
|
374
|
+
node_counter,
|
|
375
|
+
prefix,
|
|
387
376
|
)
|
|
388
377
|
|
|
389
378
|
return node_id
|
|
@@ -26,7 +26,7 @@ class PrettyPrinter:
|
|
|
26
26
|
php_binary_url: Optional[str] = None,
|
|
27
27
|
) -> None:
|
|
28
28
|
"""Initialize PrettyPrinter with Runner.
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
Args:
|
|
31
31
|
php_binary_path: Optional path to local PHP binary.
|
|
32
32
|
php_binary_url: Optional URL to download PHP binary from.
|
|
@@ -104,6 +104,4 @@ class PrettyPrinter:
|
|
|
104
104
|
json_str = ast.to_json(file_hash=file_hash)
|
|
105
105
|
return self._runner.print(json_str)
|
|
106
106
|
|
|
107
|
-
raise KeyError(
|
|
108
|
-
f"File with relative path '{relative_path}' not found in AST."
|
|
109
|
-
)
|
|
107
|
+
raise KeyError(f"File with relative path '{relative_path}' not found in AST.")
|
|
@@ -9,8 +9,8 @@ from typing import Any, Optional
|
|
|
9
9
|
from static_php_py import PHP
|
|
10
10
|
from static_php_py.exceptions import BinaryNotFoundError, DownloadError
|
|
11
11
|
|
|
12
|
+
from php_parser_py._exceptions import ParseError, RunnerError
|
|
12
13
|
from php_parser_py._resources import ensure_php_parser_extracted
|
|
13
|
-
from php_parser_py.exceptions import ParseError, RunnerError
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
16
16
|
|
|
@@ -105,7 +105,7 @@ class Runner:
|
|
|
105
105
|
if result.stdout:
|
|
106
106
|
logger.error(f"PHP stdout: {result.stdout}")
|
|
107
107
|
error_msg += f"\nStdout: {result.stdout}"
|
|
108
|
-
|
|
108
|
+
|
|
109
109
|
raise RunnerError(
|
|
110
110
|
error_msg,
|
|
111
111
|
stderr=result.stderr,
|
|
@@ -117,18 +117,14 @@ class Runner:
|
|
|
117
117
|
except FileNotFoundError as e:
|
|
118
118
|
error_msg = f"PHP binary not found: {self._php_binary}"
|
|
119
119
|
logger.error(error_msg)
|
|
120
|
-
raise RunnerError(
|
|
121
|
-
error_msg, stderr=str(e), exit_code=1
|
|
122
|
-
) from e
|
|
120
|
+
raise RunnerError(error_msg, stderr=str(e), exit_code=1) from e
|
|
123
121
|
except Exception as e:
|
|
124
122
|
error_msg = f"Failed to execute PHP script: {e}"
|
|
125
123
|
logger.error(error_msg)
|
|
126
124
|
# Don't wrap the error if it's already a RunnerError
|
|
127
125
|
if isinstance(e, RunnerError):
|
|
128
126
|
raise
|
|
129
|
-
raise RunnerError(
|
|
130
|
-
error_msg, stderr=str(e), exit_code=1
|
|
131
|
-
) from e
|
|
127
|
+
raise RunnerError(error_msg, stderr=str(e), exit_code=1) from e
|
|
132
128
|
|
|
133
129
|
def parse(self, code: str) -> dict[str, Any]:
|
|
134
130
|
"""Invoke PHP-Parser parse + JsonSerializer.
|
|
File without changes
|
|
File without changes
|
{php_parser_py-1.1.0 → php_parser_py-1.1.1}/src/php_parser_py/resources/php-parser-4.19.4.zip
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|