tree-sitter-analyzer 1.9.17.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.
- tree_sitter_analyzer/__init__.py +132 -0
- tree_sitter_analyzer/__main__.py +11 -0
- tree_sitter_analyzer/api.py +853 -0
- tree_sitter_analyzer/cli/__init__.py +39 -0
- tree_sitter_analyzer/cli/__main__.py +12 -0
- tree_sitter_analyzer/cli/argument_validator.py +89 -0
- tree_sitter_analyzer/cli/commands/__init__.py +26 -0
- tree_sitter_analyzer/cli/commands/advanced_command.py +226 -0
- tree_sitter_analyzer/cli/commands/base_command.py +181 -0
- tree_sitter_analyzer/cli/commands/default_command.py +18 -0
- tree_sitter_analyzer/cli/commands/find_and_grep_cli.py +188 -0
- tree_sitter_analyzer/cli/commands/list_files_cli.py +133 -0
- tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -0
- tree_sitter_analyzer/cli/commands/query_command.py +109 -0
- tree_sitter_analyzer/cli/commands/search_content_cli.py +161 -0
- tree_sitter_analyzer/cli/commands/structure_command.py +156 -0
- tree_sitter_analyzer/cli/commands/summary_command.py +116 -0
- tree_sitter_analyzer/cli/commands/table_command.py +414 -0
- tree_sitter_analyzer/cli/info_commands.py +124 -0
- tree_sitter_analyzer/cli_main.py +472 -0
- tree_sitter_analyzer/constants.py +85 -0
- tree_sitter_analyzer/core/__init__.py +15 -0
- tree_sitter_analyzer/core/analysis_engine.py +580 -0
- tree_sitter_analyzer/core/cache_service.py +333 -0
- tree_sitter_analyzer/core/engine.py +585 -0
- tree_sitter_analyzer/core/parser.py +293 -0
- tree_sitter_analyzer/core/query.py +605 -0
- tree_sitter_analyzer/core/query_filter.py +200 -0
- tree_sitter_analyzer/core/query_service.py +340 -0
- tree_sitter_analyzer/encoding_utils.py +530 -0
- tree_sitter_analyzer/exceptions.py +747 -0
- tree_sitter_analyzer/file_handler.py +246 -0
- tree_sitter_analyzer/formatters/__init__.py +1 -0
- tree_sitter_analyzer/formatters/base_formatter.py +201 -0
- tree_sitter_analyzer/formatters/csharp_formatter.py +367 -0
- tree_sitter_analyzer/formatters/formatter_config.py +197 -0
- tree_sitter_analyzer/formatters/formatter_factory.py +84 -0
- tree_sitter_analyzer/formatters/formatter_registry.py +377 -0
- tree_sitter_analyzer/formatters/formatter_selector.py +96 -0
- tree_sitter_analyzer/formatters/go_formatter.py +368 -0
- tree_sitter_analyzer/formatters/html_formatter.py +498 -0
- tree_sitter_analyzer/formatters/java_formatter.py +423 -0
- tree_sitter_analyzer/formatters/javascript_formatter.py +611 -0
- tree_sitter_analyzer/formatters/kotlin_formatter.py +268 -0
- tree_sitter_analyzer/formatters/language_formatter_factory.py +123 -0
- tree_sitter_analyzer/formatters/legacy_formatter_adapters.py +228 -0
- tree_sitter_analyzer/formatters/markdown_formatter.py +725 -0
- tree_sitter_analyzer/formatters/php_formatter.py +301 -0
- tree_sitter_analyzer/formatters/python_formatter.py +830 -0
- tree_sitter_analyzer/formatters/ruby_formatter.py +278 -0
- tree_sitter_analyzer/formatters/rust_formatter.py +233 -0
- tree_sitter_analyzer/formatters/sql_formatter_wrapper.py +689 -0
- tree_sitter_analyzer/formatters/sql_formatters.py +536 -0
- tree_sitter_analyzer/formatters/typescript_formatter.py +543 -0
- tree_sitter_analyzer/formatters/yaml_formatter.py +462 -0
- tree_sitter_analyzer/interfaces/__init__.py +9 -0
- tree_sitter_analyzer/interfaces/cli.py +535 -0
- tree_sitter_analyzer/interfaces/cli_adapter.py +359 -0
- tree_sitter_analyzer/interfaces/mcp_adapter.py +224 -0
- tree_sitter_analyzer/interfaces/mcp_server.py +428 -0
- tree_sitter_analyzer/language_detector.py +553 -0
- tree_sitter_analyzer/language_loader.py +271 -0
- tree_sitter_analyzer/languages/__init__.py +10 -0
- tree_sitter_analyzer/languages/csharp_plugin.py +1076 -0
- tree_sitter_analyzer/languages/css_plugin.py +449 -0
- tree_sitter_analyzer/languages/go_plugin.py +836 -0
- tree_sitter_analyzer/languages/html_plugin.py +496 -0
- tree_sitter_analyzer/languages/java_plugin.py +1299 -0
- tree_sitter_analyzer/languages/javascript_plugin.py +1622 -0
- tree_sitter_analyzer/languages/kotlin_plugin.py +656 -0
- tree_sitter_analyzer/languages/markdown_plugin.py +1928 -0
- tree_sitter_analyzer/languages/php_plugin.py +862 -0
- tree_sitter_analyzer/languages/python_plugin.py +1636 -0
- tree_sitter_analyzer/languages/ruby_plugin.py +757 -0
- tree_sitter_analyzer/languages/rust_plugin.py +673 -0
- tree_sitter_analyzer/languages/sql_plugin.py +2444 -0
- tree_sitter_analyzer/languages/typescript_plugin.py +1892 -0
- tree_sitter_analyzer/languages/yaml_plugin.py +695 -0
- tree_sitter_analyzer/legacy_table_formatter.py +860 -0
- tree_sitter_analyzer/mcp/__init__.py +34 -0
- tree_sitter_analyzer/mcp/resources/__init__.py +43 -0
- tree_sitter_analyzer/mcp/resources/code_file_resource.py +208 -0
- tree_sitter_analyzer/mcp/resources/project_stats_resource.py +586 -0
- tree_sitter_analyzer/mcp/server.py +869 -0
- tree_sitter_analyzer/mcp/tools/__init__.py +28 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +779 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +291 -0
- tree_sitter_analyzer/mcp/tools/base_tool.py +139 -0
- tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +816 -0
- tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +686 -0
- tree_sitter_analyzer/mcp/tools/list_files_tool.py +413 -0
- tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
- tree_sitter_analyzer/mcp/tools/query_tool.py +443 -0
- tree_sitter_analyzer/mcp/tools/read_partial_tool.py +464 -0
- tree_sitter_analyzer/mcp/tools/search_content_tool.py +836 -0
- tree_sitter_analyzer/mcp/tools/table_format_tool.py +572 -0
- tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +653 -0
- tree_sitter_analyzer/mcp/utils/__init__.py +113 -0
- tree_sitter_analyzer/mcp/utils/error_handler.py +569 -0
- tree_sitter_analyzer/mcp/utils/file_output_factory.py +217 -0
- tree_sitter_analyzer/mcp/utils/file_output_manager.py +322 -0
- tree_sitter_analyzer/mcp/utils/gitignore_detector.py +358 -0
- tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -0
- tree_sitter_analyzer/mcp/utils/search_cache.py +343 -0
- tree_sitter_analyzer/models.py +840 -0
- tree_sitter_analyzer/mypy_current_errors.txt +2 -0
- tree_sitter_analyzer/output_manager.py +255 -0
- tree_sitter_analyzer/platform_compat/__init__.py +3 -0
- tree_sitter_analyzer/platform_compat/adapter.py +324 -0
- tree_sitter_analyzer/platform_compat/compare.py +224 -0
- tree_sitter_analyzer/platform_compat/detector.py +67 -0
- tree_sitter_analyzer/platform_compat/fixtures.py +228 -0
- tree_sitter_analyzer/platform_compat/profiles.py +217 -0
- tree_sitter_analyzer/platform_compat/record.py +55 -0
- tree_sitter_analyzer/platform_compat/recorder.py +155 -0
- tree_sitter_analyzer/platform_compat/report.py +92 -0
- tree_sitter_analyzer/plugins/__init__.py +280 -0
- tree_sitter_analyzer/plugins/base.py +647 -0
- tree_sitter_analyzer/plugins/manager.py +384 -0
- tree_sitter_analyzer/project_detector.py +328 -0
- tree_sitter_analyzer/queries/__init__.py +27 -0
- tree_sitter_analyzer/queries/csharp.py +216 -0
- tree_sitter_analyzer/queries/css.py +615 -0
- tree_sitter_analyzer/queries/go.py +275 -0
- tree_sitter_analyzer/queries/html.py +543 -0
- tree_sitter_analyzer/queries/java.py +402 -0
- tree_sitter_analyzer/queries/javascript.py +724 -0
- tree_sitter_analyzer/queries/kotlin.py +192 -0
- tree_sitter_analyzer/queries/markdown.py +258 -0
- tree_sitter_analyzer/queries/php.py +95 -0
- tree_sitter_analyzer/queries/python.py +859 -0
- tree_sitter_analyzer/queries/ruby.py +92 -0
- tree_sitter_analyzer/queries/rust.py +223 -0
- tree_sitter_analyzer/queries/sql.py +555 -0
- tree_sitter_analyzer/queries/typescript.py +871 -0
- tree_sitter_analyzer/queries/yaml.py +236 -0
- tree_sitter_analyzer/query_loader.py +272 -0
- tree_sitter_analyzer/security/__init__.py +22 -0
- tree_sitter_analyzer/security/boundary_manager.py +277 -0
- tree_sitter_analyzer/security/regex_checker.py +297 -0
- tree_sitter_analyzer/security/validator.py +599 -0
- tree_sitter_analyzer/table_formatter.py +782 -0
- tree_sitter_analyzer/utils/__init__.py +53 -0
- tree_sitter_analyzer/utils/logging.py +433 -0
- tree_sitter_analyzer/utils/tree_sitter_compat.py +289 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/METADATA +485 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/RECORD +149 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/WHEEL +4 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/entry_points.txt +25 -0
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PHP Language Plugin
|
|
4
|
+
|
|
5
|
+
Provides PHP-specific parsing and element extraction functionality.
|
|
6
|
+
Supports extraction of classes, interfaces, traits, enums, methods, functions, properties, and use statements.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
import tree_sitter
|
|
13
|
+
|
|
14
|
+
from ..core.analysis_engine import AnalysisRequest
|
|
15
|
+
from ..models import AnalysisResult
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import tree_sitter
|
|
19
|
+
|
|
20
|
+
TREE_SITTER_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
TREE_SITTER_AVAILABLE = False
|
|
23
|
+
|
|
24
|
+
from ..models import Class, Function, Import, Variable
|
|
25
|
+
from ..plugins.base import ElementExtractor, LanguagePlugin
|
|
26
|
+
from ..utils import log_error
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PHPElementExtractor(ElementExtractor):
|
|
30
|
+
"""
|
|
31
|
+
PHP-specific element extractor.
|
|
32
|
+
|
|
33
|
+
This extractor parses PHP AST and extracts code elements, mapping them
|
|
34
|
+
to the unified element model:
|
|
35
|
+
- Classes, Interfaces, Traits, Enums → Class elements
|
|
36
|
+
- Methods, Functions → Function elements
|
|
37
|
+
- Properties, Constants → Variable elements
|
|
38
|
+
- Use statements → Import elements
|
|
39
|
+
|
|
40
|
+
The extractor handles modern PHP syntax including:
|
|
41
|
+
- PHP 8+ attributes
|
|
42
|
+
- PHP 8.1+ enums
|
|
43
|
+
- PHP 7.4+ typed properties
|
|
44
|
+
- Magic methods
|
|
45
|
+
- Namespaces
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Initialize the PHP element extractor.
|
|
51
|
+
|
|
52
|
+
Sets up internal state for source code processing and performance
|
|
53
|
+
optimization caches for node text extraction.
|
|
54
|
+
"""
|
|
55
|
+
super().__init__()
|
|
56
|
+
self.source_code: str = ""
|
|
57
|
+
self.content_lines: list[str] = []
|
|
58
|
+
self.current_namespace: str = ""
|
|
59
|
+
|
|
60
|
+
# Performance optimization caches
|
|
61
|
+
self._node_text_cache: dict[int, str] = {}
|
|
62
|
+
self._processed_nodes: set[int] = set()
|
|
63
|
+
self._element_cache: dict[tuple[int, str], Any] = {}
|
|
64
|
+
self._file_encoding: str | None = None
|
|
65
|
+
self._attribute_cache: dict[int, list[dict[str, Any]]] = {}
|
|
66
|
+
|
|
67
|
+
def _reset_caches(self) -> None:
|
|
68
|
+
"""Reset all internal caches for a new file analysis."""
|
|
69
|
+
self._node_text_cache.clear()
|
|
70
|
+
self._processed_nodes.clear()
|
|
71
|
+
self._element_cache.clear()
|
|
72
|
+
self._attribute_cache.clear()
|
|
73
|
+
self.current_namespace = ""
|
|
74
|
+
|
|
75
|
+
def _get_node_text_optimized(self, node: "tree_sitter.Node") -> str:
|
|
76
|
+
"""
|
|
77
|
+
Get text content of a node with caching for performance.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
node: Tree-sitter node to extract text from
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Text content of the node as string
|
|
84
|
+
"""
|
|
85
|
+
# Use node position as cache key instead of object id for deterministic behavior
|
|
86
|
+
cache_key = (node.start_byte, node.end_byte)
|
|
87
|
+
if cache_key in self._node_text_cache:
|
|
88
|
+
return self._node_text_cache[cache_key]
|
|
89
|
+
|
|
90
|
+
# Extract text directly from source code string
|
|
91
|
+
text = self.source_code[node.start_byte : node.end_byte]
|
|
92
|
+
self._node_text_cache[cache_key] = text
|
|
93
|
+
return text
|
|
94
|
+
|
|
95
|
+
def _extract_namespace(self, node: "tree_sitter.Node") -> None:
|
|
96
|
+
"""
|
|
97
|
+
Extract namespace from the AST and set current_namespace.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
node: Root node of the AST
|
|
101
|
+
"""
|
|
102
|
+
if node.type == "namespace_definition":
|
|
103
|
+
name_node = node.child_by_field_name("name")
|
|
104
|
+
if name_node:
|
|
105
|
+
self.current_namespace = self._get_node_text_optimized(name_node)
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
# Recursively search for namespace
|
|
109
|
+
for child in node.children:
|
|
110
|
+
if child.type == "namespace_definition":
|
|
111
|
+
name_node = child.child_by_field_name("name")
|
|
112
|
+
if name_node:
|
|
113
|
+
self.current_namespace = self._get_node_text_optimized(name_node)
|
|
114
|
+
return
|
|
115
|
+
elif child.child_count > 0:
|
|
116
|
+
self._extract_namespace(child)
|
|
117
|
+
|
|
118
|
+
def _extract_modifiers(self, node: "tree_sitter.Node") -> list[str]:
|
|
119
|
+
"""
|
|
120
|
+
Extract modifiers from a declaration node.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
node: Declaration node (class, method, property, etc.)
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
List of modifier strings (e.g., ["public", "static", "final"])
|
|
127
|
+
"""
|
|
128
|
+
modifiers: list[str] = []
|
|
129
|
+
for child in node.children:
|
|
130
|
+
if child.type in (
|
|
131
|
+
"visibility_modifier",
|
|
132
|
+
"static_modifier",
|
|
133
|
+
"final_modifier",
|
|
134
|
+
"abstract_modifier",
|
|
135
|
+
"readonly_modifier",
|
|
136
|
+
):
|
|
137
|
+
modifier_text = self._get_node_text_optimized(child)
|
|
138
|
+
modifiers.append(modifier_text)
|
|
139
|
+
return modifiers
|
|
140
|
+
|
|
141
|
+
def _determine_visibility(self, modifiers: list[str]) -> str:
|
|
142
|
+
"""
|
|
143
|
+
Determine visibility from modifiers.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
modifiers: List of modifier strings
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Visibility string ("public", "private", "protected")
|
|
150
|
+
"""
|
|
151
|
+
if "public" in modifiers:
|
|
152
|
+
return "public"
|
|
153
|
+
elif "private" in modifiers:
|
|
154
|
+
return "private"
|
|
155
|
+
elif "protected" in modifiers:
|
|
156
|
+
return "protected"
|
|
157
|
+
else:
|
|
158
|
+
return "public" # PHP default visibility
|
|
159
|
+
|
|
160
|
+
def _extract_attributes(self, node: "tree_sitter.Node") -> list[dict[str, Any]]:
|
|
161
|
+
"""
|
|
162
|
+
Extract PHP 8+ attributes from a node.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
node: Node to extract attributes from
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
List of attribute dictionaries with name and arguments
|
|
169
|
+
"""
|
|
170
|
+
# Check cache first
|
|
171
|
+
node_id = id(node)
|
|
172
|
+
if node_id in self._attribute_cache:
|
|
173
|
+
return self._attribute_cache[node_id]
|
|
174
|
+
|
|
175
|
+
attributes: list[dict[str, Any]] = []
|
|
176
|
+
|
|
177
|
+
# Look for attribute_list nodes before the declaration
|
|
178
|
+
for child in node.children:
|
|
179
|
+
if child.type == "attribute_list":
|
|
180
|
+
for attr_group in child.children:
|
|
181
|
+
if attr_group.type == "attribute_group":
|
|
182
|
+
for attr in attr_group.children:
|
|
183
|
+
if attr.type == "attribute":
|
|
184
|
+
name_node = attr.child_by_field_name("name")
|
|
185
|
+
if name_node:
|
|
186
|
+
attr_name = self._get_node_text_optimized(name_node)
|
|
187
|
+
attributes.append(
|
|
188
|
+
{"name": attr_name, "arguments": []}
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
self._attribute_cache[node_id] = attributes
|
|
192
|
+
return attributes
|
|
193
|
+
|
|
194
|
+
def extract_classes(
|
|
195
|
+
self, tree: "tree_sitter.Tree", source_code: str
|
|
196
|
+
) -> list[Class]:
|
|
197
|
+
"""
|
|
198
|
+
Extract PHP classes, interfaces, traits, and enums.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
tree: Parsed tree-sitter tree
|
|
202
|
+
source_code: Source code string
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
List of Class elements
|
|
206
|
+
"""
|
|
207
|
+
self.source_code = source_code
|
|
208
|
+
self.content_lines = source_code.splitlines()
|
|
209
|
+
self._reset_caches()
|
|
210
|
+
self._extract_namespace(tree.root_node)
|
|
211
|
+
|
|
212
|
+
classes: list[Class] = []
|
|
213
|
+
|
|
214
|
+
# Iterative traversal to avoid stack overflow
|
|
215
|
+
stack: list[tree_sitter.Node] = [tree.root_node]
|
|
216
|
+
|
|
217
|
+
while stack:
|
|
218
|
+
node = stack.pop()
|
|
219
|
+
|
|
220
|
+
if node.type in (
|
|
221
|
+
"class_declaration",
|
|
222
|
+
"interface_declaration",
|
|
223
|
+
"trait_declaration",
|
|
224
|
+
"enum_declaration",
|
|
225
|
+
):
|
|
226
|
+
class_elem = self._extract_class_element(node)
|
|
227
|
+
if class_elem:
|
|
228
|
+
classes.append(class_elem)
|
|
229
|
+
|
|
230
|
+
# Add children to stack for traversal
|
|
231
|
+
for child in reversed(node.children):
|
|
232
|
+
stack.append(child)
|
|
233
|
+
|
|
234
|
+
return classes
|
|
235
|
+
|
|
236
|
+
def _extract_class_element(self, node: "tree_sitter.Node") -> Class | None:
|
|
237
|
+
"""
|
|
238
|
+
Extract a single class, interface, trait, or enum element.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
node: Class/interface/trait/enum declaration node
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Class element or None if extraction fails
|
|
245
|
+
"""
|
|
246
|
+
try:
|
|
247
|
+
name_node = node.child_by_field_name("name")
|
|
248
|
+
if not name_node:
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
name = self._get_node_text_optimized(name_node)
|
|
252
|
+
modifiers = self._extract_modifiers(node)
|
|
253
|
+
visibility = self._determine_visibility(modifiers)
|
|
254
|
+
attributes = self._extract_attributes(node)
|
|
255
|
+
|
|
256
|
+
# Determine type
|
|
257
|
+
is_interface = node.type == "interface_declaration"
|
|
258
|
+
is_trait = node.type == "trait_declaration"
|
|
259
|
+
is_enum = node.type == "enum_declaration"
|
|
260
|
+
|
|
261
|
+
# Extract base class and interfaces
|
|
262
|
+
base_classes: list[str] = []
|
|
263
|
+
interfaces: list[str] = []
|
|
264
|
+
|
|
265
|
+
for child in node.children:
|
|
266
|
+
if child.type == "base_clause":
|
|
267
|
+
base_node = child.child_by_field_name("type")
|
|
268
|
+
if base_node:
|
|
269
|
+
base_classes.append(self._get_node_text_optimized(base_node))
|
|
270
|
+
elif child.type == "class_interface_clause":
|
|
271
|
+
for interface_node in child.children:
|
|
272
|
+
if interface_node.type == "name":
|
|
273
|
+
interfaces.append(
|
|
274
|
+
self._get_node_text_optimized(interface_node)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Build fully qualified name
|
|
278
|
+
full_name = (
|
|
279
|
+
f"{self.current_namespace}\\{name}" if self.current_namespace else name
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Determine class type
|
|
283
|
+
class_type = "class"
|
|
284
|
+
if is_interface:
|
|
285
|
+
class_type = "interface"
|
|
286
|
+
elif is_trait:
|
|
287
|
+
class_type = "trait"
|
|
288
|
+
elif is_enum:
|
|
289
|
+
class_type = "enum"
|
|
290
|
+
|
|
291
|
+
return Class(
|
|
292
|
+
name=full_name,
|
|
293
|
+
start_line=node.start_point[0] + 1,
|
|
294
|
+
end_line=node.end_point[0] + 1,
|
|
295
|
+
visibility=visibility,
|
|
296
|
+
is_abstract="abstract" in modifiers,
|
|
297
|
+
full_qualified_name=full_name,
|
|
298
|
+
superclass=base_classes[0] if base_classes else None,
|
|
299
|
+
interfaces=interfaces,
|
|
300
|
+
modifiers=modifiers,
|
|
301
|
+
annotations=[{"name": attr["name"]} for attr in attributes],
|
|
302
|
+
class_type=class_type,
|
|
303
|
+
)
|
|
304
|
+
except Exception as e:
|
|
305
|
+
log_error(f"Error extracting class element: {e}")
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
def extract_functions(
|
|
309
|
+
self, tree: "tree_sitter.Tree", source_code: str
|
|
310
|
+
) -> list[Function]:
|
|
311
|
+
"""
|
|
312
|
+
Extract PHP methods and functions.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
tree: Parsed tree-sitter tree
|
|
316
|
+
source_code: Source code string
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
List of Function elements
|
|
320
|
+
"""
|
|
321
|
+
self.source_code = source_code
|
|
322
|
+
self.content_lines = source_code.splitlines()
|
|
323
|
+
|
|
324
|
+
functions: list[Function] = []
|
|
325
|
+
|
|
326
|
+
# Iterative traversal
|
|
327
|
+
stack: list[tuple[tree_sitter.Node, str]] = [(tree.root_node, "")]
|
|
328
|
+
|
|
329
|
+
while stack:
|
|
330
|
+
node, parent_class = stack.pop()
|
|
331
|
+
|
|
332
|
+
if node.type == "method_declaration":
|
|
333
|
+
func_elem = self._extract_method_element(node, parent_class)
|
|
334
|
+
if func_elem:
|
|
335
|
+
functions.append(func_elem)
|
|
336
|
+
elif node.type == "function_definition":
|
|
337
|
+
func_elem = self._extract_function_element(node)
|
|
338
|
+
if func_elem:
|
|
339
|
+
functions.append(func_elem)
|
|
340
|
+
|
|
341
|
+
# Track parent class for methods
|
|
342
|
+
new_parent = parent_class
|
|
343
|
+
if node.type in (
|
|
344
|
+
"class_declaration",
|
|
345
|
+
"interface_declaration",
|
|
346
|
+
"trait_declaration",
|
|
347
|
+
):
|
|
348
|
+
name_node = node.child_by_field_name("name")
|
|
349
|
+
if name_node:
|
|
350
|
+
new_parent = self._get_node_text_optimized(name_node)
|
|
351
|
+
|
|
352
|
+
# Add children to stack
|
|
353
|
+
for child in reversed(node.children):
|
|
354
|
+
stack.append((child, new_parent))
|
|
355
|
+
|
|
356
|
+
return functions
|
|
357
|
+
|
|
358
|
+
def _extract_method_element(
|
|
359
|
+
self, node: "tree_sitter.Node", parent_class: str
|
|
360
|
+
) -> Function | None:
|
|
361
|
+
"""
|
|
362
|
+
Extract a method element.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
node: Method declaration node
|
|
366
|
+
parent_class: Name of the parent class
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Function element or None if extraction fails
|
|
370
|
+
"""
|
|
371
|
+
try:
|
|
372
|
+
name_node = node.child_by_field_name("name")
|
|
373
|
+
if not name_node:
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
name = self._get_node_text_optimized(name_node)
|
|
377
|
+
modifiers = self._extract_modifiers(node)
|
|
378
|
+
visibility = self._determine_visibility(modifiers)
|
|
379
|
+
attributes = self._extract_attributes(node)
|
|
380
|
+
|
|
381
|
+
# Extract parameters
|
|
382
|
+
parameters: list[str] = []
|
|
383
|
+
params_node = node.child_by_field_name("parameters")
|
|
384
|
+
if params_node:
|
|
385
|
+
for param in params_node.children:
|
|
386
|
+
if (
|
|
387
|
+
param.type == "simple_parameter"
|
|
388
|
+
or param.type == "property_promotion_parameter"
|
|
389
|
+
):
|
|
390
|
+
param_text = self._get_node_text_optimized(param)
|
|
391
|
+
parameters.append(param_text)
|
|
392
|
+
|
|
393
|
+
# Extract return type
|
|
394
|
+
return_type = "void"
|
|
395
|
+
return_type_node = node.child_by_field_name("return_type")
|
|
396
|
+
if return_type_node:
|
|
397
|
+
return_type = self._get_node_text_optimized(return_type_node)
|
|
398
|
+
|
|
399
|
+
# Check if magic method
|
|
400
|
+
# is_magic = name.startswith("__") # Reserved for future use
|
|
401
|
+
|
|
402
|
+
return Function(
|
|
403
|
+
name=f"{parent_class}::{name}" if parent_class else name,
|
|
404
|
+
start_line=node.start_point[0] + 1,
|
|
405
|
+
end_line=node.end_point[0] + 1,
|
|
406
|
+
visibility=visibility,
|
|
407
|
+
is_static="static" in modifiers,
|
|
408
|
+
is_async=False, # PHP doesn't have async/await like C#
|
|
409
|
+
is_abstract="abstract" in modifiers,
|
|
410
|
+
parameters=parameters,
|
|
411
|
+
return_type=return_type,
|
|
412
|
+
modifiers=modifiers,
|
|
413
|
+
annotations=[{"name": attr["name"]} for attr in attributes],
|
|
414
|
+
)
|
|
415
|
+
except Exception as e:
|
|
416
|
+
log_error(f"Error extracting method element: {e}")
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
def _extract_function_element(self, node: "tree_sitter.Node") -> Function | None:
|
|
420
|
+
"""
|
|
421
|
+
Extract a function element.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
node: Function definition node
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Function element or None if extraction fails
|
|
428
|
+
"""
|
|
429
|
+
try:
|
|
430
|
+
name_node = node.child_by_field_name("name")
|
|
431
|
+
if not name_node:
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
name = self._get_node_text_optimized(name_node)
|
|
435
|
+
|
|
436
|
+
# Extract parameters
|
|
437
|
+
parameters: list[str] = []
|
|
438
|
+
params_node = node.child_by_field_name("parameters")
|
|
439
|
+
if params_node:
|
|
440
|
+
for param in params_node.children:
|
|
441
|
+
if param.type == "simple_parameter":
|
|
442
|
+
param_text = self._get_node_text_optimized(param)
|
|
443
|
+
parameters.append(param_text)
|
|
444
|
+
|
|
445
|
+
# Extract return type
|
|
446
|
+
return_type = "void"
|
|
447
|
+
return_type_node = node.child_by_field_name("return_type")
|
|
448
|
+
if return_type_node:
|
|
449
|
+
return_type = self._get_node_text_optimized(return_type_node)
|
|
450
|
+
|
|
451
|
+
# Build fully qualified name
|
|
452
|
+
full_name = (
|
|
453
|
+
f"{self.current_namespace}\\{name}" if self.current_namespace else name
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
return Function(
|
|
457
|
+
name=full_name,
|
|
458
|
+
start_line=node.start_point[0] + 1,
|
|
459
|
+
end_line=node.end_point[0] + 1,
|
|
460
|
+
visibility="public",
|
|
461
|
+
is_static=False,
|
|
462
|
+
is_async=False,
|
|
463
|
+
is_abstract=False,
|
|
464
|
+
parameters=parameters,
|
|
465
|
+
return_type=return_type,
|
|
466
|
+
modifiers=[],
|
|
467
|
+
annotations=[],
|
|
468
|
+
)
|
|
469
|
+
except Exception as e:
|
|
470
|
+
log_error(f"Error extracting function element: {e}")
|
|
471
|
+
return None
|
|
472
|
+
|
|
473
|
+
def extract_variables(
|
|
474
|
+
self, tree: "tree_sitter.Tree", source_code: str
|
|
475
|
+
) -> list[Variable]:
|
|
476
|
+
"""
|
|
477
|
+
Extract PHP properties and constants.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
tree: Parsed tree-sitter tree
|
|
481
|
+
source_code: Source code string
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
List of Variable elements
|
|
485
|
+
"""
|
|
486
|
+
self.source_code = source_code
|
|
487
|
+
self.content_lines = source_code.splitlines()
|
|
488
|
+
|
|
489
|
+
variables: list[Variable] = []
|
|
490
|
+
|
|
491
|
+
# Iterative traversal
|
|
492
|
+
stack: list[tuple[tree_sitter.Node, str]] = [(tree.root_node, "")]
|
|
493
|
+
|
|
494
|
+
while stack:
|
|
495
|
+
node, parent_class = stack.pop()
|
|
496
|
+
|
|
497
|
+
if node.type == "property_declaration":
|
|
498
|
+
var_elems = self._extract_property_elements(node, parent_class)
|
|
499
|
+
variables.extend(var_elems)
|
|
500
|
+
elif node.type == "const_declaration":
|
|
501
|
+
var_elems = self._extract_constant_elements(node, parent_class)
|
|
502
|
+
variables.extend(var_elems)
|
|
503
|
+
|
|
504
|
+
# Track parent class
|
|
505
|
+
new_parent = parent_class
|
|
506
|
+
if node.type in (
|
|
507
|
+
"class_declaration",
|
|
508
|
+
"interface_declaration",
|
|
509
|
+
"trait_declaration",
|
|
510
|
+
):
|
|
511
|
+
name_node = node.child_by_field_name("name")
|
|
512
|
+
if name_node:
|
|
513
|
+
new_parent = self._get_node_text_optimized(name_node)
|
|
514
|
+
|
|
515
|
+
# Add children to stack
|
|
516
|
+
for child in reversed(node.children):
|
|
517
|
+
stack.append((child, new_parent))
|
|
518
|
+
|
|
519
|
+
return variables
|
|
520
|
+
|
|
521
|
+
def _extract_property_elements(
|
|
522
|
+
self, node: "tree_sitter.Node", parent_class: str
|
|
523
|
+
) -> list[Variable]:
|
|
524
|
+
"""
|
|
525
|
+
Extract property elements from a property declaration.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
node: Property declaration node
|
|
529
|
+
parent_class: Name of the parent class
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
List of Variable elements
|
|
533
|
+
"""
|
|
534
|
+
variables: list[Variable] = []
|
|
535
|
+
|
|
536
|
+
try:
|
|
537
|
+
modifiers = self._extract_modifiers(node)
|
|
538
|
+
visibility = self._determine_visibility(modifiers)
|
|
539
|
+
|
|
540
|
+
# Extract type
|
|
541
|
+
var_type = "mixed"
|
|
542
|
+
type_node = node.child_by_field_name("type")
|
|
543
|
+
if type_node:
|
|
544
|
+
var_type = self._get_node_text_optimized(type_node)
|
|
545
|
+
|
|
546
|
+
# Extract property names
|
|
547
|
+
for child in node.children:
|
|
548
|
+
if child.type == "property_element":
|
|
549
|
+
name_node = child.child_by_field_name("name")
|
|
550
|
+
if name_node:
|
|
551
|
+
name = self._get_node_text_optimized(name_node).lstrip("$")
|
|
552
|
+
full_name = f"{parent_class}::{name}" if parent_class else name
|
|
553
|
+
|
|
554
|
+
variables.append(
|
|
555
|
+
Variable(
|
|
556
|
+
name=full_name,
|
|
557
|
+
start_line=node.start_point[0] + 1,
|
|
558
|
+
end_line=node.end_point[0] + 1,
|
|
559
|
+
visibility=visibility,
|
|
560
|
+
is_static="static" in modifiers,
|
|
561
|
+
is_constant=False,
|
|
562
|
+
is_final="readonly" in modifiers,
|
|
563
|
+
variable_type=var_type,
|
|
564
|
+
modifiers=modifiers,
|
|
565
|
+
)
|
|
566
|
+
)
|
|
567
|
+
except Exception as e:
|
|
568
|
+
log_error(f"Error extracting property elements: {e}")
|
|
569
|
+
|
|
570
|
+
return variables
|
|
571
|
+
|
|
572
|
+
def _extract_constant_elements(
|
|
573
|
+
self, node: "tree_sitter.Node", parent_class: str
|
|
574
|
+
) -> list[Variable]:
|
|
575
|
+
"""
|
|
576
|
+
Extract constant elements from a const declaration.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
node: Const declaration node
|
|
580
|
+
parent_class: Name of the parent class
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
List of Variable elements
|
|
584
|
+
"""
|
|
585
|
+
variables: list[Variable] = []
|
|
586
|
+
|
|
587
|
+
try:
|
|
588
|
+
modifiers = self._extract_modifiers(node)
|
|
589
|
+
visibility = self._determine_visibility(modifiers)
|
|
590
|
+
|
|
591
|
+
# Extract constant names
|
|
592
|
+
for child in node.children:
|
|
593
|
+
if child.type == "const_element":
|
|
594
|
+
name_node = child.child_by_field_name("name")
|
|
595
|
+
if name_node:
|
|
596
|
+
name = self._get_node_text_optimized(name_node)
|
|
597
|
+
full_name = f"{parent_class}::{name}" if parent_class else name
|
|
598
|
+
|
|
599
|
+
variables.append(
|
|
600
|
+
Variable(
|
|
601
|
+
name=full_name,
|
|
602
|
+
start_line=node.start_point[0] + 1,
|
|
603
|
+
end_line=node.end_point[0] + 1,
|
|
604
|
+
visibility=visibility,
|
|
605
|
+
is_static=True,
|
|
606
|
+
is_constant=True,
|
|
607
|
+
is_final=True,
|
|
608
|
+
variable_type="const",
|
|
609
|
+
modifiers=modifiers,
|
|
610
|
+
)
|
|
611
|
+
)
|
|
612
|
+
except Exception as e:
|
|
613
|
+
log_error(f"Error extracting constant elements: {e}")
|
|
614
|
+
|
|
615
|
+
return variables
|
|
616
|
+
|
|
617
|
+
def extract_imports(
|
|
618
|
+
self, tree: "tree_sitter.Tree", source_code: str
|
|
619
|
+
) -> list[Import]:
|
|
620
|
+
"""
|
|
621
|
+
Extract PHP use statements.
|
|
622
|
+
|
|
623
|
+
Args:
|
|
624
|
+
tree: Parsed tree-sitter tree
|
|
625
|
+
source_code: Source code string
|
|
626
|
+
|
|
627
|
+
Returns:
|
|
628
|
+
List of Import elements
|
|
629
|
+
"""
|
|
630
|
+
self.source_code = source_code
|
|
631
|
+
self.content_lines = source_code.splitlines()
|
|
632
|
+
|
|
633
|
+
imports: list[Import] = []
|
|
634
|
+
|
|
635
|
+
# Iterative traversal
|
|
636
|
+
stack: list[tree_sitter.Node] = [tree.root_node]
|
|
637
|
+
|
|
638
|
+
while stack:
|
|
639
|
+
node = stack.pop()
|
|
640
|
+
|
|
641
|
+
if node.type == "namespace_use_declaration":
|
|
642
|
+
import_elems = self._extract_use_statement(node)
|
|
643
|
+
imports.extend(import_elems)
|
|
644
|
+
|
|
645
|
+
# Add children to stack
|
|
646
|
+
for child in reversed(node.children):
|
|
647
|
+
stack.append(child)
|
|
648
|
+
|
|
649
|
+
return imports
|
|
650
|
+
|
|
651
|
+
def _extract_use_statement(self, node: "tree_sitter.Node") -> list[Import]:
|
|
652
|
+
"""
|
|
653
|
+
Extract use statement elements.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
node: Namespace use declaration node
|
|
657
|
+
|
|
658
|
+
Returns:
|
|
659
|
+
List of Import elements
|
|
660
|
+
"""
|
|
661
|
+
imports: list[Import] = []
|
|
662
|
+
|
|
663
|
+
try:
|
|
664
|
+
# Check for use type (function, const, or class)
|
|
665
|
+
# use_type = "class" # Reserved for future use
|
|
666
|
+
for child in node.children:
|
|
667
|
+
if child.type == "use":
|
|
668
|
+
use_text = self._get_node_text_optimized(child)
|
|
669
|
+
if "function" in use_text:
|
|
670
|
+
pass # use_type = "function" # Reserved for future use
|
|
671
|
+
elif "const" in use_text:
|
|
672
|
+
pass # use_type = "const" # Reserved for future use
|
|
673
|
+
|
|
674
|
+
# Extract use clauses
|
|
675
|
+
for child in node.children:
|
|
676
|
+
if child.type == "namespace_use_clause":
|
|
677
|
+
name_node = child.child_by_field_name("name")
|
|
678
|
+
alias_node = child.child_by_field_name("alias")
|
|
679
|
+
|
|
680
|
+
if name_node:
|
|
681
|
+
import_name = self._get_node_text_optimized(name_node)
|
|
682
|
+
alias = None
|
|
683
|
+
if alias_node:
|
|
684
|
+
alias = self._get_node_text_optimized(alias_node)
|
|
685
|
+
|
|
686
|
+
imports.append(
|
|
687
|
+
Import(
|
|
688
|
+
name=import_name,
|
|
689
|
+
start_line=node.start_point[0] + 1,
|
|
690
|
+
end_line=node.end_point[0] + 1,
|
|
691
|
+
alias=alias,
|
|
692
|
+
is_wildcard=False,
|
|
693
|
+
)
|
|
694
|
+
)
|
|
695
|
+
except Exception as e:
|
|
696
|
+
log_error(f"Error extracting use statement: {e}")
|
|
697
|
+
|
|
698
|
+
return imports
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
class PHPPlugin(LanguagePlugin):
|
|
702
|
+
"""
|
|
703
|
+
PHP language plugin.
|
|
704
|
+
|
|
705
|
+
Provides PHP-specific parsing and element extraction using tree-sitter-php.
|
|
706
|
+
Supports modern PHP features including PHP 8+ attributes, enums, and typed properties.
|
|
707
|
+
"""
|
|
708
|
+
|
|
709
|
+
_language_instance: Optional["tree_sitter.Language"] = None
|
|
710
|
+
|
|
711
|
+
def get_language_name(self) -> str:
|
|
712
|
+
"""
|
|
713
|
+
Get the name of the language.
|
|
714
|
+
|
|
715
|
+
Returns:
|
|
716
|
+
Language name string
|
|
717
|
+
"""
|
|
718
|
+
return "php"
|
|
719
|
+
|
|
720
|
+
def get_file_extensions(self) -> list[str]:
|
|
721
|
+
"""
|
|
722
|
+
Get supported file extensions.
|
|
723
|
+
|
|
724
|
+
Returns:
|
|
725
|
+
List of file extensions
|
|
726
|
+
"""
|
|
727
|
+
return [".php"]
|
|
728
|
+
|
|
729
|
+
def get_tree_sitter_language(self) -> "tree_sitter.Language":
|
|
730
|
+
"""
|
|
731
|
+
Get the tree-sitter language instance for PHP.
|
|
732
|
+
|
|
733
|
+
Returns:
|
|
734
|
+
tree-sitter Language instance
|
|
735
|
+
|
|
736
|
+
Raises:
|
|
737
|
+
ImportError: If tree-sitter-php is not installed
|
|
738
|
+
"""
|
|
739
|
+
if not TREE_SITTER_AVAILABLE:
|
|
740
|
+
raise ImportError(
|
|
741
|
+
"tree-sitter is not installed. Install it with: pip install tree-sitter"
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
if PHPPlugin._language_instance is None:
|
|
745
|
+
try:
|
|
746
|
+
import tree_sitter_php
|
|
747
|
+
|
|
748
|
+
PHPPlugin._language_instance = tree_sitter.Language(
|
|
749
|
+
tree_sitter_php.language_php()
|
|
750
|
+
)
|
|
751
|
+
except ImportError as e:
|
|
752
|
+
raise ImportError(
|
|
753
|
+
"tree-sitter-php is not installed. Install it with: pip install tree-sitter-php"
|
|
754
|
+
) from e
|
|
755
|
+
|
|
756
|
+
return PHPPlugin._language_instance
|
|
757
|
+
|
|
758
|
+
def create_extractor(self) -> ElementExtractor:
|
|
759
|
+
"""
|
|
760
|
+
Create a PHP element extractor.
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
PHPElementExtractor instance
|
|
764
|
+
"""
|
|
765
|
+
return PHPElementExtractor()
|
|
766
|
+
|
|
767
|
+
async def analyze_file(
|
|
768
|
+
self, file_path: str, request: "AnalysisRequest"
|
|
769
|
+
) -> "AnalysisResult":
|
|
770
|
+
"""
|
|
771
|
+
Analyze a PHP file.
|
|
772
|
+
|
|
773
|
+
Args:
|
|
774
|
+
file_path: Path to the PHP file
|
|
775
|
+
request: Analysis request configuration
|
|
776
|
+
|
|
777
|
+
Returns:
|
|
778
|
+
AnalysisResult containing extracted elements
|
|
779
|
+
"""
|
|
780
|
+
from ..models import AnalysisResult
|
|
781
|
+
|
|
782
|
+
try:
|
|
783
|
+
# Load file content
|
|
784
|
+
content = await self._load_file_safe(file_path)
|
|
785
|
+
|
|
786
|
+
# Parse with tree-sitter
|
|
787
|
+
language = self.get_tree_sitter_language()
|
|
788
|
+
parser = tree_sitter.Parser(language)
|
|
789
|
+
tree = parser.parse(content.encode("utf-8"))
|
|
790
|
+
|
|
791
|
+
# Extract elements
|
|
792
|
+
extractor = self.create_extractor()
|
|
793
|
+
classes = extractor.extract_classes(tree, content)
|
|
794
|
+
functions = extractor.extract_functions(tree, content)
|
|
795
|
+
variables = extractor.extract_variables(tree, content)
|
|
796
|
+
imports = extractor.extract_imports(tree, content)
|
|
797
|
+
|
|
798
|
+
# Combine all elements
|
|
799
|
+
all_elements = classes + functions + variables + imports
|
|
800
|
+
|
|
801
|
+
return AnalysisResult(
|
|
802
|
+
language=self.get_language_name(),
|
|
803
|
+
file_path=file_path,
|
|
804
|
+
success=True,
|
|
805
|
+
elements=all_elements,
|
|
806
|
+
node_count=self._count_nodes(tree.root_node),
|
|
807
|
+
)
|
|
808
|
+
except Exception as e:
|
|
809
|
+
log_error(f"Error analyzing PHP file {file_path}: {e}")
|
|
810
|
+
return AnalysisResult(
|
|
811
|
+
language=self.get_language_name(),
|
|
812
|
+
file_path=file_path,
|
|
813
|
+
success=False,
|
|
814
|
+
error_message=str(e),
|
|
815
|
+
elements=[],
|
|
816
|
+
node_count=0,
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
def _count_nodes(self, node: "tree_sitter.Node") -> int:
|
|
820
|
+
"""
|
|
821
|
+
Count total nodes in the AST.
|
|
822
|
+
|
|
823
|
+
Args:
|
|
824
|
+
node: Root node to count from
|
|
825
|
+
|
|
826
|
+
Returns:
|
|
827
|
+
Total node count
|
|
828
|
+
"""
|
|
829
|
+
count = 1
|
|
830
|
+
for child in node.children:
|
|
831
|
+
count += self._count_nodes(child)
|
|
832
|
+
return count
|
|
833
|
+
|
|
834
|
+
async def _load_file_safe(self, file_path: str) -> str:
|
|
835
|
+
"""
|
|
836
|
+
Load file content with encoding detection.
|
|
837
|
+
|
|
838
|
+
Args:
|
|
839
|
+
file_path: Path to the file
|
|
840
|
+
|
|
841
|
+
Returns:
|
|
842
|
+
File content as string
|
|
843
|
+
|
|
844
|
+
Raises:
|
|
845
|
+
IOError: If file cannot be read
|
|
846
|
+
"""
|
|
847
|
+
import chardet
|
|
848
|
+
|
|
849
|
+
try:
|
|
850
|
+
# Read file in binary mode
|
|
851
|
+
with open(file_path, "rb") as f:
|
|
852
|
+
raw_content = f.read()
|
|
853
|
+
|
|
854
|
+
# Detect encoding
|
|
855
|
+
detected = chardet.detect(raw_content)
|
|
856
|
+
encoding = detected.get("encoding", "utf-8")
|
|
857
|
+
|
|
858
|
+
# Decode with detected encoding
|
|
859
|
+
return raw_content.decode(encoding or "utf-8")
|
|
860
|
+
except Exception as e:
|
|
861
|
+
log_error(f"Error loading file {file_path}: {e}")
|
|
862
|
+
raise OSError(f"Failed to load file {file_path}: {e}") from e
|