php-parser-py 0.1.1__py3-none-any.whl

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,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
+ ]
php_parser_py/_ast.py ADDED
@@ -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
php_parser_py/_edge.py ADDED
@@ -0,0 +1,130 @@
1
+ """Edge class for AST relationships."""
2
+
3
+ from typing import Any
4
+
5
+ from cpg2py import AbcEdgeQuerier, Storage
6
+
7
+
8
+ class Edge(AbcEdgeQuerier):
9
+ """Represents an edge between AST nodes.
10
+
11
+ Extends cpg2py's AbcEdgeQuerier to represent parent-child relationships
12
+ in the PHP AST. Edges store the relationship type and optional metadata
13
+ like field names and array indices for ordered children.
14
+
15
+ Note: PHP-Parser doesn't have explicit Edge objects. This is our abstraction
16
+ for representing the graph structure in cpg2py Storage.
17
+
18
+ Attributes:
19
+ _storage: cpg2py Storage instance containing edge data.
20
+ _edge_id: Tuple of (from_id, to_id, edge_type).
21
+ """
22
+
23
+ def __init__(self, graph: Storage, f_nid: str, t_nid: str, e_type: str = "PARENT_OF") -> None:
24
+ """Initialize Edge with storage and edge identifiers.
25
+
26
+ Args:
27
+ graph: cpg2py Storage containing the graph.
28
+ f_nid: From node ID.
29
+ t_nid: To node ID.
30
+ e_type: Edge type (default: "PARENT_OF").
31
+ """
32
+ super().__init__(graph, f_nid, t_nid, e_type)
33
+ self._storage = graph
34
+ self._edge_id = (str(f_nid), str(t_nid), str(e_type))
35
+
36
+ # Core properties
37
+
38
+ @property
39
+ def type(self) -> str:
40
+ """Return the edge type.
41
+
42
+ Returns:
43
+ Edge type string (e.g., "PARENT_OF").
44
+ """
45
+ return self._edge_id[2]
46
+
47
+ @property
48
+ def field(self) -> str | None:
49
+ """Return the field name for this edge.
50
+
51
+ The field name indicates which subnode property this edge represents
52
+ (e.g., "name", "params", "stmts").
53
+
54
+ Returns:
55
+ Field name string or None.
56
+ """
57
+ props = self._storage.get_edge_props(self._edge_id)
58
+ if props is None:
59
+ return None
60
+ return props.get("field")
61
+
62
+ @property
63
+ def index(self) -> int | None:
64
+ """Return the array index for ordered child relationships.
65
+
66
+ For edges representing array elements in PHP-Parser JSON, this property
67
+ contains the index position to maintain ordering.
68
+
69
+ Returns:
70
+ Integer index or None if not an array element.
71
+ """
72
+ props = self._storage.get_edge_props(self._edge_id)
73
+ if props is None:
74
+ return None
75
+ return props.get("index")
76
+
77
+ @property
78
+ def all_properties(self) -> dict:
79
+ """Return all edge properties.
80
+
81
+ Returns:
82
+ Dictionary of edge properties.
83
+ """
84
+ return self._storage.get_edge_props(self._edge_id) or {}
85
+
86
+ # Dict-like access methods
87
+
88
+ def __getitem__(self, key: str) -> Any:
89
+ """Access edge properties using dict-like syntax.
90
+
91
+ Enables edge["field"], edge["index"], etc.
92
+
93
+ Args:
94
+ key: Property name to access.
95
+
96
+ Returns:
97
+ Property value.
98
+
99
+ Raises:
100
+ KeyError: If property doesn't exist.
101
+ """
102
+ props = self.all_properties
103
+ if key not in props:
104
+ raise KeyError(f"Property '{key}' not found in edge {self._edge_id}")
105
+ return props[key]
106
+
107
+ def __contains__(self, key: str) -> bool:
108
+ """Check if property exists using 'in' operator.
109
+
110
+ Enables: if "index" in edge: ...
111
+
112
+ Args:
113
+ key: Property name to check.
114
+
115
+ Returns:
116
+ True if property exists, False otherwise.
117
+ """
118
+ return key in self.all_properties
119
+
120
+ def get(self, key: str, default: Any = None) -> Any:
121
+ """Get property value with default fallback.
122
+
123
+ Args:
124
+ key: Property name to access.
125
+ default: Default value if property doesn't exist.
126
+
127
+ Returns:
128
+ Property value or default.
129
+ """
130
+ return self.all_properties.get(key, default)
php_parser_py/_node.py ADDED
@@ -0,0 +1,208 @@
1
+ """Node class for PHP AST nodes."""
2
+
3
+ from typing import Any
4
+
5
+ from cpg2py import AbcNodeQuerier, Storage
6
+
7
+
8
+ class Node(AbcNodeQuerier):
9
+ """Wraps a single AST node with dynamic property access to PHP-Parser JSON fields.
10
+
11
+ Extends cpg2py's AbcNodeQuerier to provide access to PHP-Parser node data
12
+ stored in cpg2py Storage. All node properties come from PHP-Parser's JSON
13
+ output and are accessed dynamically without hardcoded field definitions.
14
+
15
+ PHP-Parser nodes have two types of data:
16
+ - **Subnodes**: Structural properties like name, params, stmts, byRef, etc.
17
+ - **Attributes**: Metadata like startLine, endLine, comments, etc.
18
+
19
+ Both are accessible through this class using dict-like syntax or properties.
20
+
21
+ Attributes:
22
+ _storage: cpg2py Storage instance containing node data.
23
+ _nid: Unique identifier for this node.
24
+ """
25
+
26
+ def __init__(self, storage: Storage, nid: str) -> None:
27
+ """Initialize Node with storage reference and node ID.
28
+
29
+ Args:
30
+ storage: cpg2py Storage instance containing node data.
31
+ nid: Unique identifier for this node.
32
+
33
+ Raises:
34
+ Exception: If node ID is not found in storage.
35
+ """
36
+ super().__init__(storage, nid)
37
+ self._storage = storage
38
+ self._nid = nid
39
+
40
+ # Core properties
41
+
42
+ @property
43
+ def id(self) -> str:
44
+ """Return the node identifier.
45
+
46
+ Returns:
47
+ Node ID string.
48
+ """
49
+ return self._nid
50
+
51
+ @property
52
+ def node_type(self) -> str | None:
53
+ """Return the node type from PHP-Parser.
54
+
55
+ Returns the `nodeType` field from the stored JSON, which contains
56
+ the PHP-Parser class name (e.g., "Stmt_Function", "Expr_Variable").
57
+
58
+ Returns:
59
+ Node type string or None if not set.
60
+ """
61
+ return self.get_property("nodeType")
62
+
63
+ @property
64
+ def all_properties(self) -> dict:
65
+ """Return all stored properties for this node.
66
+
67
+ Includes both subnodes (structural properties) and attributes (metadata).
68
+
69
+ Returns:
70
+ Dictionary containing all properties from PHP-Parser JSON.
71
+ """
72
+ return self.properties or {}
73
+
74
+ # Dict-like access methods
75
+
76
+ def __getitem__(self, key: str) -> Any:
77
+ """Access node properties using dict-like syntax.
78
+
79
+ Enables node["nodeType"], node["name"], node["startLine"], etc.
80
+
81
+ Args:
82
+ key: Property name to access.
83
+
84
+ Returns:
85
+ Property value.
86
+
87
+ Raises:
88
+ KeyError: If property doesn't exist.
89
+ """
90
+ props = self.all_properties
91
+ if key not in props:
92
+ raise KeyError(f"Property '{key}' not found in node {self._nid}")
93
+ return props[key]
94
+
95
+ def __contains__(self, key: str) -> bool:
96
+ """Check if property exists using 'in' operator.
97
+
98
+ Enables: if "name" in node: ...
99
+
100
+ Args:
101
+ key: Property name to check.
102
+
103
+ Returns:
104
+ True if property exists, False otherwise.
105
+ """
106
+ return key in self.all_properties
107
+
108
+ def get(self, key: str, default: Any = None) -> Any:
109
+ """Get property value with default fallback.
110
+
111
+ Args:
112
+ key: Property name to access.
113
+ default: Default value if property doesn't exist.
114
+
115
+ Returns:
116
+ Property value or default.
117
+ """
118
+ return self.all_properties.get(key, default)
119
+
120
+ # PHP-Parser standard attributes (metadata properties)
121
+
122
+ @property
123
+ def start_line(self) -> int | None:
124
+ """Get starting line number.
125
+
126
+ Returns:
127
+ Starting line number or None.
128
+ """
129
+ return self.get_property("startLine")
130
+
131
+ @property
132
+ def end_line(self) -> int | None:
133
+ """Get ending line number.
134
+
135
+ Returns:
136
+ Ending line number or None.
137
+ """
138
+ return self.get_property("endLine")
139
+
140
+ @property
141
+ def start_file_pos(self) -> int | None:
142
+ """Get starting file position (byte offset).
143
+
144
+ Returns:
145
+ Starting byte offset or None.
146
+ """
147
+ return self.get_property("startFilePos")
148
+
149
+ @property
150
+ def end_file_pos(self) -> int | None:
151
+ """Get ending file position (byte offset).
152
+
153
+ Returns:
154
+ Ending byte offset or None.
155
+ """
156
+ return self.get_property("endFilePos")
157
+
158
+ @property
159
+ def start_token_pos(self) -> int | None:
160
+ """Get starting token position.
161
+
162
+ Returns:
163
+ Starting token index or None.
164
+ """
165
+ return self.get_property("startTokenPos")
166
+
167
+ @property
168
+ def end_token_pos(self) -> int | None:
169
+ """Get ending token position.
170
+
171
+ Returns:
172
+ Ending token index or None.
173
+ """
174
+ return self.get_property("endTokenPos")
175
+
176
+ @property
177
+ def comments(self) -> list | None:
178
+ """Get comments associated with this node.
179
+
180
+ Returns:
181
+ List of Comment objects or None.
182
+ """
183
+ return self.get_property("comments")
184
+
185
+ # Attribute helper methods
186
+
187
+ def has_attribute(self, name: str) -> bool:
188
+ """Check if attribute exists.
189
+
190
+ Args:
191
+ name: Attribute name to check.
192
+
193
+ Returns:
194
+ True if attribute exists, False otherwise.
195
+ """
196
+ return name in self
197
+
198
+ def get_attribute(self, name: str, default: Any = None) -> Any:
199
+ """Get attribute value with default.
200
+
201
+ Args:
202
+ name: Attribute name.
203
+ default: Default value if attribute doesn't exist.
204
+
205
+ Returns:
206
+ Attribute value or default.
207
+ """
208
+ return self.get(name, default)