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.
Files changed (149) hide show
  1. tree_sitter_analyzer/__init__.py +132 -0
  2. tree_sitter_analyzer/__main__.py +11 -0
  3. tree_sitter_analyzer/api.py +853 -0
  4. tree_sitter_analyzer/cli/__init__.py +39 -0
  5. tree_sitter_analyzer/cli/__main__.py +12 -0
  6. tree_sitter_analyzer/cli/argument_validator.py +89 -0
  7. tree_sitter_analyzer/cli/commands/__init__.py +26 -0
  8. tree_sitter_analyzer/cli/commands/advanced_command.py +226 -0
  9. tree_sitter_analyzer/cli/commands/base_command.py +181 -0
  10. tree_sitter_analyzer/cli/commands/default_command.py +18 -0
  11. tree_sitter_analyzer/cli/commands/find_and_grep_cli.py +188 -0
  12. tree_sitter_analyzer/cli/commands/list_files_cli.py +133 -0
  13. tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -0
  14. tree_sitter_analyzer/cli/commands/query_command.py +109 -0
  15. tree_sitter_analyzer/cli/commands/search_content_cli.py +161 -0
  16. tree_sitter_analyzer/cli/commands/structure_command.py +156 -0
  17. tree_sitter_analyzer/cli/commands/summary_command.py +116 -0
  18. tree_sitter_analyzer/cli/commands/table_command.py +414 -0
  19. tree_sitter_analyzer/cli/info_commands.py +124 -0
  20. tree_sitter_analyzer/cli_main.py +472 -0
  21. tree_sitter_analyzer/constants.py +85 -0
  22. tree_sitter_analyzer/core/__init__.py +15 -0
  23. tree_sitter_analyzer/core/analysis_engine.py +580 -0
  24. tree_sitter_analyzer/core/cache_service.py +333 -0
  25. tree_sitter_analyzer/core/engine.py +585 -0
  26. tree_sitter_analyzer/core/parser.py +293 -0
  27. tree_sitter_analyzer/core/query.py +605 -0
  28. tree_sitter_analyzer/core/query_filter.py +200 -0
  29. tree_sitter_analyzer/core/query_service.py +340 -0
  30. tree_sitter_analyzer/encoding_utils.py +530 -0
  31. tree_sitter_analyzer/exceptions.py +747 -0
  32. tree_sitter_analyzer/file_handler.py +246 -0
  33. tree_sitter_analyzer/formatters/__init__.py +1 -0
  34. tree_sitter_analyzer/formatters/base_formatter.py +201 -0
  35. tree_sitter_analyzer/formatters/csharp_formatter.py +367 -0
  36. tree_sitter_analyzer/formatters/formatter_config.py +197 -0
  37. tree_sitter_analyzer/formatters/formatter_factory.py +84 -0
  38. tree_sitter_analyzer/formatters/formatter_registry.py +377 -0
  39. tree_sitter_analyzer/formatters/formatter_selector.py +96 -0
  40. tree_sitter_analyzer/formatters/go_formatter.py +368 -0
  41. tree_sitter_analyzer/formatters/html_formatter.py +498 -0
  42. tree_sitter_analyzer/formatters/java_formatter.py +423 -0
  43. tree_sitter_analyzer/formatters/javascript_formatter.py +611 -0
  44. tree_sitter_analyzer/formatters/kotlin_formatter.py +268 -0
  45. tree_sitter_analyzer/formatters/language_formatter_factory.py +123 -0
  46. tree_sitter_analyzer/formatters/legacy_formatter_adapters.py +228 -0
  47. tree_sitter_analyzer/formatters/markdown_formatter.py +725 -0
  48. tree_sitter_analyzer/formatters/php_formatter.py +301 -0
  49. tree_sitter_analyzer/formatters/python_formatter.py +830 -0
  50. tree_sitter_analyzer/formatters/ruby_formatter.py +278 -0
  51. tree_sitter_analyzer/formatters/rust_formatter.py +233 -0
  52. tree_sitter_analyzer/formatters/sql_formatter_wrapper.py +689 -0
  53. tree_sitter_analyzer/formatters/sql_formatters.py +536 -0
  54. tree_sitter_analyzer/formatters/typescript_formatter.py +543 -0
  55. tree_sitter_analyzer/formatters/yaml_formatter.py +462 -0
  56. tree_sitter_analyzer/interfaces/__init__.py +9 -0
  57. tree_sitter_analyzer/interfaces/cli.py +535 -0
  58. tree_sitter_analyzer/interfaces/cli_adapter.py +359 -0
  59. tree_sitter_analyzer/interfaces/mcp_adapter.py +224 -0
  60. tree_sitter_analyzer/interfaces/mcp_server.py +428 -0
  61. tree_sitter_analyzer/language_detector.py +553 -0
  62. tree_sitter_analyzer/language_loader.py +271 -0
  63. tree_sitter_analyzer/languages/__init__.py +10 -0
  64. tree_sitter_analyzer/languages/csharp_plugin.py +1076 -0
  65. tree_sitter_analyzer/languages/css_plugin.py +449 -0
  66. tree_sitter_analyzer/languages/go_plugin.py +836 -0
  67. tree_sitter_analyzer/languages/html_plugin.py +496 -0
  68. tree_sitter_analyzer/languages/java_plugin.py +1299 -0
  69. tree_sitter_analyzer/languages/javascript_plugin.py +1622 -0
  70. tree_sitter_analyzer/languages/kotlin_plugin.py +656 -0
  71. tree_sitter_analyzer/languages/markdown_plugin.py +1928 -0
  72. tree_sitter_analyzer/languages/php_plugin.py +862 -0
  73. tree_sitter_analyzer/languages/python_plugin.py +1636 -0
  74. tree_sitter_analyzer/languages/ruby_plugin.py +757 -0
  75. tree_sitter_analyzer/languages/rust_plugin.py +673 -0
  76. tree_sitter_analyzer/languages/sql_plugin.py +2444 -0
  77. tree_sitter_analyzer/languages/typescript_plugin.py +1892 -0
  78. tree_sitter_analyzer/languages/yaml_plugin.py +695 -0
  79. tree_sitter_analyzer/legacy_table_formatter.py +860 -0
  80. tree_sitter_analyzer/mcp/__init__.py +34 -0
  81. tree_sitter_analyzer/mcp/resources/__init__.py +43 -0
  82. tree_sitter_analyzer/mcp/resources/code_file_resource.py +208 -0
  83. tree_sitter_analyzer/mcp/resources/project_stats_resource.py +586 -0
  84. tree_sitter_analyzer/mcp/server.py +869 -0
  85. tree_sitter_analyzer/mcp/tools/__init__.py +28 -0
  86. tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +779 -0
  87. tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +291 -0
  88. tree_sitter_analyzer/mcp/tools/base_tool.py +139 -0
  89. tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +816 -0
  90. tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +686 -0
  91. tree_sitter_analyzer/mcp/tools/list_files_tool.py +413 -0
  92. tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
  93. tree_sitter_analyzer/mcp/tools/query_tool.py +443 -0
  94. tree_sitter_analyzer/mcp/tools/read_partial_tool.py +464 -0
  95. tree_sitter_analyzer/mcp/tools/search_content_tool.py +836 -0
  96. tree_sitter_analyzer/mcp/tools/table_format_tool.py +572 -0
  97. tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +653 -0
  98. tree_sitter_analyzer/mcp/utils/__init__.py +113 -0
  99. tree_sitter_analyzer/mcp/utils/error_handler.py +569 -0
  100. tree_sitter_analyzer/mcp/utils/file_output_factory.py +217 -0
  101. tree_sitter_analyzer/mcp/utils/file_output_manager.py +322 -0
  102. tree_sitter_analyzer/mcp/utils/gitignore_detector.py +358 -0
  103. tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -0
  104. tree_sitter_analyzer/mcp/utils/search_cache.py +343 -0
  105. tree_sitter_analyzer/models.py +840 -0
  106. tree_sitter_analyzer/mypy_current_errors.txt +2 -0
  107. tree_sitter_analyzer/output_manager.py +255 -0
  108. tree_sitter_analyzer/platform_compat/__init__.py +3 -0
  109. tree_sitter_analyzer/platform_compat/adapter.py +324 -0
  110. tree_sitter_analyzer/platform_compat/compare.py +224 -0
  111. tree_sitter_analyzer/platform_compat/detector.py +67 -0
  112. tree_sitter_analyzer/platform_compat/fixtures.py +228 -0
  113. tree_sitter_analyzer/platform_compat/profiles.py +217 -0
  114. tree_sitter_analyzer/platform_compat/record.py +55 -0
  115. tree_sitter_analyzer/platform_compat/recorder.py +155 -0
  116. tree_sitter_analyzer/platform_compat/report.py +92 -0
  117. tree_sitter_analyzer/plugins/__init__.py +280 -0
  118. tree_sitter_analyzer/plugins/base.py +647 -0
  119. tree_sitter_analyzer/plugins/manager.py +384 -0
  120. tree_sitter_analyzer/project_detector.py +328 -0
  121. tree_sitter_analyzer/queries/__init__.py +27 -0
  122. tree_sitter_analyzer/queries/csharp.py +216 -0
  123. tree_sitter_analyzer/queries/css.py +615 -0
  124. tree_sitter_analyzer/queries/go.py +275 -0
  125. tree_sitter_analyzer/queries/html.py +543 -0
  126. tree_sitter_analyzer/queries/java.py +402 -0
  127. tree_sitter_analyzer/queries/javascript.py +724 -0
  128. tree_sitter_analyzer/queries/kotlin.py +192 -0
  129. tree_sitter_analyzer/queries/markdown.py +258 -0
  130. tree_sitter_analyzer/queries/php.py +95 -0
  131. tree_sitter_analyzer/queries/python.py +859 -0
  132. tree_sitter_analyzer/queries/ruby.py +92 -0
  133. tree_sitter_analyzer/queries/rust.py +223 -0
  134. tree_sitter_analyzer/queries/sql.py +555 -0
  135. tree_sitter_analyzer/queries/typescript.py +871 -0
  136. tree_sitter_analyzer/queries/yaml.py +236 -0
  137. tree_sitter_analyzer/query_loader.py +272 -0
  138. tree_sitter_analyzer/security/__init__.py +22 -0
  139. tree_sitter_analyzer/security/boundary_manager.py +277 -0
  140. tree_sitter_analyzer/security/regex_checker.py +297 -0
  141. tree_sitter_analyzer/security/validator.py +599 -0
  142. tree_sitter_analyzer/table_formatter.py +782 -0
  143. tree_sitter_analyzer/utils/__init__.py +53 -0
  144. tree_sitter_analyzer/utils/logging.py +433 -0
  145. tree_sitter_analyzer/utils/tree_sitter_compat.py +289 -0
  146. tree_sitter_analyzer-1.9.17.1.dist-info/METADATA +485 -0
  147. tree_sitter_analyzer-1.9.17.1.dist-info/RECORD +149 -0
  148. tree_sitter_analyzer-1.9.17.1.dist-info/WHEEL +4 -0
  149. 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