php-parser-py 1.2.0__py3-none-any.whl → 1.2.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.
php_parser_py/__init__.py CHANGED
@@ -5,11 +5,8 @@ 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
8
+ from pathlib import Path
9
+ from typing import Callable
13
10
 
14
11
  from php_parser_py._parser import Parser as _Parser
15
12
  from php_parser_py._printer import PrettyPrinter as _PrettyPrinter
@@ -21,7 +18,7 @@ ensure_php_parser_extracted()
21
18
  # Import and expose public classes directly
22
19
  from php_parser_py._ast import AST
23
20
  from php_parser_py._edge import Edge
24
- from php_parser_py._exceptions import ParseError, RunnerError
21
+ from php_parser_py._exceptions import NodeNotInFileError, ParseError, RunnerError
25
22
  from php_parser_py._node import Node
26
23
 
27
24
 
@@ -79,9 +76,13 @@ def parse_file(path: str) -> AST:
79
76
  return parser.parse_file(path)
80
77
 
81
78
 
79
+ def _default_file_filter(path: Path) -> bool:
80
+ return path.suffix == ".php"
81
+
82
+
82
83
  def parse_project(
83
84
  project_path: str,
84
- file_filter: "Callable[[Path], bool]" = lambda p: p.suffix == ".php", # type: ignore
85
+ file_filter: Callable[[Path], bool] | None = None,
85
86
  ) -> AST:
86
87
  """Parse all PHP files in a project directory into an AST.
87
88
 
@@ -101,6 +102,8 @@ def parse_project(
101
102
  FileNotFoundError: If project directory does not exist.
102
103
  ValueError: If project_path is not a directory.
103
104
  """
105
+ if file_filter is None:
106
+ file_filter = _default_file_filter
104
107
  parser = Parser()
105
108
  return parser.parse_project(project_path, file_filter=file_filter)
106
109
 
@@ -114,6 +117,7 @@ __all__ = [
114
117
  "PrettyPrinter",
115
118
  "ParseError",
116
119
  "RunnerError",
120
+ "NodeNotInFileError",
117
121
  "parse_code",
118
122
  "parse_file",
119
123
  "parse_project",
php_parser_py/_ast.py CHANGED
@@ -4,11 +4,12 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import logging
7
- from typing import Any, Optional
7
+ from typing import Any
8
8
 
9
9
  from cpg2py import AbcGraphQuerier, Storage
10
10
 
11
11
  from php_parser_py._edge import Edge
12
+ from php_parser_py._exceptions import NodeNotInFileError
12
13
  from php_parser_py._node import Node
13
14
 
14
15
  logger = logging.getLogger(__name__)
@@ -30,10 +31,11 @@ class AST(AbcGraphQuerier[Node, Edge]):
30
31
  Edge: The concrete edge type for parent-child relationships
31
32
 
32
33
  Provides graph traversal methods, JSON reconstruction for code generation,
33
- and AST merging capabilities.
34
+ and AST merging capabilities. Uses cpg2py graph API (nodes, edges, succ,
35
+ prev, ancestors, descendants); storage is only used where the graph API
36
+ does not provide an alternative (node/edge existence and Node/Edge construction).
34
37
 
35
38
  Attributes:
36
- _storage: cpg2py Storage containing AST nodes and edges.
37
39
  _root_node_id: ID of the root project node (always "project").
38
40
  """
39
41
 
@@ -45,23 +47,25 @@ class AST(AbcGraphQuerier[Node, Edge]):
45
47
  root_node_id: ID of the root node. Defaults to "project".
46
48
  """
47
49
  super().__init__(storage)
48
- self._storage = storage
49
50
  self._root_node_id = root_node_id
50
51
 
51
- def node(self, whose_id_is: str) -> Optional[Node]:
52
+ def node(self, whose_id_is: str) -> Node:
52
53
  """Return node wrapper by ID.
53
54
 
54
55
  Args:
55
56
  whose_id_is: Node ID string.
56
57
 
57
58
  Returns:
58
- Node instance or None if not found.
59
+ Node instance for the given ID.
60
+
61
+ Raises:
62
+ KeyError: If the node ID is not in the graph.
59
63
  """
60
- if not self._storage.contains_node(whose_id_is):
61
- return None
62
- return Node(self._storage, whose_id_is)
64
+ if not self.storage.contains_node(whose_id_is):
65
+ raise KeyError(f"Node not found: {whose_id_is!r}")
66
+ return Node(self.storage, whose_id_is)
63
67
 
64
- def edge(self, fid: str, tid: str, eid: str) -> Optional[Edge]:
68
+ def edge(self, fid: str, tid: str, eid: str) -> Edge:
65
69
  """Return edge wrapper by IDs.
66
70
 
67
71
  Args:
@@ -70,24 +74,31 @@ class AST(AbcGraphQuerier[Node, Edge]):
70
74
  eid: Edge type.
71
75
 
72
76
  Returns:
73
- Edge instance or None if not found.
77
+ Edge instance for the given (from, to, type).
78
+
79
+ Raises:
80
+ KeyError: If the edge is not in the graph.
74
81
  """
75
82
  edge_id = (fid, tid, eid)
76
- if not self._storage.contains_edge(edge_id):
77
- return None
78
- return Edge(self._storage, fid, tid, eid)
83
+ if not self.storage.contains_edge(edge_id):
84
+ raise KeyError(f"Edge not found: {edge_id!r}")
85
+ return Edge(self.storage, fid, tid, eid)
79
86
 
80
- def project_node(self) -> Optional[Node]:
81
- """Return the project node.
87
+ def project_node(self) -> Node:
88
+ """Return the project node (root of the AST).
82
89
 
83
- The project node is the root of the AST with ID "project".
90
+ The project node has ID "project" and is always present when the AST
91
+ was created by parse_file or parse_project.
84
92
 
85
93
  Returns:
86
- Project Node instance or None if not found.
94
+ Project Node instance.
95
+
96
+ Raises:
97
+ KeyError: If the root node is not in the graph.
87
98
  """
88
- if not self._storage.contains_node(self._root_node_id):
89
- return None
90
- return Node(self._storage, self._root_node_id)
99
+ if not self.storage.contains_node(self._root_node_id):
100
+ raise KeyError(f"Project node not found: {self._root_node_id!r}")
101
+ return Node(self.storage, self._root_node_id)
91
102
 
92
103
  def file_nodes(self) -> list[Node]:
93
104
  """Return all file nodes in the project.
@@ -95,86 +106,79 @@ class AST(AbcGraphQuerier[Node, Edge]):
95
106
  Returns:
96
107
  List of File Node instances.
97
108
  """
98
- if not self.project_node():
109
+ try:
110
+ project = self.project_node()
111
+ except KeyError:
99
112
  return []
100
113
 
101
- file_nodes = []
102
- for edge_id in self._storage.get_edges():
103
- from_id, to_id, edge_type = edge_id
104
- if from_id == self._root_node_id and edge_type == "PARENT_OF":
105
- file_node = self.node(to_id)
106
- if file_node and file_node.get_property("nodeType") == "File":
107
- file_nodes.append(file_node)
108
-
109
- # Sort by file path for consistent ordering
110
- return sorted(file_nodes, key=lambda n: n.get_property("filePath", ""))
114
+ file_nodes = [n for n in self.succ(project) if n.node_type == "File"]
115
+ return sorted(file_nodes, key=lambda n: n.get("absolutePath", ""))
111
116
 
112
- def get_file(
113
- self, node_id: str
114
- ) -> Optional[Node]: # pylint: disable=too-many-return-statements
117
+ def get_file_node(self, node_id: str) -> Node:
115
118
  """Get the file node that contains the given node.
116
119
 
117
- Traverses upward from the given node via PARENT_OF edges until
118
- finding a File node. If the node itself is a File node, returns it.
120
+ Uses the AST ID convention: file node ID is the file hash (e.g. 8-char hex);
121
+ nodes inside that file have ID ``file_hash + "_" + increment`` (e.g. ``a1b2c3d4_1``).
122
+ Resolves by structure first, then falls back to ancestors() traversal.
119
123
 
120
124
  Args:
121
125
  node_id: ID of any node in the AST.
122
126
 
123
127
  Returns:
124
- File Node instance containing the given node, or None if:
125
- - Node ID doesn't exist
126
- - Node is the project node (project doesn't belong to a file)
127
- - Node is not under any file node
128
- """
129
- if not self._storage.contains_node(node_id):
130
- return None
128
+ File Node instance containing the given node.
131
129
 
132
- # Check if the node itself is a File node
130
+ Raises:
131
+ KeyError: If the node ID is not in the graph.
132
+ NodeNotInFileError: If the node is not under any file (e.g. project node).
133
+ """
133
134
  node = self.node(node_id)
134
- if node and node.get_property("nodeType") == "File":
135
+ if node.node_type == "File":
135
136
  return node
136
137
 
137
- # Check if it's the project node
138
138
  if node_id == self._root_node_id:
139
- return None
140
-
141
- # Traverse upward via PARENT_OF edges to find File node
142
- current_id = node_id
143
- visited = set()
139
+ raise NodeNotInFileError(node_id, "Project node has no containing file.")
144
140
 
145
- while current_id and current_id not in visited:
146
- visited.add(current_id)
141
+ result = self._try_file_by_id_prefix(node_id)
142
+ if result is not None:
143
+ return result
147
144
 
148
- # Find parent node via incoming PARENT_OF edge (each node has one parent)
149
- parent_id = None
150
- for edge_id in self._storage.get_edges():
151
- from_id, to_id, edge_type = edge_id
152
- if to_id == current_id and edge_type == "PARENT_OF":
153
- parent_id = from_id
154
- break
145
+ return self._find_file_ancestor(node)
155
146
 
156
- if not parent_id:
157
- # No parent found, can't determine file
158
- return None
147
+ def _try_file_by_id_prefix(self, node_id: str) -> Node | None:
148
+ """Try to find file node by ID prefix convention.
159
149
 
160
- # Check parent node
161
- parent_node = self.node(parent_id)
162
- if not parent_node:
163
- return None
150
+ If node_id is like "hash_123", attempts to get file node with ID "hash".
151
+ Returns None if ID doesn't match convention or file not found.
152
+ """
153
+ if "_" not in node_id:
154
+ return None
164
155
 
165
- node_type = parent_node.get_property("nodeType")
166
- if node_type == "File":
167
- return parent_node
168
- elif node_type == "Project":
169
- # Reached project, no file found
170
- return None
156
+ prefix, rest = node_id.split("_", 1)
157
+ if not rest.isdigit():
158
+ return None
171
159
 
172
- # Continue traversing upward
173
- current_id = parent_id
160
+ try:
161
+ candidate = self.node(prefix)
162
+ if candidate.node_type == "File":
163
+ return candidate
164
+ except KeyError:
165
+ pass
174
166
 
175
167
  return None
176
168
 
177
- def to_json(self, file_hash: Optional[str] = None) -> str:
169
+ def _find_file_ancestor(self, node: Node) -> Node:
170
+ """Find first File ancestor of a node via traversal.
171
+
172
+ Raises NodeNotInFileError if no File ancestor exists.
173
+ """
174
+ for ancestor in self.ancestors(node):
175
+ if ancestor.node_type == "File":
176
+ result: Node = ancestor
177
+ return result
178
+
179
+ raise NodeNotInFileError(node.id, "No File node among ancestors.")
180
+
181
+ def to_json(self, file_hash: str | None = None) -> str:
178
182
  """Reconstruct PHP-Parser JSON from Storage for code generation.
179
183
 
180
184
  If file_hash is provided, exports only that file's statements.
@@ -187,10 +191,8 @@ class AST(AbcGraphQuerier[Node, Edge]):
187
191
  JSON string compatible with PHP-Parser's JsonDecoder.
188
192
  """
189
193
  if file_hash:
190
- # Export single file
191
- file_node = self.node(file_hash)
192
- if not file_node:
193
- return json.dumps([])
194
+ # Export single file (node() raises KeyError if file_hash not in graph)
195
+ self.node(file_hash)
194
196
  top_level_nodes = self._get_file_statements(file_hash)
195
197
  else:
196
198
  # Try to export from file structure first
@@ -201,53 +203,34 @@ class AST(AbcGraphQuerier[Node, Edge]):
201
203
  for file_node in file_nodes:
202
204
  top_level_nodes.extend(self._get_file_statements(file_node.id))
203
205
  else:
204
- # No file structure - find root nodes (nodes without incoming PARENT_OF edges)
205
- # Exclude project node if it exists
206
- all_nodes = set(self._storage.get_nodes())
207
- nodes_with_parents = set()
208
-
209
- for edge_id in self._storage.get_edges():
210
- _, to_id, edge_type = edge_id
211
- if edge_type == "PARENT_OF":
212
- nodes_with_parents.add(to_id)
213
-
206
+ # No file structure - find root nodes via graph API (no incoming PARENT_OF)
207
+ all_nodes = {n.id for n in self.nodes()}
208
+ nodes_with_parents = {
209
+ e.to_nid for e in self.edges() if e.type == "PARENT_OF"
210
+ }
214
211
  root_nodes = all_nodes - nodes_with_parents
215
- # Exclude project node if present
216
212
  root_nodes.discard(self._root_node_id)
217
- top_level_nodes = sorted(list(root_nodes))
213
+ top_level_nodes = sorted(root_nodes)
218
214
 
219
215
  result = [self._reconstruct_node(nid) for nid in top_level_nodes]
220
216
  return json.dumps(result)
221
217
 
222
218
  def _get_file_statements(self, file_hash: str) -> list[str]:
223
- """Get top-level statement nodes for a specific file.
224
-
225
- Args:
226
- file_hash: File node ID (hash).
219
+ """Get top-level statement node IDs for a file (direct children with edge field \"stmts\").
227
220
 
228
- Returns:
229
- List of statement node IDs.
221
+ Uses succ() traversal; statement IDs follow the convention file_hash_1, file_hash_2, ...
230
222
  """
231
- statement_nodes = []
232
- for edge_id in self._storage.get_edges():
233
- from_id, to_id, edge_type = edge_id
234
- if from_id == file_hash and edge_type == "PARENT_OF":
235
- edge_props = self._storage.get_edge_props(edge_id)
236
- if edge_props and edge_props.get("field") == "stmts":
237
- statement_nodes.append(to_id)
238
-
239
- # Sort by edge index to preserve order
240
- def get_index(nid: str) -> int:
241
- edge_props = self._storage.get_edge_props((file_hash, nid, "PARENT_OF"))
242
- if edge_props:
243
- return edge_props.get("index", 999999)
244
- return 999999
245
-
246
- return sorted(statement_nodes, key=get_index)
247
-
248
- def _reconstruct_node( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
249
- self, nid: str
250
- ) -> dict[str, Any]:
223
+ file_node = self.node(file_hash)
224
+ stmts_with_index = []
225
+ for child in self.succ(file_node):
226
+ edge = self.edge(file_hash, child.id, "PARENT_OF")
227
+ if edge.get("field") == "stmts":
228
+ idx = edge.get("index")
229
+ stmts_with_index.append((999999 if idx is None else idx, child.id))
230
+ stmts_with_index.sort(key=lambda t: t[0])
231
+ return [nid for _, nid in stmts_with_index]
232
+
233
+ def _reconstruct_node(self, nid: str) -> dict[str, Any]:
251
234
  """Recursively reconstruct JSON object for a node.
252
235
 
253
236
  Args:
@@ -256,106 +239,103 @@ class AST(AbcGraphQuerier[Node, Edge]):
256
239
  Returns:
257
240
  Dictionary representing the node in PHP-Parser JSON format.
258
241
  """
259
- props = self._storage.get_node_props(nid)
260
- if props is None:
261
- logger.warning("Node %s not found in storage, returning empty dict", nid)
262
- return {}
242
+ node = self.node(nid)
243
+ props = node.all_properties
244
+ result: dict[str, Any] = {"nodeType": node.node_type}
245
+
246
+ attributes = self._extract_attributes(props)
247
+ if attributes:
248
+ result["attributes"] = attributes
263
249
 
264
- result: dict[str, Any] = {}
265
- node_type = props.get("nodeType")
266
- if node_type:
267
- result["nodeType"] = node_type
250
+ self._add_non_attribute_props(result, props)
251
+ self._reconstruct_child_fields(result, nid)
252
+ self._add_default_attrs(result, node.node_type)
268
253
 
269
- # Collect attribute fields
270
- attributes = {}
271
- child_fields: dict[str, Any] = {}
254
+ return result
272
255
 
256
+ def _extract_attributes(self, props: dict[str, Any]) -> dict[str, Any]:
257
+ """Extract metadata attributes from node properties.
258
+
259
+ Returns dict with position and comment metadata.
260
+ """
261
+ attr_keys = {
262
+ "startLine",
263
+ "endLine",
264
+ "startFilePos",
265
+ "endFilePos",
266
+ "startTokenPos",
267
+ "endTokenPos",
268
+ "kind",
269
+ "comments",
270
+ }
271
+ return {k: v for k, v in props.items() if k in attr_keys}
272
+
273
+ def _add_non_attribute_props(
274
+ self, result: dict[str, Any], props: dict[str, Any]
275
+ ) -> None:
276
+ """Add non-attribute, non-nodeType properties to result."""
277
+ attr_keys = {
278
+ "nodeType",
279
+ "startLine",
280
+ "endLine",
281
+ "startFilePos",
282
+ "endFilePos",
283
+ "startTokenPos",
284
+ "endTokenPos",
285
+ "kind",
286
+ "comments",
287
+ }
273
288
  for key, value in props.items():
274
- if key == "nodeType":
275
- continue
276
- # Attributes are typically line numbers and metadata
277
- if key in (
278
- "startLine",
279
- "endLine",
280
- "startFilePos",
281
- "endFilePos",
282
- "startTokenPos",
283
- "endTokenPos",
284
- "kind",
285
- "comments",
286
- ):
287
- attributes[key] = value
288
- else:
289
- # All other fields go directly in result
290
- # Child nodes will be added separately via edges
289
+ if key not in attr_keys:
291
290
  result[key] = value
292
291
 
293
- if attributes:
294
- result["attributes"] = attributes
292
+ def _reconstruct_child_fields(self, result: dict[str, Any], nid: str) -> None:
293
+ """Reconstruct and add child fields to result.
295
294
 
296
- # Reconstruct child nodes from PARENT_OF edges
297
- outgoing_edges = [
298
- eid
299
- for eid in self._storage.get_edges()
300
- if eid[0] == nid and eid[2] == "PARENT_OF"
301
- ]
295
+ Child nodes are fetched via succ() and indexed by edge properties.
296
+ """
297
+ node = self.node(nid)
298
+ child_fields: dict[str, dict[int, Any]] = {}
302
299
 
303
- for from_id, to_id, edge_type in outgoing_edges:
304
- edge_props = self._storage.get_edge_props((from_id, to_id, edge_type))
305
- if edge_props is None:
300
+ for child in self.succ(node):
301
+ try:
302
+ edge = self.edge(nid, child.id, "PARENT_OF")
303
+ except KeyError:
306
304
  continue
307
305
 
308
- field_name = edge_props.get("field")
306
+ field_name = edge.get("field")
309
307
  if field_name is None:
310
308
  continue
311
309
 
312
- # Skip project/file structure edges when reconstructing
313
- if field_name in ("files", "stmts"):
314
- # Only include stmts edges for file nodes
315
- if field_name == "stmts":
316
- child_json = self._reconstruct_node(to_id)
317
- index = edge_props.get("index")
318
-
319
- if index is not None:
320
- # Array field
321
- if field_name not in child_fields:
322
- child_fields[field_name] = {}
323
- child_fields[field_name][index] = child_json
324
- else:
325
- # Single object field
326
- result[field_name] = child_json
310
+ child_json = self._reconstruct_node(child.id)
311
+ index = edge.get("index")
312
+
313
+ if field_name not in child_fields:
314
+ child_fields[field_name] = {}
315
+
316
+ if index is not None:
317
+ child_fields[field_name][index] = child_json
327
318
  else:
328
- # Regular child field
329
- child_json = self._reconstruct_node(to_id)
330
- index = edge_props.get("index")
331
-
332
- if index is not None:
333
- # Array field
334
- if field_name not in child_fields:
335
- child_fields[field_name] = {}
336
- child_fields[field_name][index] = child_json
337
- else:
338
- # Single object field
339
- result[field_name] = child_json
340
-
341
- # Convert indexed dictionaries to arrays
319
+ result[field_name] = child_json
320
+
342
321
  for field_name, indexed_children in child_fields.items():
343
- if isinstance(indexed_children, dict):
344
- # Sort by index and create array
322
+ if indexed_children:
345
323
  max_index = max(indexed_children.keys())
346
324
  array = [None] * (max_index + 1)
347
325
  for idx, child in indexed_children.items():
348
326
  array[idx] = child
349
327
  result[field_name] = array
350
328
 
351
- # Add default values for common optional fields that PHP-Parser expects
352
- if node_type and ("attrGroups" not in result):
353
- if (
354
- node_type.startswith("Stmt_")
355
- or node_type == "Param"
356
- or node_type.startswith("Expr_Closure")
357
- or node_type.startswith("Expr_ArrowFunction")
358
- ):
359
- result["attrGroups"] = []
329
+ def _add_default_attrs(self, result: dict[str, Any], node_type: str) -> None:
330
+ """Add default attrGroups if not already present.
360
331
 
361
- return result
332
+ PHP-Parser expects attrGroups on certain node types.
333
+ """
334
+ if "attrGroups" in result:
335
+ return
336
+
337
+ if any(
338
+ node_type.startswith(prefix)
339
+ for prefix in ("Stmt_", "Expr_Closure", "Expr_ArrowFunction")
340
+ ) or node_type == "Param":
341
+ result["attrGroups"] = []
php_parser_py/_edge.py CHANGED
@@ -6,14 +6,14 @@ from cpg2py import AbcEdgeQuerier, Storage
6
6
 
7
7
 
8
8
  class Edge(AbcEdgeQuerier):
9
- """Represents an edge between AST nodes.
9
+ """Represents an edge between AST nodes with generic property access.
10
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.
11
+ Extends cpg2py's AbcEdgeQuerier. Edge type is fixed (e.g. "PARENT_OF");
12
+ other data is stored as arbitrary properties via Storage. For PARENT_OF
13
+ edges in our PHP AST mapping, we use properties "field" and "index" to
14
+ represent the PHP-Parser subnode key and array position—access them via
15
+ edge.get("field"), edge.get("index") or edge["field"], edge["index"];
16
+ they are not special-cased on this class.
17
17
 
18
18
  Attributes:
19
19
  _storage: cpg2py Storage instance containing edge data.
@@ -39,45 +39,11 @@ class Edge(AbcEdgeQuerier):
39
39
 
40
40
  @property
41
41
  def type(self) -> str:
42
- """Return the edge type.
43
-
44
- Returns:
45
- Edge type string (e.g., "PARENT_OF").
46
- """
42
+ """Return the edge type (e.g. "PARENT_OF")."""
47
43
  return self._edge_id[2]
48
44
 
49
45
  @property
50
- def field(self) -> str | None:
51
- """Return the field name for this edge.
52
-
53
- The field name indicates which subnode property this edge represents
54
- (e.g., "name", "params", "stmts").
55
-
56
- Returns:
57
- Field name string or None.
58
- """
59
- props = self._storage.get_edge_props(self._edge_id)
60
- if props is None:
61
- return None
62
- return props.get("field")
63
-
64
- @property
65
- def index(self) -> int | None:
66
- """Return the array index for ordered child relationships.
67
-
68
- For edges representing array elements in PHP-Parser JSON, this property
69
- contains the index position to maintain ordering.
70
-
71
- Returns:
72
- Integer index or None if not an array element.
73
- """
74
- props = self._storage.get_edge_props(self._edge_id)
75
- if props is None:
76
- return None
77
- return props.get("index")
78
-
79
- @property
80
- def all_properties(self) -> dict:
46
+ def all_properties(self) -> dict[str, Any]:
81
47
  """Return all edge properties.
82
48
 
83
49
  Returns:
@@ -21,6 +21,18 @@ class ParseError(Exception):
21
21
  self.line = line
22
22
 
23
23
 
24
+ class NodeNotInFileError(Exception):
25
+ """Raised when a node has no containing file (e.g. project node or orphan).
26
+
27
+ Use when get_file(node_id) is called and the node is not under any file node.
28
+ """
29
+
30
+ def __init__(self, node_id: str, message: str | None = None) -> None:
31
+ self.node_id = node_id
32
+ self.message = message or f"Node {node_id!r} is not under any file node."
33
+ super().__init__(self.message)
34
+
35
+
24
36
  class RunnerError(Exception):
25
37
  """Raised when PHP process execution fails.
26
38
 
php_parser_py/_node.py CHANGED
@@ -49,19 +49,21 @@ class Node(AbcNodeQuerier):
49
49
  return self._nid
50
50
 
51
51
  @property
52
- def node_type(self) -> str | None:
52
+ def node_type(self) -> str:
53
53
  """Return the node type from PHP-Parser.
54
54
 
55
- Returns the `nodeType` field from the stored JSON, which contains
56
- the PHP-Parser class name (e.g., "Stmt_Function", "Expr_Variable").
55
+ Returns the `nodeType` field from the stored JSON (e.g. "Stmt_Function").
57
56
 
58
57
  Returns:
59
- Node type string or None if not set.
58
+ Node type string.
60
59
  """
61
- return self.get_property("nodeType")
60
+ value = self.get_property("nodeType")
61
+ if isinstance(value, str):
62
+ return value
63
+ raise TypeError(f"Invalid nodeType for node {self._nid}: {value!r}")
62
64
 
63
65
  @property
64
- def all_properties(self) -> dict:
66
+ def all_properties(self) -> dict[str, Any]:
65
67
  """Return all stored properties for this node.
66
68
 
67
69
  Includes both subnodes (structural properties) and attributes (metadata).
@@ -120,69 +122,172 @@ class Node(AbcNodeQuerier):
120
122
  # PHP-Parser standard attributes (metadata properties)
121
123
 
122
124
  @property
123
- def start_line(self) -> int | None:
125
+ def start_line(self) -> int:
124
126
  """Get starting line number.
125
127
 
126
128
  Returns:
127
- Starting line number or None.
129
+ Starting line number.
128
130
  """
129
- return self.get_property("startLine")
131
+ value = self.get_property("startLine")
132
+ if isinstance(value, int):
133
+ return value
134
+ if isinstance(value, str) and value.isdigit():
135
+ return int(value)
136
+ raise TypeError(f"Invalid startLine for node {self._nid}: {value!r}")
130
137
 
131
138
  @property
132
- def end_line(self) -> int | None:
139
+ def end_line(self) -> int:
133
140
  """Get ending line number.
134
141
 
135
142
  Returns:
136
- Ending line number or None.
143
+ Ending line number.
137
144
  """
138
- return self.get_property("endLine")
145
+ value = self.get_property("endLine")
146
+ if isinstance(value, int):
147
+ return value
148
+ if isinstance(value, str) and value.isdigit():
149
+ return int(value)
150
+ raise TypeError(f"Invalid endLine for node {self._nid}: {value!r}")
139
151
 
140
152
  @property
141
- def start_file_pos(self) -> int | None:
153
+ def start_file_pos(self) -> int:
142
154
  """Get starting file position (byte offset).
143
155
 
144
156
  Returns:
145
- Starting byte offset or None.
157
+ Starting byte offset.
146
158
  """
147
- return self.get_property("startFilePos")
159
+ value = self.get_property("startFilePos")
160
+ if isinstance(value, int):
161
+ return value
162
+ if isinstance(value, str) and value.isdigit():
163
+ return int(value)
164
+ raise TypeError(f"Invalid startFilePos for node {self._nid}: {value!r}")
148
165
 
149
166
  @property
150
- def end_file_pos(self) -> int | None:
167
+ def end_file_pos(self) -> int:
151
168
  """Get ending file position (byte offset).
152
169
 
153
170
  Returns:
154
- Ending byte offset or None.
171
+ Ending byte offset.
155
172
  """
156
- return self.get_property("endFilePos")
173
+ value = self.get_property("endFilePos")
174
+ if isinstance(value, int):
175
+ return value
176
+ if isinstance(value, str) and value.isdigit():
177
+ return int(value)
178
+ raise TypeError(f"Invalid endFilePos for node {self._nid}: {value!r}")
157
179
 
158
180
  @property
159
- def start_token_pos(self) -> int | None:
181
+ def start_token_pos(self) -> int:
160
182
  """Get starting token position.
161
183
 
162
184
  Returns:
163
- Starting token index or None.
185
+ Starting token index.
164
186
  """
165
- return self.get_property("startTokenPos")
187
+ value = self.get_property("startTokenPos")
188
+ if isinstance(value, int):
189
+ return value
190
+ if isinstance(value, str) and value.isdigit():
191
+ return int(value)
192
+ raise TypeError(f"Invalid startTokenPos for node {self._nid}: {value!r}")
166
193
 
167
194
  @property
168
- def end_token_pos(self) -> int | None:
195
+ def end_token_pos(self) -> int:
169
196
  """Get ending token position.
170
197
 
171
198
  Returns:
172
- Ending token index or None.
199
+ Ending token index.
173
200
  """
174
- return self.get_property("endTokenPos")
201
+ value = self.get_property("endTokenPos")
202
+ if isinstance(value, int):
203
+ return value
204
+ if isinstance(value, str) and value.isdigit():
205
+ return int(value)
206
+ raise TypeError(f"Invalid endTokenPos for node {self._nid}: {value!r}")
175
207
 
176
208
  @property
177
- def comments(self) -> list | None:
209
+ def comments(self) -> list[str]:
178
210
  """Get comments associated with this node.
179
211
 
180
212
  Returns:
181
- List of Comment objects or None.
213
+ List of Comment objects (may be empty).
182
214
  """
183
- return self.get_property("comments")
215
+ value = self.get_property("comments")
216
+ if isinstance(value, list):
217
+ return value
218
+ raise TypeError(f"Invalid comments for node {self._nid}: {value!r}")
184
219
 
185
- # Attribute helper methods
220
+ @property
221
+ def relative_path(self) -> str | None:
222
+ """Get the relative path of the file containing this node.
223
+
224
+ For File or Project nodes: returns their own relativePath property if set.
225
+ For other nodes: resolves the containing File node and returns its relativePath.
226
+
227
+ Returns:
228
+ Relative path string, or None if not available.
229
+ """
230
+ # If this node is a File or Project, get its own relativePath
231
+ if self.node_type in ("File", "Project"):
232
+ value = self.get_property("relativePath")
233
+ return value if isinstance(value, str) else None
234
+
235
+ # For other nodes, try to get from containing file via ID prefix convention
236
+ return self._get_file_property("relativePath")
237
+
238
+ @property
239
+ def absolute_path(self) -> str | None:
240
+ """Get the absolute path of the file containing this node.
241
+
242
+ For File or Project nodes: returns their own absolutePath property if set.
243
+ For other nodes: resolves the containing File node and returns its absolutePath.
244
+
245
+ Returns:
246
+ Absolute path string, or None if not available.
247
+ """
248
+ # If this node is a File or Project, get its own absolutePath
249
+ if self.node_type in ("File", "Project"):
250
+ value = self.get_property("absolutePath")
251
+ return value if isinstance(value, str) else None
252
+
253
+ # For other nodes, try to get from containing file via ID prefix convention
254
+ return self._get_file_property("absolutePath")
255
+
256
+ def _get_file_property(self, prop_name: str) -> str | None:
257
+ """Get a path property from the containing file node via ID prefix convention.
258
+
259
+ Node IDs follow the pattern: file_hash_1, file_hash_2, etc.
260
+ This method extracts the file_hash prefix and retrieves the property from that file node.
261
+
262
+ Args:
263
+ prop_name: Property name to retrieve ("relativePath" or "absolutePath").
264
+
265
+ Returns:
266
+ Property value from the file node, or None if not found.
267
+ """
268
+ if "_" not in self._nid:
269
+ return None
270
+
271
+ # Extract prefix: everything before the last underscore followed by digits
272
+ last_underscore = self._nid.rfind("_")
273
+ if last_underscore == -1:
274
+ return None
275
+
276
+ potential_index = self._nid[last_underscore + 1 :]
277
+ if not potential_index.isdigit():
278
+ return None
279
+
280
+ # Get the file node with ID = prefix
281
+ file_id = self._nid[:last_underscore]
282
+ if not self._storage.contains_node(file_id):
283
+ return None
284
+
285
+ file_props = self._storage.get_node_props(file_id)
286
+ if not file_props or file_props.get("nodeType") != "File":
287
+ return None
288
+
289
+ value = file_props.get(prop_name)
290
+ return value if isinstance(value, str) else None
186
291
 
187
292
  def has_attribute(self, name: str) -> bool:
188
293
  """Check if attribute exists.
php_parser_py/_parser.py CHANGED
@@ -60,15 +60,16 @@ class Parser:
60
60
  raise
61
61
 
62
62
  # Normalize to list
63
- if not isinstance(json_data, list):
64
- json_data = [json_data]
63
+ node_list: list[dict[str, Any]] = (
64
+ [json_data] if not isinstance(json_data, list) else json_data
65
+ )
65
66
 
66
67
  # Create temporary storage to convert JSON to nodes
67
68
  storage = Storage()
68
- node_ids = []
69
+ node_ids: list[str] = []
69
70
  node_counter = [1] # Use list to allow mutation in recursive calls
70
71
 
71
- for item in json_data:
72
+ for item in node_list:
72
73
  node_id = self._process_node(
73
74
  storage, item, None, None, None, node_counter, ""
74
75
  )
@@ -114,12 +115,11 @@ class Parser:
114
115
  raise ParseError("Syntax error in PHP code", line=1) from e
115
116
  raise
116
117
 
117
- # Normalize json_data to list
118
- if not isinstance(json_data, list):
119
- json_data = [json_data]
120
-
118
+ file_list: list[dict[str, Any]] = (
119
+ [json_data] if not isinstance(json_data, list) else json_data
120
+ )
121
121
  storage = self._create_project_structure(
122
- [(file_path, json_data)],
122
+ [(file_path, file_list)],
123
123
  [(file_path, file_hash)],
124
124
  project_path=project_path,
125
125
  )
@@ -162,18 +162,14 @@ class Parser:
162
162
  php_files = [f for f in all_files if f.is_file() and file_filter(f)]
163
163
 
164
164
  if not php_files:
165
- logger.warning(f"No PHP files found in project directory: {project_path}")
165
+ logger.warning("No PHP files found in project directory: %s", project_path)
166
166
  # Create empty project structure
167
167
  storage = Storage()
168
168
  project_id = "project"
169
169
  storage.add_node(project_id)
170
170
  storage.set_node_props(
171
171
  project_id,
172
- {
173
- "nodeType": "Project",
174
- "label": "Project",
175
- "path": str(project_path_obj),
176
- },
172
+ {"nodeType": "Project", "absolutePath": str(project_path)},
177
173
  )
178
174
  return AST(storage, root_node_id="project")
179
175
 
@@ -189,9 +185,10 @@ class Parser:
189
185
  code = file_path.read_text(encoding="utf-8")
190
186
  try:
191
187
  json_data = self._runner.parse(code)
192
- if not isinstance(json_data, list):
193
- json_data = [json_data]
194
- all_json_data.append((file_path, json_data))
188
+ node_list: list[dict[str, Any]] = (
189
+ [json_data] if not isinstance(json_data, list) else json_data
190
+ )
191
+ all_json_data.append((file_path, node_list))
195
192
  except RunnerError as e:
196
193
  if "Syntax error" in str(e):
197
194
  raise ParseError(f"Syntax error in {file_path}", line=1) from e
@@ -225,7 +222,7 @@ class Parser:
225
222
  storage.add_node(project_id)
226
223
  storage.set_node_props(
227
224
  project_id,
228
- {"nodeType": "Project", "label": "Project", "path": str(project_path)},
225
+ {"nodeType": "Project", "absolutePath": str(project_path)},
229
226
  )
230
227
 
231
228
  # Create file nodes and process their statements
@@ -234,8 +231,26 @@ class Parser:
234
231
  try:
235
232
  relative_path = file_path.relative_to(project_path)
236
233
  except ValueError:
237
- # If file is not under project_path, use filename
238
- relative_path = file_path.name
234
+ relative_path = Path(file_path.name)
235
+
236
+ # Normalize and inspect statements to compute file position range
237
+ stmt_list: list[dict[str, Any]] = (
238
+ [json_data] if not isinstance(json_data, list) else json_data
239
+ )
240
+ file_end_line = 1
241
+ file_end_file_pos = 0
242
+ file_end_token_pos = 0
243
+ for stmt in stmt_list:
244
+ attributes = stmt.get("attributes", {})
245
+ end_line = attributes.get("endLine")
246
+ if isinstance(end_line, int) and end_line > file_end_line:
247
+ file_end_line = end_line
248
+ end_file_pos = attributes.get("endFilePos")
249
+ if isinstance(end_file_pos, int) and end_file_pos > file_end_file_pos:
250
+ file_end_file_pos = end_file_pos
251
+ end_token_pos = attributes.get("endTokenPos")
252
+ if isinstance(end_token_pos, int) and end_token_pos > file_end_token_pos:
253
+ file_end_token_pos = end_token_pos
239
254
 
240
255
  # Create file node (ID: hex hash)
241
256
  file_id = file_hash
@@ -244,9 +259,14 @@ class Parser:
244
259
  file_id,
245
260
  {
246
261
  "nodeType": "File",
247
- "label": "File",
248
- "filePath": str(file_path), # Absolute path
249
- "path": str(relative_path), # Relative path from project root
262
+ "absolutePath": str(file_path),
263
+ "relativePath": str(relative_path),
264
+ "startLine": 1,
265
+ "endLine": file_end_line,
266
+ "startFilePos": 0,
267
+ "endFilePos": file_end_file_pos,
268
+ "startTokenPos": 0,
269
+ "endTokenPos": file_end_token_pos,
250
270
  },
251
271
  )
252
272
 
@@ -257,17 +277,30 @@ class Parser:
257
277
 
258
278
  # Process statements in file (node IDs: {hex}_1, {hex}_2, ...)
259
279
  node_counter = [1]
260
- if not isinstance(json_data, list):
261
- json_data = [json_data]
262
-
263
- for idx, item in enumerate(json_data):
280
+ for idx, item in enumerate(stmt_list):
264
281
  self._process_node(
265
282
  storage, item, file_id, "stmts", idx, node_counter, file_hash
266
283
  )
267
284
 
285
+ # Project node does not correspond to a concrete source range.
286
+ # Use sentinel values for all position attributes.
287
+ storage.set_node_props(
288
+ project_id,
289
+ {
290
+ "nodeType": "Project",
291
+ "absolutePath": str(project_path),
292
+ "startLine": -1,
293
+ "endLine": -1,
294
+ "startFilePos": -1,
295
+ "endFilePos": -1,
296
+ "startTokenPos": -1,
297
+ "endTokenPos": -1,
298
+ },
299
+ )
300
+
268
301
  return storage
269
302
 
270
- def _process_node( # pylint: disable=too-many-arguments,too-many-locals,too-many-branches
303
+ def _process_node(
271
304
  self,
272
305
  storage: Storage,
273
306
  node_data: Any,
@@ -340,7 +373,7 @@ class Parser:
340
373
 
341
374
  # Create edge from parent if applicable
342
375
  if parent_id is not None and field_name is not None:
343
- edge_props = {"field": field_name}
376
+ edge_props: dict[str, str | int] = {"field": field_name}
344
377
  if index is not None:
345
378
  edge_props["index"] = index
346
379
  edge_id = (parent_id, node_id, "PARENT_OF")
@@ -349,20 +382,18 @@ class Parser:
349
382
  storage.set_edge_props(edge_id, edge_props)
350
383
 
351
384
  # Process child fields recursively
352
- for field_name, field_value in child_fields:
353
- if isinstance(field_value, list):
354
- # Array of child objects
355
- for idx, item in enumerate(field_value):
385
+ for child_key, child_value in child_fields:
386
+ if isinstance(child_value, list):
387
+ for idx, item in enumerate(child_value):
356
388
  self._process_node(
357
- storage, item, node_id, field_name, idx, node_counter, prefix
389
+ storage, item, node_id, child_key, idx, node_counter, prefix
358
390
  )
359
391
  else:
360
- # Single child object
361
392
  self._process_node(
362
393
  storage,
363
- field_value,
394
+ child_value,
364
395
  node_id,
365
- field_name,
396
+ child_key,
366
397
  None,
367
398
  node_counter,
368
399
  prefix,
php_parser_py/_printer.py CHANGED
@@ -59,7 +59,7 @@ class PrettyPrinter:
59
59
  # Generate code for each file
60
60
  result: dict[str, str] = {}
61
61
  for file_node in file_nodes:
62
- file_path = file_node.get_property("filePath", "")
62
+ file_path = file_node.get_property("absolutePath", "")
63
63
  file_hash = file_node.id
64
64
 
65
65
  # Get JSON for this file only
@@ -97,7 +97,7 @@ class PrettyPrinter:
97
97
  """
98
98
  # Find file node by its relative path property
99
99
  for file_node in ast.file_nodes():
100
- if file_node.get_property("path") == relative_path:
100
+ if file_node.get_property("relativePath") == relative_path:
101
101
  file_hash = file_node.id
102
102
  json_str = ast.to_json(file_hash=file_hash)
103
103
  return self._runner.print(json_str)
@@ -12,7 +12,7 @@ from typing import Optional
12
12
 
13
13
  # Global lock for thread-safe extraction
14
14
  _extraction_lock = threading.Lock()
15
- _extraction_done = False # pylint: disable=invalid-name
15
+ _extraction_state: dict[str, bool] = {"done": False}
16
16
 
17
17
 
18
18
  def get_vendor_path() -> Path:
@@ -55,7 +55,7 @@ def is_already_extracted(zip_path: Path) -> bool:
55
55
  stored_hash = marker_file.read_text().strip()
56
56
  current_hash = calculate_zip_hash(zip_path)
57
57
  return stored_hash == current_hash
58
- except Exception:
58
+ except OSError:
59
59
  # If we can't read the marker or calculate hash, re-extract
60
60
  return False
61
61
 
@@ -95,10 +95,7 @@ def ensure_php_parser_extracted() -> Path:
95
95
  FileNotFoundError: If the php-parser zip file is not found
96
96
  RuntimeError: If extraction fails
97
97
  """
98
- global _extraction_done
99
-
100
- # Fast path: already extracted in this process
101
- if _extraction_done:
98
+ if _extraction_state["done"]:
102
99
  return get_vendor_path()
103
100
 
104
101
  vendor_path = get_vendor_path()
@@ -114,25 +111,21 @@ def ensure_php_parser_extracted() -> Path:
114
111
 
115
112
  zip_path = zip_files[0]
116
113
 
117
- # Check if already extracted (with marker file)
118
114
  if is_already_extracted(zip_path):
119
- _extraction_done = True
115
+ _extraction_state["done"] = True
120
116
  return vendor_path
121
117
 
122
- # Thread-safe extraction
123
118
  with _extraction_lock:
124
- # Double-check after acquiring lock (another thread might have extracted)
125
119
  if is_already_extracted(zip_path):
126
- _extraction_done = True
120
+ _extraction_state["done"] = True
127
121
  return vendor_path
128
122
 
129
123
  try:
130
- # Extract PHP-Parser
131
124
  extract_php_parser(zip_path, vendor_path)
132
- _extraction_done = True
125
+ _extraction_state["done"] = True
133
126
  return vendor_path
134
127
 
135
- except Exception as e:
128
+ except (OSError, zipfile.BadZipFile, ValueError) as e:
136
129
  raise RuntimeError(
137
130
  f"Failed to extract PHP-Parser from {zip_path}: {e}"
138
131
  ) from e
php_parser_py/_runner.py CHANGED
@@ -3,7 +3,7 @@
3
3
  import json
4
4
  import logging
5
5
  import subprocess
6
- from typing import Any, Optional
6
+ from typing import Any, Optional, cast
7
7
 
8
8
  from static_php_py import PHP
9
9
  from static_php_py.exceptions import BinaryNotFoundError, DownloadError
@@ -39,7 +39,7 @@ class Runner:
39
39
  try:
40
40
  self._php = php if php is not None else PHP.builtin()
41
41
  self._php_binary = self._php.path()
42
-
42
+
43
43
  except BinaryNotFoundError as e:
44
44
  raise RunnerError(
45
45
  f"PHP binary not found: {e}. "
@@ -84,10 +84,10 @@ class Runner:
84
84
  # Log complete error information
85
85
  error_msg = f"PHP execution failed with exit code {result.returncode}"
86
86
  if result.stderr:
87
- logger.error(f"PHP stderr: {result.stderr}")
87
+ logger.error("PHP stderr: %s", result.stderr)
88
88
  error_msg += f"\nStderr: {result.stderr}"
89
89
  if result.stdout:
90
- logger.error(f"PHP stdout: {result.stdout}")
90
+ logger.error("PHP stdout: %s", result.stdout)
91
91
  error_msg += f"\nStdout: {result.stdout}"
92
92
 
93
93
  raise RunnerError(
@@ -139,7 +139,7 @@ class Runner:
139
139
  first_error.get("line"),
140
140
  )
141
141
 
142
- return result
142
+ return cast(dict[str, Any], result)
143
143
 
144
144
  except json.JSONDecodeError as e:
145
145
  raise RunnerError(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: php-parser-py
3
- Version: 1.2.0
3
+ Version: 1.2.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>
@@ -11,6 +11,8 @@ Description-Content-Type: text/markdown
11
11
 
12
12
  # php-parser-py
13
13
 
14
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jhu-seclab-beluga/php-parser-py)
15
+
14
16
  Python wrapper for [nikic/PHP-Parser](https://github.com/nikic/PHP-Parser) built on the [cpg2py](https://github.com/jhu-seclab/cpg2py) graph framework.
15
17
 
16
18
  - **Automated**: Uses [static-php-cli](https://github.com/static-php-cli/static-php-cli) binaries (no local PHP required).
@@ -0,0 +1,16 @@
1
+ php_parser_py/__init__.py,sha256=heraS-IKwTKjDeatzSe2OMPv7zUErOeuKZHMeJMEPUg,3387
2
+ php_parser_py/_ast.py,sha256=hEH5XmhCAD_Qn5QgVBmGlas0mUqKVMY1eMFXeLLZdtc,11744
3
+ php_parser_py/_edge.py,sha256=bHMtb0Bf-bQjBzlTaB01pnntYcE3WL6JRWiMyGlzq0M,2919
4
+ php_parser_py/_exceptions.py,sha256=4jX9WfV--vCnSacEQgEE0Xtm09Ii88GkYj17yBMf7Cc,1788
5
+ php_parser_py/_node.py,sha256=zyp0j4GRBCe2VFt22ajbYM9ZYuxgLs-Awdjwi2FisL4,9918
6
+ php_parser_py/_parser.py,sha256=mwi7idEJZLgzkMPvUThHyt1MjOEEuNEqfyJam9YVRg4,14511
7
+ php_parser_py/_printer.py,sha256=mvJ9o8ql_Yk9-6u69Jx-4azD7aIG9W1NDWaJVkzc2gI,3506
8
+ php_parser_py/_resources.py,sha256=TUYfH58HV1y93kq39gW-aHKkR1MRZQb131VHzfR4LBc,4305
9
+ php_parser_py/_runner.py,sha256=MxTh5Z7tuyOLneX0IggfshmyQf6mL1duJPYozu4899c,6985
10
+ php_parser_py/resources/php-parser-4.19.4.zip,sha256=ND6zsylItt4owMoQnq-3id_ScMAXEfVyHzzRi0wKthQ,144334
11
+ php_parser_py/vendor/.extracted,sha256=35VHVRaR8xmvJSW2J6Z8MXxiFVnsRgeuJJPNS_5yzB8,64
12
+ php_parser_py/vendor/LICENSE.txt,sha256=3YWeEH-sn4ekQTq4w8lvDxYRoX9Zai21YqWk9JBMHpc,1520
13
+ php_parser_py/vendor/php-parser.phar,sha256=xeuVm4n37yt_DX3pNH1Ugc1ihGtoC97b7ao7a-AGjTk,1009441
14
+ php_parser_py-1.2.1.dist-info/WHEEL,sha256=iHtWm8nRfs0VRdCYVXocAWFW8ppjHL-uTJkAdZJKOBM,80
15
+ php_parser_py-1.2.1.dist-info/METADATA,sha256=1IagDWYT0alURw-YwmgfCnu4nF9m5tpdob2vpr_DFS4,2037
16
+ php_parser_py-1.2.1.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- php_parser_py/__init__.py,sha256=Iiea9w5auFtG4yAA8KNECRIwbw6IysYteTWv8HcE16E,3284
2
- php_parser_py/_ast.py,sha256=QnkdtMsaB3IFDhGRxqr1QIXQif9blIZE8tIGus9qmuo,12728
3
- php_parser_py/_edge.py,sha256=CFGIakA_-kAeS8PQZ56ukBBohdBN7XpXZmYjuJ7fq1Q,3789
4
- php_parser_py/_exceptions.py,sha256=cqGfU64-jXzJ9Y0edlMMZUzmMyClco0t-e33p5r90TA,1351
5
- php_parser_py/_node.py,sha256=8YZfXjKf_4lf1R29UxxByw_iN2pBlTK6HNbx0z-9uWU,5654
6
- php_parser_py/_parser.py,sha256=rJ8_Oco7sCOwhy512oJJmGr2WijF25rqvLRA4Da6c3Y,13071
7
- php_parser_py/_printer.py,sha256=wj6poxv_85YvhTHwa0zEJpP3_FXyTWxxzBrxQTvtgSY,3494
8
- php_parser_py/_resources.py,sha256=1BL6cNiBuGr7kPFw7MKF5F_ZaUvFsvgEPjU2wThNlCE,4519
9
- php_parser_py/_runner.py,sha256=RVysl2489tBpg0t595nZ1buutz5HdOVjQaPnk36F9Tg,6967
10
- php_parser_py/resources/php-parser-4.19.4.zip,sha256=ND6zsylItt4owMoQnq-3id_ScMAXEfVyHzzRi0wKthQ,144334
11
- php_parser_py/vendor/.extracted,sha256=35VHVRaR8xmvJSW2J6Z8MXxiFVnsRgeuJJPNS_5yzB8,64
12
- php_parser_py/vendor/LICENSE.txt,sha256=3YWeEH-sn4ekQTq4w8lvDxYRoX9Zai21YqWk9JBMHpc,1520
13
- php_parser_py/vendor/php-parser.phar,sha256=xeuVm4n37yt_DX3pNH1Ugc1ihGtoC97b7ao7a-AGjTk,1009441
14
- php_parser_py-1.2.0.dist-info/WHEEL,sha256=iHtWm8nRfs0VRdCYVXocAWFW8ppjHL-uTJkAdZJKOBM,80
15
- php_parser_py-1.2.0.dist-info/METADATA,sha256=ji7zPJ5JqLdjVKyj-5AaArrh9Lc8mc-73yRqkAUdgpQ,1932
16
- php_parser_py-1.2.0.dist-info/RECORD,,