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.
- php_parser_py/__init__.py +86 -0
- php_parser_py/_ast.py +190 -0
- php_parser_py/_edge.py +130 -0
- php_parser_py/_node.py +208 -0
- php_parser_py/_parser.py +207 -0
- php_parser_py/_printer.py +52 -0
- php_parser_py/_resources.py +157 -0
- php_parser_py/_runner.py +239 -0
- php_parser_py/exceptions.py +44 -0
- php_parser_py/resources/php-parser-4.19.4.zip +0 -0
- php_parser_py/vendor/.extracted +1 -0
- php_parser_py/vendor/LICENSE.txt +29 -0
- php_parser_py/vendor/php-parser.phar +0 -0
- php_parser_py-0.1.1.dist-info/METADATA +190 -0
- php_parser_py-0.1.1.dist-info/RECORD +16 -0
- php_parser_py-0.1.1.dist-info/WHEEL +4 -0
|
@@ -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)
|