php-parser-py 1.1.0__tar.gz → 1.2.0__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.
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.3
2
+ Name: php-parser-py
3
+ Version: 1.2.0
4
+ Summary: Python wrapper for PHP-Parser using cpg2py
5
+ Author: samhsu-dev
6
+ Author-email: samhsu-dev <yxu166@jh.edu>
7
+ Requires-Dist: cpg2py>=1.2.0
8
+ Requires-Dist: static-php-py
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+
12
+ # php-parser-py
13
+
14
+ Python wrapper for [nikic/PHP-Parser](https://github.com/nikic/PHP-Parser) built on the [cpg2py](https://github.com/jhu-seclab/cpg2py) graph framework.
15
+
16
+ - **Automated**: Uses [static-php-cli](https://github.com/static-php-cli/static-php-cli) binaries (no local PHP required).
17
+ - **Graph-Based**: AST nodes are stored in a queryable graph database.
18
+ - **Accurate**: Full support for PHP-Parser 4.19.4 AST structure.
19
+
20
+ ## Quick Start
21
+ ```bash
22
+ pip install php-parser-py
23
+ ```
24
+
25
+ ```python
26
+ from php_parser_py import parse_file, parse_project
27
+
28
+ # 1. Parse
29
+ ast = parse_file("src/User.php")
30
+ # ast = parse_project("src/")
31
+
32
+ # 2. Traverse
33
+ files = ast.file_nodes()
34
+
35
+ # Find specific nodes
36
+ functions = ast.nodes(lambda n: n.node_type == "Stmt_Function")
37
+
38
+ # Get properties (scalars)
39
+ for func in functions:
40
+ print(f"Function: {func['namespacedName']}")
41
+
42
+ # Get child nodes (structure)
43
+ for func in functions:
44
+ # Use helper method or traverse edges
45
+ params = [c for c in ast.succ(func) if c.node_type == "Param"]
46
+ ```
47
+
48
+ ## Documentation
49
+
50
+ - **[Architecture & Design](docs/design.md)**
51
+ Detailed design documentation covering internal architecture, class specifications, and design rationale.
52
+
53
+ - **[AST Structure Reference](docs/libs/php_parser_ast.md)**
54
+ Complete table of all node types, subnodes (children), and properties. Verified against PHP-Parser source.
55
+
56
+ - **[Graph Traversal Guide](docs/libs/cpg2py_traversal.md)**
57
+ How to navigate the graph (`succ`, `prev`), access properties, and modify the graph (`set_property`).
58
+
59
+ ## Requirements
60
+ - Python ≥ 3.11
61
+ - (Optional) Custom PHP binary can be configured via `Parser` constructor.
@@ -0,0 +1,50 @@
1
+ # php-parser-py
2
+
3
+ Python wrapper for [nikic/PHP-Parser](https://github.com/nikic/PHP-Parser) built on the [cpg2py](https://github.com/jhu-seclab/cpg2py) graph framework.
4
+
5
+ - **Automated**: Uses [static-php-cli](https://github.com/static-php-cli/static-php-cli) binaries (no local PHP required).
6
+ - **Graph-Based**: AST nodes are stored in a queryable graph database.
7
+ - **Accurate**: Full support for PHP-Parser 4.19.4 AST structure.
8
+
9
+ ## Quick Start
10
+ ```bash
11
+ pip install php-parser-py
12
+ ```
13
+
14
+ ```python
15
+ from php_parser_py import parse_file, parse_project
16
+
17
+ # 1. Parse
18
+ ast = parse_file("src/User.php")
19
+ # ast = parse_project("src/")
20
+
21
+ # 2. Traverse
22
+ files = ast.file_nodes()
23
+
24
+ # Find specific nodes
25
+ functions = ast.nodes(lambda n: n.node_type == "Stmt_Function")
26
+
27
+ # Get properties (scalars)
28
+ for func in functions:
29
+ print(f"Function: {func['namespacedName']}")
30
+
31
+ # Get child nodes (structure)
32
+ for func in functions:
33
+ # Use helper method or traverse edges
34
+ params = [c for c in ast.succ(func) if c.node_type == "Param"]
35
+ ```
36
+
37
+ ## Documentation
38
+
39
+ - **[Architecture & Design](docs/design.md)**
40
+ Detailed design documentation covering internal architecture, class specifications, and design rationale.
41
+
42
+ - **[AST Structure Reference](docs/libs/php_parser_ast.md)**
43
+ Complete table of all node types, subnodes (children), and properties. Verified against PHP-Parser source.
44
+
45
+ - **[Graph Traversal Guide](docs/libs/cpg2py_traversal.md)**
46
+ How to navigate the graph (`succ`, `prev`), access properties, and modify the graph (`set_property`).
47
+
48
+ ## Requirements
49
+ - Python ≥ 3.11
50
+ - (Optional) Custom PHP binary can be configured via `Parser` constructor.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "php-parser-py"
3
- version = "1.1.0"
3
+ version = "1.2.0"
4
4
  description = "Python wrapper for PHP-Parser using cpg2py"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -8,7 +8,7 @@ authors = [
8
8
  ]
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
11
- "cpg2py>=1.1.0",
11
+ "cpg2py>=1.2.0",
12
12
  "static-php-py",
13
13
  ]
14
14
 
@@ -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.exceptions import ParseError, RunnerError
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(paths: list[str], project_path: str | None = None) -> AST:
106
- """Parse multiple PHP files into an AST.
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
- Creates a project node and multiple file nodes. Each file node
109
- contains statements from its corresponding file.
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
- paths: List of file path strings.
113
- project_path: Optional project root path. If not provided, computed as common parent.
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 any file does not exist.
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(paths, project_path=project_path)
105
+ return parser.parse_project(project_path, file_filter=file_filter)
124
106
 
125
107
 
126
108
  __version__ = "0.1.0"
@@ -77,35 +77,25 @@ class AST(AbcGraphQuerier[Node, Edge]):
77
77
  return None
78
78
  return Edge(self._storage, fid, tid, eid)
79
79
 
80
- @property
81
- def root_node(self) -> Optional[Node]:
82
- """Return the root project node of the AST.
80
+ def project_node(self) -> Optional[Node]:
81
+ """Return the project node.
83
82
 
84
- The root node is always the project node with ID "project".
83
+ The project node is the root of the AST with ID "project".
85
84
 
86
85
  Returns:
87
- Root Node instance or None if root node doesn't exist.
86
+ Project Node instance or None if not found.
88
87
  """
89
88
  if not self._storage.contains_node(self._root_node_id):
90
89
  return None
91
90
  return Node(self._storage, self._root_node_id)
92
91
 
93
- @property
94
- def project_node(self) -> Optional[Node]:
95
- """Return the project node (alias for root_node).
96
-
97
- Returns:
98
- Project Node instance or None if not found.
99
- """
100
- return self.root_node
101
-
102
- def files(self) -> list[Node]:
92
+ def file_nodes(self) -> list[Node]:
103
93
  """Return all file nodes in the project.
104
94
 
105
95
  Returns:
106
96
  List of File Node instances.
107
97
  """
108
- if not self.root_node:
98
+ if not self.project_node():
109
99
  return []
110
100
 
111
101
  file_nodes = []
@@ -119,7 +109,9 @@ class AST(AbcGraphQuerier[Node, Edge]):
119
109
  # Sort by file path for consistent ordering
120
110
  return sorted(file_nodes, key=lambda n: n.get_property("filePath", ""))
121
111
 
122
- def get_file(self, node_id: str) -> Optional[Node]:
112
+ def get_file(
113
+ self, node_id: str
114
+ ) -> Optional[Node]: # pylint: disable=too-many-return-statements
123
115
  """Get the file node that contains the given node.
124
116
 
125
117
  Traverses upward from the given node via PARENT_OF edges until
@@ -202,7 +194,7 @@ class AST(AbcGraphQuerier[Node, Edge]):
202
194
  top_level_nodes = self._get_file_statements(file_hash)
203
195
  else:
204
196
  # Try to export from file structure first
205
- file_nodes = self.files()
197
+ file_nodes = self.file_nodes()
206
198
  if file_nodes:
207
199
  # Export all files (flattened)
208
200
  top_level_nodes = []
@@ -215,7 +207,7 @@ class AST(AbcGraphQuerier[Node, Edge]):
215
207
  nodes_with_parents = set()
216
208
 
217
209
  for edge_id in self._storage.get_edges():
218
- from_id, to_id, edge_type = edge_id
210
+ _, to_id, edge_type = edge_id
219
211
  if edge_type == "PARENT_OF":
220
212
  nodes_with_parents.add(to_id)
221
213
 
@@ -253,10 +245,20 @@ class AST(AbcGraphQuerier[Node, Edge]):
253
245
 
254
246
  return sorted(statement_nodes, key=get_index)
255
247
 
256
- def _reconstruct_node(self, nid: str) -> dict[str, Any]:
257
- """Recursively reconstruct JSON object for a node."""
248
+ def _reconstruct_node( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
249
+ self, nid: str
250
+ ) -> dict[str, Any]:
251
+ """Recursively reconstruct JSON object for a node.
252
+
253
+ Args:
254
+ nid: Node ID to reconstruct.
255
+
256
+ Returns:
257
+ Dictionary representing the node in PHP-Parser JSON format.
258
+ """
258
259
  props = self._storage.get_node_props(nid)
259
260
  if props is None:
261
+ logger.warning("Node %s not found in storage, returning empty dict", nid)
260
262
  return {}
261
263
 
262
264
  result: dict[str, Any] = {}
@@ -348,10 +350,12 @@ class AST(AbcGraphQuerier[Node, Edge]):
348
350
 
349
351
  # Add default values for common optional fields that PHP-Parser expects
350
352
  if node_type and ("attrGroups" not in result):
351
- if (node_type.startswith("Stmt_") or
352
- node_type == "Param" or
353
- node_type.startswith("Expr_Closure") or
354
- node_type.startswith("Expr_ArrowFunction")):
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
+ ):
355
359
  result["attrGroups"] = []
356
-
360
+
357
361
  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__(self, graph: Storage, f_nid: str, t_nid: str, e_type: str = "PARENT_OF") -> None:
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,15 @@
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
+ from static_php_py import PHP
9
10
 
10
11
  from php_parser_py._ast import AST
12
+ from php_parser_py._exceptions import ParseError, RunnerError
11
13
  from php_parser_py._node import Node
12
14
  from php_parser_py._runner import Runner
13
- from php_parser_py.exceptions import ParseError, RunnerError
14
15
 
15
16
  logger = logging.getLogger(__name__)
16
17
 
@@ -21,56 +22,19 @@ class Parser:
21
22
  Provides three parsing methods:
22
23
  - parse_code: Parse code string, returns list of top-level statement nodes
23
24
  - parse_file: Parse single file, returns AST with project and file nodes
24
- - parse_project: Parse multiple files, returns AST with project and multiple file nodes
25
+ - parse_project: Parse project directory, returns AST with project and all PHP files
25
26
 
26
27
  Attributes:
27
28
  _runner: Runner instance for PHP-Parser invocation.
28
29
  """
29
30
 
30
- def __init__(
31
- self,
32
- php_binary_path: Optional[Path] = None,
33
- php_binary_url: Optional[str] = None,
34
- ) -> None:
31
+ def __init__(self, php: Optional[PHP] = None) -> None:
35
32
  """Initialize Parser with Runner.
36
-
37
- Args:
38
- php_binary_path: Optional path to local PHP binary.
39
- php_binary_url: Optional URL to download PHP binary from.
40
- """
41
- self._runner = Runner(
42
- php_binary_path=php_binary_path,
43
- php_binary_url=php_binary_url,
44
- )
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
33
 
52
34
  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.
35
+ php: Optional PHP instance. If not provided, uses builtin PHP.
61
36
  """
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()
37
+ self._runner = Runner(php=php)
74
38
 
75
39
  def parse_code(self, code: str) -> list[Node]:
76
40
  """Parse PHP code string into a list of top-level statement nodes.
@@ -94,7 +58,7 @@ class Parser:
94
58
  if "Syntax error" in str(e):
95
59
  raise ParseError("Syntax error in PHP code", line=1) from e
96
60
  raise
97
-
61
+
98
62
  # Normalize to list
99
63
  if not isinstance(json_data, list):
100
64
  json_data = [json_data]
@@ -105,7 +69,9 @@ class Parser:
105
69
  node_counter = [1] # Use list to allow mutation in recursive calls
106
70
 
107
71
  for item in json_data:
108
- node_id = self._process_node(storage, item, None, None, None, node_counter, "")
72
+ node_id = self._process_node(
73
+ storage, item, None, None, None, node_counter, ""
74
+ )
109
75
  if node_id:
110
76
  node_ids.append(node_id)
111
77
 
@@ -140,7 +106,7 @@ class Parser:
140
106
  file_hash = hashlib.md5(str(file_path).encode()).hexdigest()[:8]
141
107
 
142
108
  code = file_path.read_text(encoding="utf-8")
143
-
109
+
144
110
  try:
145
111
  json_data = self._runner.parse(code)
146
112
  except RunnerError as e:
@@ -153,63 +119,69 @@ class Parser:
153
119
  json_data = [json_data]
154
120
 
155
121
  storage = self._create_project_structure(
156
- [(file_path, json_data)],
122
+ [(file_path, json_data)],
157
123
  [(file_path, file_hash)],
158
- project_path=project_path
124
+ project_path=project_path,
159
125
  )
160
126
  return AST(storage, root_node_id="project")
161
127
 
162
- def parse_project(self, paths: list[str], project_path: str | None = None) -> AST:
163
- """Parse multiple PHP files into an AST with project and file nodes.
128
+ def parse_project(
129
+ self,
130
+ project_path: str,
131
+ file_filter: Callable[[Path], bool] = lambda p: p.suffix == ".php",
132
+ ) -> AST:
133
+ """Parse all PHP files in a project directory into an AST.
164
134
 
165
- Creates a project node (ID: "project") and multiple file nodes.
135
+ Recursively traverses the project directory to find all PHP files,
136
+ then parses them into a single AST with project and file nodes.
166
137
  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
138
 
169
139
  Args:
170
- paths: List of file path strings.
171
- project_path: Optional project root path. If not provided, computed as common parent.
140
+ project_path: Project root directory path.
141
+ file_filter: Function to filter files. Takes a Path and returns
142
+ True if the file should be parsed. Defaults to checking for `.php` suffix.
172
143
 
173
144
  Returns:
174
145
  AST instance with project -> files -> statements hierarchy.
175
146
 
176
147
  Raises:
177
148
  ParseError: If any file has syntax errors.
178
- FileNotFoundError: If any file does not exist.
149
+ FileNotFoundError: If project directory does not exist.
150
+ ValueError: If project_path is not a directory.
179
151
  """
152
+ project_path_obj = Path(project_path).resolve()
153
+
154
+ if not project_path_obj.exists():
155
+ raise FileNotFoundError(f"Project directory not found: {project_path}")
156
+
157
+ if not project_path_obj.is_dir():
158
+ raise ValueError(f"Project path is not a directory: {project_path}")
159
+
160
+ # Find all files recursively and filter
161
+ all_files = project_path_obj.rglob("*")
162
+ php_files = [f for f in all_files if f.is_file() and file_filter(f)]
163
+
164
+ if not php_files:
165
+ logger.warning(f"No PHP files found in project directory: {project_path}")
166
+ # Create empty project structure
167
+ storage = Storage()
168
+ project_id = "project"
169
+ storage.add_node(project_id)
170
+ storage.set_node_props(
171
+ project_id,
172
+ {
173
+ "nodeType": "Project",
174
+ "label": "Project",
175
+ "path": str(project_path_obj),
176
+ },
177
+ )
178
+ return AST(storage, root_node_id="project")
179
+
180
+ # Prepare file information
180
181
  file_infos = []
181
- resolved_paths = []
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}")
182
+ for file_path in php_files:
186
183
  file_hash = hashlib.md5(str(file_path).encode()).hexdigest()[:8]
187
184
  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
185
 
214
186
  # Parse all files
215
187
  all_json_data = []
@@ -225,14 +197,16 @@ class Parser:
225
197
  raise ParseError(f"Syntax error in {file_path}", line=1) from e
226
198
  raise
227
199
 
228
- storage = self._create_project_structure(all_json_data, file_infos, project_path=project_path)
200
+ storage = self._create_project_structure(
201
+ all_json_data, file_infos, project_path=project_path_obj
202
+ )
229
203
  return AST(storage, root_node_id="project")
230
204
 
231
205
  def _create_project_structure(
232
206
  self,
233
207
  files_data: list[tuple[Path, Any]],
234
208
  file_infos: list[tuple[Path, str]],
235
- project_path: Path
209
+ project_path: Path,
236
210
  ) -> Storage:
237
211
  """Create AST structure with project -> files -> statements hierarchy.
238
212
 
@@ -249,11 +223,10 @@ class Parser:
249
223
  # Create project node (fixed ID: "project")
250
224
  project_id = "project"
251
225
  storage.add_node(project_id)
252
- storage.set_node_props(project_id, {
253
- "nodeType": "Project",
254
- "label": "Project",
255
- "path": str(project_path)
256
- })
226
+ storage.set_node_props(
227
+ project_id,
228
+ {"nodeType": "Project", "label": "Project", "path": str(project_path)},
229
+ )
257
230
 
258
231
  # Create file nodes and process their statements
259
232
  for (file_path, file_hash), (_, json_data) in zip(file_infos, files_data):
@@ -267,12 +240,15 @@ class Parser:
267
240
  # Create file node (ID: hex hash)
268
241
  file_id = file_hash
269
242
  storage.add_node(file_id)
270
- storage.set_node_props(file_id, {
271
- "nodeType": "File",
272
- "label": "File",
273
- "filePath": str(file_path), # Absolute path
274
- "path": str(relative_path) # Relative path from project root
275
- })
243
+ storage.set_node_props(
244
+ file_id,
245
+ {
246
+ "nodeType": "File",
247
+ "label": "File",
248
+ "filePath": str(file_path), # Absolute path
249
+ "path": str(relative_path), # Relative path from project root
250
+ },
251
+ )
276
252
 
277
253
  # Link file to project
278
254
  edge_id = (project_id, file_id, "PARENT_OF")
@@ -291,7 +267,7 @@ class Parser:
291
267
 
292
268
  return storage
293
269
 
294
- def _process_node(
270
+ def _process_node( # pylint: disable=too-many-arguments,too-many-locals,too-many-branches
295
271
  self,
296
272
  storage: Storage,
297
273
  node_data: Any,
@@ -299,7 +275,7 @@ class Parser:
299
275
  field_name: str | None,
300
276
  index: int | None,
301
277
  node_counter: list[int],
302
- prefix: str
278
+ prefix: str,
303
279
  ) -> str | None:
304
280
  """Process a single node and its children recursively.
305
281
 
@@ -383,7 +359,13 @@ class Parser:
383
359
  else:
384
360
  # Single child object
385
361
  self._process_node(
386
- storage, field_value, node_id, field_name, None, node_counter, prefix
362
+ storage,
363
+ field_value,
364
+ node_id,
365
+ field_name,
366
+ None,
367
+ node_counter,
368
+ prefix,
387
369
  )
388
370
 
389
371
  return node_id