tree-sitter-analyzer 0.8.3__py3-none-any.whl → 0.9.2__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.

Potentially problematic release.


This version of tree-sitter-analyzer might be problematic. Click here for more details.

Files changed (62) hide show
  1. tree_sitter_analyzer/__init__.py +132 -132
  2. tree_sitter_analyzer/__main__.py +11 -11
  3. tree_sitter_analyzer/api.py +533 -533
  4. tree_sitter_analyzer/cli/__init__.py +39 -39
  5. tree_sitter_analyzer/cli/__main__.py +12 -12
  6. tree_sitter_analyzer/cli/commands/__init__.py +26 -26
  7. tree_sitter_analyzer/cli/commands/advanced_command.py +88 -88
  8. tree_sitter_analyzer/cli/commands/base_command.py +182 -180
  9. tree_sitter_analyzer/cli/commands/structure_command.py +138 -138
  10. tree_sitter_analyzer/cli/commands/summary_command.py +101 -101
  11. tree_sitter_analyzer/core/__init__.py +15 -15
  12. tree_sitter_analyzer/core/analysis_engine.py +74 -78
  13. tree_sitter_analyzer/core/cache_service.py +320 -320
  14. tree_sitter_analyzer/core/engine.py +566 -566
  15. tree_sitter_analyzer/core/parser.py +293 -293
  16. tree_sitter_analyzer/encoding_utils.py +459 -459
  17. tree_sitter_analyzer/file_handler.py +210 -210
  18. tree_sitter_analyzer/formatters/__init__.py +1 -1
  19. tree_sitter_analyzer/formatters/base_formatter.py +167 -167
  20. tree_sitter_analyzer/formatters/formatter_factory.py +78 -78
  21. tree_sitter_analyzer/formatters/java_formatter.py +18 -18
  22. tree_sitter_analyzer/formatters/python_formatter.py +19 -19
  23. tree_sitter_analyzer/interfaces/__init__.py +9 -9
  24. tree_sitter_analyzer/interfaces/cli.py +528 -528
  25. tree_sitter_analyzer/interfaces/cli_adapter.py +344 -343
  26. tree_sitter_analyzer/interfaces/mcp_adapter.py +206 -206
  27. tree_sitter_analyzer/language_detector.py +53 -53
  28. tree_sitter_analyzer/languages/__init__.py +10 -10
  29. tree_sitter_analyzer/languages/java_plugin.py +1 -1
  30. tree_sitter_analyzer/languages/javascript_plugin.py +446 -446
  31. tree_sitter_analyzer/languages/python_plugin.py +755 -755
  32. tree_sitter_analyzer/mcp/__init__.py +34 -31
  33. tree_sitter_analyzer/mcp/resources/__init__.py +44 -44
  34. tree_sitter_analyzer/mcp/resources/code_file_resource.py +209 -209
  35. tree_sitter_analyzer/mcp/server.py +623 -436
  36. tree_sitter_analyzer/mcp/tools/__init__.py +30 -30
  37. tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +10 -6
  38. tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +247 -242
  39. tree_sitter_analyzer/mcp/tools/base_tool.py +54 -54
  40. tree_sitter_analyzer/mcp/tools/read_partial_tool.py +310 -308
  41. tree_sitter_analyzer/mcp/tools/table_format_tool.py +386 -379
  42. tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +563 -559
  43. tree_sitter_analyzer/mcp/utils/__init__.py +107 -107
  44. tree_sitter_analyzer/models.py +10 -10
  45. tree_sitter_analyzer/output_manager.py +253 -253
  46. tree_sitter_analyzer/plugins/__init__.py +280 -280
  47. tree_sitter_analyzer/plugins/base.py +529 -529
  48. tree_sitter_analyzer/plugins/manager.py +379 -379
  49. tree_sitter_analyzer/queries/__init__.py +26 -26
  50. tree_sitter_analyzer/queries/java.py +391 -391
  51. tree_sitter_analyzer/queries/javascript.py +148 -148
  52. tree_sitter_analyzer/queries/python.py +285 -285
  53. tree_sitter_analyzer/queries/typescript.py +229 -229
  54. tree_sitter_analyzer/query_loader.py +257 -257
  55. tree_sitter_analyzer/security/boundary_manager.py +237 -279
  56. tree_sitter_analyzer/security/validator.py +60 -58
  57. tree_sitter_analyzer/utils.py +294 -277
  58. {tree_sitter_analyzer-0.8.3.dist-info → tree_sitter_analyzer-0.9.2.dist-info}/METADATA +28 -19
  59. tree_sitter_analyzer-0.9.2.dist-info/RECORD +77 -0
  60. {tree_sitter_analyzer-0.8.3.dist-info → tree_sitter_analyzer-0.9.2.dist-info}/entry_points.txt +1 -0
  61. tree_sitter_analyzer-0.8.3.dist-info/RECORD +0 -77
  62. {tree_sitter_analyzer-0.8.3.dist-info → tree_sitter_analyzer-0.9.2.dist-info}/WHEEL +0 -0
@@ -1,755 +1,755 @@
1
- #!/usr/bin/env python3
2
- """
3
- Python Language Plugin
4
-
5
- Provides Python-specific parsing and element extraction functionality.
6
- Migrated to the new plugin architecture with enhanced query integration.
7
- """
8
-
9
- from typing import TYPE_CHECKING, 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
- from ..models import Class, CodeElement, Function, Import, Variable
18
- from ..plugins.base import ElementExtractor, LanguagePlugin
19
- from ..utils import log_error, log_warning
20
-
21
-
22
- class PythonElementExtractor(ElementExtractor):
23
- """Python-specific element extractor with comprehensive analysis"""
24
-
25
- def __init__(self) -> None:
26
- """Initialize the Python element extractor."""
27
- self.current_module: str = ""
28
- self.current_file: str = ""
29
- self.source_code: str = ""
30
- self.imports: list[str] = []
31
-
32
- def extract_functions(
33
- self, tree: "tree_sitter.Tree", source_code: str
34
- ) -> list[Function]:
35
- """Extract Python function definitions with comprehensive analysis"""
36
- self.source_code = source_code
37
- functions: list[Function] = []
38
-
39
- # Function definition queries
40
- function_queries = [
41
- # Regular function definitions
42
- """
43
- (function_definition
44
- name: (identifier) @function.name
45
- parameters: (parameters) @function.params
46
- body: (block) @function.body) @function.definition
47
- """,
48
- # Async function definitions
49
- """
50
- (function_definition
51
- "async"
52
- name: (identifier) @async_function.name
53
- parameters: (parameters) @async_function.params
54
- body: (block) @async_function.body) @async_function.definition
55
- """,
56
- ]
57
-
58
- try:
59
- language = tree.language if hasattr(tree, "language") else None
60
- if language:
61
- for query_string in function_queries:
62
- query = language.query(query_string)
63
- captures = query.captures(tree.root_node)
64
-
65
- if isinstance(captures, dict):
66
- # Process regular functions
67
- function_nodes = captures.get("function.definition", [])
68
- for node in function_nodes:
69
- function = self._extract_detailed_function_info(
70
- node, source_code, is_async=False
71
- )
72
- if function:
73
- functions.append(function)
74
-
75
- # Process async functions
76
- async_nodes = captures.get("async_function.definition", [])
77
- for node in async_nodes:
78
- function = self._extract_detailed_function_info(
79
- node, source_code, is_async=True
80
- )
81
- if function:
82
- functions.append(function)
83
-
84
- except Exception as e:
85
- log_warning(f"Could not extract Python functions: {e}")
86
-
87
- return functions
88
-
89
- def extract_classes(
90
- self, tree: "tree_sitter.Tree", source_code: str
91
- ) -> list[Class]:
92
- """Extract Python class definitions with comprehensive analysis"""
93
- self.source_code = source_code
94
- classes: list[Class] = []
95
-
96
- # Class definition query
97
- query_string = """
98
- (class_definition
99
- name: (identifier) @class.name
100
- superclasses: (argument_list)? @class.superclasses
101
- body: (block) @class.body) @class.definition
102
- """
103
-
104
- try:
105
- language = tree.language if hasattr(tree, "language") else None
106
- if language:
107
- query = language.query(query_string)
108
- captures = query.captures(tree.root_node)
109
-
110
- if isinstance(captures, dict):
111
- class_nodes = captures.get("class.definition", [])
112
- for node in class_nodes:
113
- cls = self._extract_detailed_class_info(node, source_code)
114
- if cls:
115
- classes.append(cls)
116
-
117
- except Exception as e:
118
- log_warning(f"Could not extract Python classes: {e}")
119
-
120
- return classes
121
-
122
- def extract_variables(
123
- self, tree: "tree_sitter.Tree", source_code: str
124
- ) -> list[Variable]:
125
- """Extract Python variable definitions (class attributes only)"""
126
- variables: list[Variable] = []
127
-
128
- # Only extract class-level attributes, not function-level variables
129
- try:
130
- # Find class declarations
131
- class_query = """
132
- (class_definition
133
- body: (block) @class.body) @class.definition
134
- """
135
-
136
- language = tree.language if hasattr(tree, "language") else None
137
- if language:
138
- query = language.query(class_query)
139
- captures = query.captures(tree.root_node)
140
-
141
- if isinstance(captures, dict):
142
- class_bodies = captures.get("class.body", [])
143
-
144
- # For each class body, extract attribute assignments
145
- for class_body in class_bodies:
146
- variables.extend(
147
- self._extract_class_attributes(class_body, source_code)
148
- )
149
-
150
- except Exception as e:
151
- log_warning(f"Could not extract Python class attributes: {e}")
152
-
153
- return variables
154
-
155
- def _extract_class_attributes(
156
- self, class_body_node: "tree_sitter.Node", source_code: str
157
- ) -> list[Variable]:
158
- """Extract class-level attribute assignments"""
159
- attributes: list[Variable] = []
160
-
161
- try:
162
- # Look for assignments directly under class body
163
- for child in class_body_node.children:
164
- if child.type == "expression_statement":
165
- # Check if it's an assignment
166
- for grandchild in child.children:
167
- if grandchild.type == "assignment":
168
- attribute = self._extract_class_attribute_info(
169
- grandchild, source_code
170
- )
171
- if attribute:
172
- attributes.append(attribute)
173
- elif child.type == "assignment":
174
- attribute = self._extract_class_attribute_info(child, source_code)
175
- if attribute:
176
- attributes.append(attribute)
177
-
178
- except Exception as e:
179
- log_warning(f"Could not extract class attributes: {e}")
180
-
181
- return attributes
182
-
183
- def _extract_class_attribute_info(
184
- self, node: "tree_sitter.Node", source_code: str
185
- ) -> Variable | None:
186
- """Extract class attribute information from assignment node"""
187
- try:
188
- # Get the full assignment text
189
- assignment_text = source_code[node.start_byte : node.end_byte]
190
-
191
- # Extract attribute name and type annotation
192
- if "=" in assignment_text:
193
- left_part = assignment_text.split("=")[0].strip()
194
-
195
- # Handle type annotations (e.g., "name: str = ...")
196
- if ":" in left_part:
197
- name_part, type_part = left_part.split(":", 1)
198
- attr_name = name_part.strip()
199
- attr_type = type_part.strip()
200
- else:
201
- attr_name = left_part
202
- attr_type = None
203
-
204
- return Variable(
205
- name=attr_name,
206
- start_line=node.start_point[0] + 1,
207
- end_line=node.end_point[0] + 1,
208
- raw_text=assignment_text,
209
- language="python",
210
- variable_type=attr_type,
211
- )
212
-
213
- except Exception as e:
214
- log_warning(f"Could not extract class attribute info: {e}")
215
-
216
- return None
217
-
218
- def extract_imports(
219
- self, tree: "tree_sitter.Tree", source_code: str
220
- ) -> list[Import]:
221
- """Extract Python import statements"""
222
- imports: list[Import] = []
223
-
224
- # Import statement queries
225
- import_queries = [
226
- # Regular import statements
227
- """
228
- (import_statement
229
- name: (dotted_name) @import.name) @import.statement
230
- """,
231
- # From import statements
232
- """
233
- (import_from_statement
234
- module_name: (dotted_name) @from_import.module
235
- name: (dotted_name) @from_import.name) @from_import.statement
236
- """,
237
- # Aliased imports
238
- """
239
- (aliased_import
240
- name: (dotted_name) @aliased_import.name
241
- alias: (identifier) @aliased_import.alias) @aliased_import.statement
242
- """,
243
- ]
244
-
245
- try:
246
- language = tree.language if hasattr(tree, "language") else None
247
- if language:
248
- for query_string in import_queries:
249
- query = language.query(query_string)
250
- captures = query.captures(tree.root_node)
251
-
252
- if isinstance(captures, dict):
253
- # Process different types of imports
254
- for key, nodes in captures.items():
255
- if key.endswith("statement"):
256
- import_type = key.split(".")[0]
257
- for node in nodes:
258
- imp = self._extract_import_info(
259
- node, source_code, import_type
260
- )
261
- if imp:
262
- imports.append(imp)
263
-
264
- except Exception as e:
265
- log_warning(f"Could not extract Python imports: {e}")
266
-
267
- return imports
268
-
269
- def _extract_detailed_function_info(
270
- self, node: "tree_sitter.Node", source_code: str, is_async: bool = False
271
- ) -> Function | None:
272
- """Extract comprehensive function information from AST node"""
273
- try:
274
- # Extract basic information
275
- name = self._extract_name_from_node(node, source_code)
276
- if not name:
277
- return None
278
-
279
- # Extract parameters
280
- parameters = self._extract_parameters_from_node(node, source_code)
281
-
282
- # Extract decorators
283
- decorators = self._extract_decorators_from_node(node, source_code)
284
-
285
- # Extract return type hint
286
- return_type = self._extract_return_type_from_node(node, source_code)
287
-
288
- # Extract docstring
289
- # docstring = self._extract_docstring_from_node(node, source_code) # Not used currently
290
-
291
- # Extract function body
292
- # body = self._extract_function_body(node, source_code) # Not used currently
293
-
294
- # Calculate complexity (simplified)
295
- # complexity_score = self._calculate_complexity(body) # Not used currently
296
-
297
- # Determine visibility (Python conventions)
298
- visibility = "public"
299
- if name.startswith("__") and name.endswith("__"):
300
- visibility = "magic" # Magic methods
301
- elif name.startswith("_"):
302
- visibility = "private"
303
-
304
- # Safely extract raw text, avoiding index out of bounds
305
- start_byte = min(node.start_byte, len(source_code))
306
- end_byte = min(node.end_byte, len(source_code))
307
- raw_text = (
308
- source_code[start_byte:end_byte]
309
- if start_byte < end_byte
310
- else source_code
311
- )
312
-
313
- return Function(
314
- name=name,
315
- start_line=node.start_point[0] + 1,
316
- end_line=node.end_point[0] + 1,
317
- raw_text=raw_text,
318
- language="python",
319
- parameters=parameters,
320
- return_type=return_type or "Any",
321
- modifiers=decorators,
322
- is_static="staticmethod" in decorators,
323
- is_private=visibility == "private",
324
- is_public=visibility == "public",
325
- )
326
-
327
- except Exception as e:
328
- log_warning(f"Could not extract detailed function info: {e}")
329
- return None
330
-
331
- def _extract_detailed_class_info(
332
- self, node: "tree_sitter.Node", source_code: str
333
- ) -> Class | None:
334
- """Extract comprehensive class information from AST node"""
335
- try:
336
- # Extract basic information
337
- name = self._extract_name_from_node(node, source_code)
338
- if not name:
339
- return None
340
-
341
- # Extract superclasses
342
- superclasses = self._extract_superclasses_from_node(node, source_code)
343
-
344
- # Extract decorators
345
- decorators = self._extract_decorators_from_node(node, source_code)
346
-
347
- # Extract docstring
348
- # docstring = self._extract_docstring_from_node(node, source_code) # Not used currently
349
-
350
- # Generate fully qualified name
351
- full_qualified_name = (
352
- f"{self.current_module}.{name}" if self.current_module else name
353
- )
354
-
355
- # Determine visibility
356
- # visibility = "public"
357
- # if name.startswith("_"):
358
- # visibility = "private" # Not used currently
359
-
360
- return Class(
361
- name=name,
362
- start_line=node.start_point[0] + 1,
363
- end_line=node.end_point[0] + 1,
364
- raw_text=source_code[node.start_byte : node.end_byte],
365
- language="python",
366
- class_type="class",
367
- full_qualified_name=full_qualified_name,
368
- package_name=self.current_module,
369
- superclass=superclasses[0] if superclasses else None,
370
- interfaces=superclasses[1:] if len(superclasses) > 1 else [],
371
- modifiers=decorators,
372
- )
373
-
374
- except Exception as e:
375
- log_warning(f"Could not extract detailed class info: {e}")
376
- return None
377
-
378
- def _extract_variable_info(
379
- self, node: "tree_sitter.Node", source_code: str, assignment_type: str
380
- ) -> Variable | None:
381
- """Extract detailed variable information from AST node"""
382
- try:
383
- if not self._validate_node(node):
384
- return None
385
-
386
- # Extract variable text
387
- variable_text = source_code[node.start_byte : node.end_byte]
388
-
389
- # Extract variable name (simplified)
390
- if "=" in variable_text:
391
- name_part = variable_text.split("=")[0].strip()
392
- if assignment_type == "multiple_assignment" and "," in name_part:
393
- name = name_part.split(",")[0].strip()
394
- else:
395
- name = name_part
396
- else:
397
- name = "variable"
398
-
399
- return Variable(
400
- name=name,
401
- start_line=node.start_point[0] + 1,
402
- end_line=node.end_point[0] + 1,
403
- raw_text=variable_text,
404
- language="python",
405
- variable_type=assignment_type,
406
- )
407
-
408
- except Exception as e:
409
- log_warning(f"Could not extract variable info: {e}")
410
- return None
411
-
412
- def _extract_import_info(
413
- self, node: "tree_sitter.Node", source_code: str, import_type: str
414
- ) -> Import | None:
415
- """Extract detailed import information from AST node"""
416
- try:
417
- if not self._validate_node(node):
418
- return None
419
-
420
- # Safely extract import text, avoiding index out of bounds
421
- start_byte = min(node.start_byte, len(source_code))
422
- end_byte = min(node.end_byte, len(source_code))
423
- import_text = (
424
- source_code[start_byte:end_byte]
425
- if start_byte < end_byte
426
- else source_code
427
- )
428
-
429
- # Extract import name and module name (simplified)
430
- if import_type == "from_import":
431
- if "from" in import_text and "import" in import_text:
432
- parts = import_text.split("import")
433
- module_name = parts[0].replace("from", "").strip()
434
- import_name = parts[1].strip()
435
- else:
436
- module_name = ""
437
- import_name = import_text
438
- elif import_type == "aliased_import":
439
- module_name = ""
440
- import_name = import_text
441
- else:
442
- module_name = ""
443
- import_name = import_text.replace("import", "").strip()
444
-
445
- return Import(
446
- name=import_name,
447
- start_line=node.start_point[0] + 1,
448
- end_line=node.end_point[0] + 1,
449
- raw_text=import_text,
450
- language="python",
451
- module_name=module_name,
452
- )
453
-
454
- except Exception as e:
455
- log_warning(f"Could not extract import info: {e}")
456
- return None
457
-
458
- # Helper methods
459
- def _validate_node(self, node: "tree_sitter.Node") -> bool:
460
- """Validate that a node has required attributes"""
461
- required_attrs = ["start_byte", "end_byte", "start_point", "end_point"]
462
- for attr in required_attrs:
463
- if not hasattr(node, attr) or getattr(node, attr) is None:
464
- return False
465
- return True
466
-
467
- def _extract_name_from_node(
468
- self, node: "tree_sitter.Node", source_code: str
469
- ) -> str | None:
470
- """Extract name from AST node"""
471
- for child in node.children:
472
- if child.type == "identifier":
473
- return source_code[child.start_byte : child.end_byte]
474
- return None
475
-
476
- def _extract_parameters_from_node(
477
- self, node: "tree_sitter.Node", source_code: str
478
- ) -> list[str]:
479
- """Extract parameters from function node"""
480
- parameters: list[str] = []
481
- for child in node.children:
482
- if child.type == "parameters":
483
- for param_child in child.children:
484
- if param_child.type in [
485
- "identifier",
486
- "typed_parameter",
487
- "default_parameter",
488
- ]:
489
- param_text = source_code[
490
- param_child.start_byte : param_child.end_byte
491
- ]
492
- parameters.append(param_text)
493
- return parameters
494
-
495
- def _extract_decorators_from_node(
496
- self, node: "tree_sitter.Node", source_code: str
497
- ) -> list[str]:
498
- """Extract decorators from node"""
499
- decorators: list[str] = []
500
-
501
- # Decorators are before function/class definitions
502
- if hasattr(node, "parent") and node.parent:
503
- for sibling in node.parent.children:
504
- if (
505
- sibling.type == "decorator"
506
- and sibling.end_point[0] < node.start_point[0]
507
- ):
508
- decorator_text = source_code[sibling.start_byte : sibling.end_byte]
509
- # Remove @
510
- if decorator_text.startswith("@"):
511
- decorator_text = decorator_text[1:].strip()
512
- decorators.append(decorator_text)
513
-
514
- return decorators
515
-
516
- def _extract_return_type_from_node(
517
- self, node: "tree_sitter.Node", source_code: str
518
- ) -> str | None:
519
- """Extract return type annotation from function node"""
520
- for child in node.children:
521
- if child.type == "type":
522
- return source_code[child.start_byte : child.end_byte]
523
- return None
524
-
525
- def _extract_docstring_from_node(
526
- self, node: "tree_sitter.Node", source_code: str
527
- ) -> str | None:
528
- """Extract docstring from function/class node"""
529
- for child in node.children:
530
- if child.type == "block":
531
- # Check if the first statement in the block is a docstring
532
- for stmt in child.children:
533
- if stmt.type == "expression_statement":
534
- for expr in stmt.children:
535
- if expr.type == "string":
536
- if self._validate_node(expr):
537
- docstring = source_code[
538
- expr.start_byte : expr.end_byte
539
- ]
540
- # Remove quotes
541
- if docstring.startswith(
542
- '"""'
543
- ) or docstring.startswith("'''"):
544
- return docstring[3:-3].strip()
545
- elif docstring.startswith(
546
- '"'
547
- ) or docstring.startswith("'"):
548
- return docstring[1:-1].strip()
549
- return docstring
550
- break
551
- break
552
- return None
553
-
554
- def _extract_function_body(self, node: "tree_sitter.Node", source_code: str) -> str:
555
- """Extract function body"""
556
- for child in node.children:
557
- if child.type == "block":
558
- return source_code[child.start_byte : child.end_byte]
559
- return ""
560
-
561
- def _extract_superclasses_from_node(
562
- self, node: "tree_sitter.Node", source_code: str
563
- ) -> list[str]:
564
- """Extract superclasses from class node"""
565
- superclasses: list[str] = []
566
- for child in node.children:
567
- if child.type == "argument_list":
568
- for arg in child.children:
569
- if arg.type == "identifier":
570
- superclasses.append(source_code[arg.start_byte : arg.end_byte])
571
- return superclasses
572
-
573
- def _calculate_complexity(self, body: str) -> int:
574
- """Calculate cyclomatic complexity (simplified)"""
575
- complexity = 1 # Base complexity
576
- keywords = ["if", "elif", "for", "while", "try", "except", "with", "and", "or"]
577
- for keyword in keywords:
578
- complexity += body.count(f" {keyword} ") + body.count(f"\n{keyword} ")
579
- return complexity
580
-
581
-
582
- class PythonPlugin(LanguagePlugin):
583
- """Python language plugin for the new architecture"""
584
-
585
- def __init__(self) -> None:
586
- """Initialize the Python plugin"""
587
- super().__init__()
588
- self._language_cache: tree_sitter.Language | None = None
589
- self._extractor: PythonElementExtractor | None = None
590
-
591
- def get_language_name(self) -> str:
592
- """Return the name of the programming language this plugin supports"""
593
- return "python"
594
-
595
- def get_file_extensions(self) -> list[str]:
596
- """Return list of file extensions this plugin supports"""
597
- return [".py", ".pyw", ".pyi"]
598
-
599
- def create_extractor(self) -> ElementExtractor:
600
- """Create and return an element extractor for this language"""
601
- return PythonElementExtractor()
602
-
603
- def get_extractor(self) -> ElementExtractor:
604
- """Get the cached extractor instance, creating it if necessary"""
605
- if self._extractor is None:
606
- self._extractor = PythonElementExtractor()
607
- return self._extractor
608
-
609
- def get_tree_sitter_language(self) -> Optional["tree_sitter.Language"]:
610
- """Get the Tree-sitter language object for Python"""
611
- if self._language_cache is None:
612
- try:
613
- import tree_sitter
614
- import tree_sitter_python as tspython
615
-
616
- # PyCapsuleオブジェクトをLanguageオブジェクトに変換
617
- language_capsule = tspython.language()
618
- self._language_cache = tree_sitter.Language(language_capsule)
619
- except ImportError:
620
- log_error("tree-sitter-python not available")
621
- return None
622
- except Exception as e:
623
- log_error(f"Failed to load Python language: {e}")
624
- return None
625
- return self._language_cache
626
-
627
- def get_supported_queries(self) -> list[str]:
628
- """Get list of supported query names for this language"""
629
- return ["class", "function", "variable", "import"]
630
-
631
- def is_applicable(self, file_path: str) -> bool:
632
- """Check if this plugin is applicable for the given file"""
633
- return any(
634
- file_path.lower().endswith(ext.lower())
635
- for ext in self.get_file_extensions()
636
- )
637
-
638
- def get_plugin_info(self) -> dict:
639
- """Get information about this plugin"""
640
- return {
641
- "name": "Python Plugin",
642
- "language": self.get_language_name(),
643
- "extensions": self.get_file_extensions(),
644
- "version": "2.0.0",
645
- "supported_queries": self.get_supported_queries(),
646
- }
647
-
648
- async def analyze_file(
649
- self, file_path: str, request: "AnalysisRequest"
650
- ) -> "AnalysisResult":
651
- """
652
- Analyze a Python file and return analysis results.
653
-
654
- Args:
655
- file_path: Path to the Python file to analyze
656
- request: Analysis request object
657
-
658
- Returns:
659
- AnalysisResult object containing the analysis results
660
- """
661
- try:
662
- from ..core.parser import Parser
663
- from ..models import AnalysisResult
664
-
665
- # Read file content
666
- with open(file_path, encoding="utf-8") as f:
667
- source_code = f.read()
668
-
669
- # Parse the file
670
- parser = Parser()
671
- parse_result = parser.parse_code(source_code, "python")
672
-
673
- if not parse_result.success:
674
- return AnalysisResult(
675
- file_path=file_path,
676
- language="python",
677
- line_count=len(source_code.splitlines()),
678
- elements=[],
679
- node_count=0,
680
- query_results={},
681
- source_code=source_code,
682
- success=False,
683
- error_message=parse_result.error_message,
684
- )
685
-
686
- # Extract elements
687
- extractor = self.create_extractor()
688
- if parse_result.tree:
689
- functions = extractor.extract_functions(parse_result.tree, source_code)
690
- classes = extractor.extract_classes(parse_result.tree, source_code)
691
- variables = extractor.extract_variables(parse_result.tree, source_code)
692
- imports = extractor.extract_imports(parse_result.tree, source_code)
693
- else:
694
- functions = []
695
- classes = []
696
- variables = []
697
- imports = []
698
-
699
- # Combine all elements
700
- all_elements: list[CodeElement] = []
701
- all_elements.extend(functions)
702
- all_elements.extend(classes)
703
- all_elements.extend(variables)
704
- all_elements.extend(imports)
705
-
706
- return AnalysisResult(
707
- file_path=file_path,
708
- language="python",
709
- line_count=len(source_code.splitlines()),
710
- elements=all_elements,
711
- node_count=(
712
- parse_result.tree.root_node.child_count if parse_result.tree else 0
713
- ),
714
- query_results={},
715
- source_code=source_code,
716
- success=True,
717
- error_message=None,
718
- )
719
-
720
- except Exception as e:
721
- log_error(f"Failed to analyze Python file {file_path}: {e}")
722
- return AnalysisResult(
723
- file_path=file_path,
724
- language="python",
725
- line_count=0,
726
- elements=[],
727
- node_count=0,
728
- query_results={},
729
- source_code="",
730
- success=False,
731
- error_message=str(e),
732
- )
733
-
734
- def execute_query(self, tree: "tree_sitter.Tree", query_name: str) -> dict:
735
- """Execute a specific query on the tree"""
736
- try:
737
- language = self.get_tree_sitter_language()
738
- if not language:
739
- return {"error": "Language not available"}
740
-
741
- # Simple query execution for testing
742
- if query_name == "function":
743
- query_string = "(function_definition) @function"
744
- elif query_name == "class":
745
- query_string = "(class_definition) @class"
746
- else:
747
- return {"error": f"Unknown query: {query_name}"}
748
-
749
- query = language.query(query_string)
750
- captures = query.captures(tree.root_node)
751
- return {"captures": captures, "query": query_string}
752
-
753
- except Exception as e:
754
- log_error(f"Query execution failed: {e}")
755
- return {"error": str(e)}
1
+ #!/usr/bin/env python3
2
+ """
3
+ Python Language Plugin
4
+
5
+ Provides Python-specific parsing and element extraction functionality.
6
+ Migrated to the new plugin architecture with enhanced query integration.
7
+ """
8
+
9
+ from typing import TYPE_CHECKING, 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
+ from ..models import Class, CodeElement, Function, Import, Variable
18
+ from ..plugins.base import ElementExtractor, LanguagePlugin
19
+ from ..utils import log_error, log_warning
20
+
21
+
22
+ class PythonElementExtractor(ElementExtractor):
23
+ """Python-specific element extractor with comprehensive analysis"""
24
+
25
+ def __init__(self) -> None:
26
+ """Initialize the Python element extractor."""
27
+ self.current_module: str = ""
28
+ self.current_file: str = ""
29
+ self.source_code: str = ""
30
+ self.imports: list[str] = []
31
+
32
+ def extract_functions(
33
+ self, tree: "tree_sitter.Tree", source_code: str
34
+ ) -> list[Function]:
35
+ """Extract Python function definitions with comprehensive analysis"""
36
+ self.source_code = source_code
37
+ functions: list[Function] = []
38
+
39
+ # Function definition queries
40
+ function_queries = [
41
+ # Regular function definitions
42
+ """
43
+ (function_definition
44
+ name: (identifier) @function.name
45
+ parameters: (parameters) @function.params
46
+ body: (block) @function.body) @function.definition
47
+ """,
48
+ # Async function definitions
49
+ """
50
+ (function_definition
51
+ "async"
52
+ name: (identifier) @async_function.name
53
+ parameters: (parameters) @async_function.params
54
+ body: (block) @async_function.body) @async_function.definition
55
+ """,
56
+ ]
57
+
58
+ try:
59
+ language = tree.language if hasattr(tree, "language") else None
60
+ if language:
61
+ for query_string in function_queries:
62
+ query = language.query(query_string)
63
+ captures = query.captures(tree.root_node)
64
+
65
+ if isinstance(captures, dict):
66
+ # Process regular functions
67
+ function_nodes = captures.get("function.definition", [])
68
+ for node in function_nodes:
69
+ function = self._extract_detailed_function_info(
70
+ node, source_code, is_async=False
71
+ )
72
+ if function:
73
+ functions.append(function)
74
+
75
+ # Process async functions
76
+ async_nodes = captures.get("async_function.definition", [])
77
+ for node in async_nodes:
78
+ function = self._extract_detailed_function_info(
79
+ node, source_code, is_async=True
80
+ )
81
+ if function:
82
+ functions.append(function)
83
+
84
+ except Exception as e:
85
+ log_warning(f"Could not extract Python functions: {e}")
86
+
87
+ return functions
88
+
89
+ def extract_classes(
90
+ self, tree: "tree_sitter.Tree", source_code: str
91
+ ) -> list[Class]:
92
+ """Extract Python class definitions with comprehensive analysis"""
93
+ self.source_code = source_code
94
+ classes: list[Class] = []
95
+
96
+ # Class definition query
97
+ query_string = """
98
+ (class_definition
99
+ name: (identifier) @class.name
100
+ superclasses: (argument_list)? @class.superclasses
101
+ body: (block) @class.body) @class.definition
102
+ """
103
+
104
+ try:
105
+ language = tree.language if hasattr(tree, "language") else None
106
+ if language:
107
+ query = language.query(query_string)
108
+ captures = query.captures(tree.root_node)
109
+
110
+ if isinstance(captures, dict):
111
+ class_nodes = captures.get("class.definition", [])
112
+ for node in class_nodes:
113
+ cls = self._extract_detailed_class_info(node, source_code)
114
+ if cls:
115
+ classes.append(cls)
116
+
117
+ except Exception as e:
118
+ log_warning(f"Could not extract Python classes: {e}")
119
+
120
+ return classes
121
+
122
+ def extract_variables(
123
+ self, tree: "tree_sitter.Tree", source_code: str
124
+ ) -> list[Variable]:
125
+ """Extract Python variable definitions (class attributes only)"""
126
+ variables: list[Variable] = []
127
+
128
+ # Only extract class-level attributes, not function-level variables
129
+ try:
130
+ # Find class declarations
131
+ class_query = """
132
+ (class_definition
133
+ body: (block) @class.body) @class.definition
134
+ """
135
+
136
+ language = tree.language if hasattr(tree, "language") else None
137
+ if language:
138
+ query = language.query(class_query)
139
+ captures = query.captures(tree.root_node)
140
+
141
+ if isinstance(captures, dict):
142
+ class_bodies = captures.get("class.body", [])
143
+
144
+ # For each class body, extract attribute assignments
145
+ for class_body in class_bodies:
146
+ variables.extend(
147
+ self._extract_class_attributes(class_body, source_code)
148
+ )
149
+
150
+ except Exception as e:
151
+ log_warning(f"Could not extract Python class attributes: {e}")
152
+
153
+ return variables
154
+
155
+ def _extract_class_attributes(
156
+ self, class_body_node: "tree_sitter.Node", source_code: str
157
+ ) -> list[Variable]:
158
+ """Extract class-level attribute assignments"""
159
+ attributes: list[Variable] = []
160
+
161
+ try:
162
+ # Look for assignments directly under class body
163
+ for child in class_body_node.children:
164
+ if child.type == "expression_statement":
165
+ # Check if it's an assignment
166
+ for grandchild in child.children:
167
+ if grandchild.type == "assignment":
168
+ attribute = self._extract_class_attribute_info(
169
+ grandchild, source_code
170
+ )
171
+ if attribute:
172
+ attributes.append(attribute)
173
+ elif child.type == "assignment":
174
+ attribute = self._extract_class_attribute_info(child, source_code)
175
+ if attribute:
176
+ attributes.append(attribute)
177
+
178
+ except Exception as e:
179
+ log_warning(f"Could not extract class attributes: {e}")
180
+
181
+ return attributes
182
+
183
+ def _extract_class_attribute_info(
184
+ self, node: "tree_sitter.Node", source_code: str
185
+ ) -> Variable | None:
186
+ """Extract class attribute information from assignment node"""
187
+ try:
188
+ # Get the full assignment text
189
+ assignment_text = source_code[node.start_byte : node.end_byte]
190
+
191
+ # Extract attribute name and type annotation
192
+ if "=" in assignment_text:
193
+ left_part = assignment_text.split("=")[0].strip()
194
+
195
+ # Handle type annotations (e.g., "name: str = ...")
196
+ if ":" in left_part:
197
+ name_part, type_part = left_part.split(":", 1)
198
+ attr_name = name_part.strip()
199
+ attr_type = type_part.strip()
200
+ else:
201
+ attr_name = left_part
202
+ attr_type = None
203
+
204
+ return Variable(
205
+ name=attr_name,
206
+ start_line=node.start_point[0] + 1,
207
+ end_line=node.end_point[0] + 1,
208
+ raw_text=assignment_text,
209
+ language="python",
210
+ variable_type=attr_type,
211
+ )
212
+
213
+ except Exception as e:
214
+ log_warning(f"Could not extract class attribute info: {e}")
215
+
216
+ return None
217
+
218
+ def extract_imports(
219
+ self, tree: "tree_sitter.Tree", source_code: str
220
+ ) -> list[Import]:
221
+ """Extract Python import statements"""
222
+ imports: list[Import] = []
223
+
224
+ # Import statement queries
225
+ import_queries = [
226
+ # Regular import statements
227
+ """
228
+ (import_statement
229
+ name: (dotted_name) @import.name) @import.statement
230
+ """,
231
+ # From import statements
232
+ """
233
+ (import_from_statement
234
+ module_name: (dotted_name) @from_import.module
235
+ name: (dotted_name) @from_import.name) @from_import.statement
236
+ """,
237
+ # Aliased imports
238
+ """
239
+ (aliased_import
240
+ name: (dotted_name) @aliased_import.name
241
+ alias: (identifier) @aliased_import.alias) @aliased_import.statement
242
+ """,
243
+ ]
244
+
245
+ try:
246
+ language = tree.language if hasattr(tree, "language") else None
247
+ if language:
248
+ for query_string in import_queries:
249
+ query = language.query(query_string)
250
+ captures = query.captures(tree.root_node)
251
+
252
+ if isinstance(captures, dict):
253
+ # Process different types of imports
254
+ for key, nodes in captures.items():
255
+ if key.endswith("statement"):
256
+ import_type = key.split(".")[0]
257
+ for node in nodes:
258
+ imp = self._extract_import_info(
259
+ node, source_code, import_type
260
+ )
261
+ if imp:
262
+ imports.append(imp)
263
+
264
+ except Exception as e:
265
+ log_warning(f"Could not extract Python imports: {e}")
266
+
267
+ return imports
268
+
269
+ def _extract_detailed_function_info(
270
+ self, node: "tree_sitter.Node", source_code: str, is_async: bool = False
271
+ ) -> Function | None:
272
+ """Extract comprehensive function information from AST node"""
273
+ try:
274
+ # Extract basic information
275
+ name = self._extract_name_from_node(node, source_code)
276
+ if not name:
277
+ return None
278
+
279
+ # Extract parameters
280
+ parameters = self._extract_parameters_from_node(node, source_code)
281
+
282
+ # Extract decorators
283
+ decorators = self._extract_decorators_from_node(node, source_code)
284
+
285
+ # Extract return type hint
286
+ return_type = self._extract_return_type_from_node(node, source_code)
287
+
288
+ # Extract docstring
289
+ # docstring = self._extract_docstring_from_node(node, source_code) # Not used currently
290
+
291
+ # Extract function body
292
+ # body = self._extract_function_body(node, source_code) # Not used currently
293
+
294
+ # Calculate complexity (simplified)
295
+ # complexity_score = self._calculate_complexity(body) # Not used currently
296
+
297
+ # Determine visibility (Python conventions)
298
+ visibility = "public"
299
+ if name.startswith("__") and name.endswith("__"):
300
+ visibility = "magic" # Magic methods
301
+ elif name.startswith("_"):
302
+ visibility = "private"
303
+
304
+ # Safely extract raw text, avoiding index out of bounds
305
+ start_byte = min(node.start_byte, len(source_code))
306
+ end_byte = min(node.end_byte, len(source_code))
307
+ raw_text = (
308
+ source_code[start_byte:end_byte]
309
+ if start_byte < end_byte
310
+ else source_code
311
+ )
312
+
313
+ return Function(
314
+ name=name,
315
+ start_line=node.start_point[0] + 1,
316
+ end_line=node.end_point[0] + 1,
317
+ raw_text=raw_text,
318
+ language="python",
319
+ parameters=parameters,
320
+ return_type=return_type or "Any",
321
+ modifiers=decorators,
322
+ is_static="staticmethod" in decorators,
323
+ is_private=visibility == "private",
324
+ is_public=visibility == "public",
325
+ )
326
+
327
+ except Exception as e:
328
+ log_warning(f"Could not extract detailed function info: {e}")
329
+ return None
330
+
331
+ def _extract_detailed_class_info(
332
+ self, node: "tree_sitter.Node", source_code: str
333
+ ) -> Class | None:
334
+ """Extract comprehensive class information from AST node"""
335
+ try:
336
+ # Extract basic information
337
+ name = self._extract_name_from_node(node, source_code)
338
+ if not name:
339
+ return None
340
+
341
+ # Extract superclasses
342
+ superclasses = self._extract_superclasses_from_node(node, source_code)
343
+
344
+ # Extract decorators
345
+ decorators = self._extract_decorators_from_node(node, source_code)
346
+
347
+ # Extract docstring
348
+ # docstring = self._extract_docstring_from_node(node, source_code) # Not used currently
349
+
350
+ # Generate fully qualified name
351
+ full_qualified_name = (
352
+ f"{self.current_module}.{name}" if self.current_module else name
353
+ )
354
+
355
+ # Determine visibility
356
+ # visibility = "public"
357
+ # if name.startswith("_"):
358
+ # visibility = "private" # Not used currently
359
+
360
+ return Class(
361
+ name=name,
362
+ start_line=node.start_point[0] + 1,
363
+ end_line=node.end_point[0] + 1,
364
+ raw_text=source_code[node.start_byte : node.end_byte],
365
+ language="python",
366
+ class_type="class",
367
+ full_qualified_name=full_qualified_name,
368
+ package_name=self.current_module,
369
+ superclass=superclasses[0] if superclasses else None,
370
+ interfaces=superclasses[1:] if len(superclasses) > 1 else [],
371
+ modifiers=decorators,
372
+ )
373
+
374
+ except Exception as e:
375
+ log_warning(f"Could not extract detailed class info: {e}")
376
+ return None
377
+
378
+ def _extract_variable_info(
379
+ self, node: "tree_sitter.Node", source_code: str, assignment_type: str
380
+ ) -> Variable | None:
381
+ """Extract detailed variable information from AST node"""
382
+ try:
383
+ if not self._validate_node(node):
384
+ return None
385
+
386
+ # Extract variable text
387
+ variable_text = source_code[node.start_byte : node.end_byte]
388
+
389
+ # Extract variable name (simplified)
390
+ if "=" in variable_text:
391
+ name_part = variable_text.split("=")[0].strip()
392
+ if assignment_type == "multiple_assignment" and "," in name_part:
393
+ name = name_part.split(",")[0].strip()
394
+ else:
395
+ name = name_part
396
+ else:
397
+ name = "variable"
398
+
399
+ return Variable(
400
+ name=name,
401
+ start_line=node.start_point[0] + 1,
402
+ end_line=node.end_point[0] + 1,
403
+ raw_text=variable_text,
404
+ language="python",
405
+ variable_type=assignment_type,
406
+ )
407
+
408
+ except Exception as e:
409
+ log_warning(f"Could not extract variable info: {e}")
410
+ return None
411
+
412
+ def _extract_import_info(
413
+ self, node: "tree_sitter.Node", source_code: str, import_type: str
414
+ ) -> Import | None:
415
+ """Extract detailed import information from AST node"""
416
+ try:
417
+ if not self._validate_node(node):
418
+ return None
419
+
420
+ # Safely extract import text, avoiding index out of bounds
421
+ start_byte = min(node.start_byte, len(source_code))
422
+ end_byte = min(node.end_byte, len(source_code))
423
+ import_text = (
424
+ source_code[start_byte:end_byte]
425
+ if start_byte < end_byte
426
+ else source_code
427
+ )
428
+
429
+ # Extract import name and module name (simplified)
430
+ if import_type == "from_import":
431
+ if "from" in import_text and "import" in import_text:
432
+ parts = import_text.split("import")
433
+ module_name = parts[0].replace("from", "").strip()
434
+ import_name = parts[1].strip()
435
+ else:
436
+ module_name = ""
437
+ import_name = import_text
438
+ elif import_type == "aliased_import":
439
+ module_name = ""
440
+ import_name = import_text
441
+ else:
442
+ module_name = ""
443
+ import_name = import_text.replace("import", "").strip()
444
+
445
+ return Import(
446
+ name=import_name,
447
+ start_line=node.start_point[0] + 1,
448
+ end_line=node.end_point[0] + 1,
449
+ raw_text=import_text,
450
+ language="python",
451
+ module_name=module_name,
452
+ )
453
+
454
+ except Exception as e:
455
+ log_warning(f"Could not extract import info: {e}")
456
+ return None
457
+
458
+ # Helper methods
459
+ def _validate_node(self, node: "tree_sitter.Node") -> bool:
460
+ """Validate that a node has required attributes"""
461
+ required_attrs = ["start_byte", "end_byte", "start_point", "end_point"]
462
+ for attr in required_attrs:
463
+ if not hasattr(node, attr) or getattr(node, attr) is None:
464
+ return False
465
+ return True
466
+
467
+ def _extract_name_from_node(
468
+ self, node: "tree_sitter.Node", source_code: str
469
+ ) -> str | None:
470
+ """Extract name from AST node"""
471
+ for child in node.children:
472
+ if child.type == "identifier":
473
+ return source_code[child.start_byte : child.end_byte]
474
+ return None
475
+
476
+ def _extract_parameters_from_node(
477
+ self, node: "tree_sitter.Node", source_code: str
478
+ ) -> list[str]:
479
+ """Extract parameters from function node"""
480
+ parameters: list[str] = []
481
+ for child in node.children:
482
+ if child.type == "parameters":
483
+ for param_child in child.children:
484
+ if param_child.type in [
485
+ "identifier",
486
+ "typed_parameter",
487
+ "default_parameter",
488
+ ]:
489
+ param_text = source_code[
490
+ param_child.start_byte : param_child.end_byte
491
+ ]
492
+ parameters.append(param_text)
493
+ return parameters
494
+
495
+ def _extract_decorators_from_node(
496
+ self, node: "tree_sitter.Node", source_code: str
497
+ ) -> list[str]:
498
+ """Extract decorators from node"""
499
+ decorators: list[str] = []
500
+
501
+ # Decorators are before function/class definitions
502
+ if hasattr(node, "parent") and node.parent:
503
+ for sibling in node.parent.children:
504
+ if (
505
+ sibling.type == "decorator"
506
+ and sibling.end_point[0] < node.start_point[0]
507
+ ):
508
+ decorator_text = source_code[sibling.start_byte : sibling.end_byte]
509
+ # Remove @
510
+ if decorator_text.startswith("@"):
511
+ decorator_text = decorator_text[1:].strip()
512
+ decorators.append(decorator_text)
513
+
514
+ return decorators
515
+
516
+ def _extract_return_type_from_node(
517
+ self, node: "tree_sitter.Node", source_code: str
518
+ ) -> str | None:
519
+ """Extract return type annotation from function node"""
520
+ for child in node.children:
521
+ if child.type == "type":
522
+ return source_code[child.start_byte : child.end_byte]
523
+ return None
524
+
525
+ def _extract_docstring_from_node(
526
+ self, node: "tree_sitter.Node", source_code: str
527
+ ) -> str | None:
528
+ """Extract docstring from function/class node"""
529
+ for child in node.children:
530
+ if child.type == "block":
531
+ # Check if the first statement in the block is a docstring
532
+ for stmt in child.children:
533
+ if stmt.type == "expression_statement":
534
+ for expr in stmt.children:
535
+ if expr.type == "string":
536
+ if self._validate_node(expr):
537
+ docstring = source_code[
538
+ expr.start_byte : expr.end_byte
539
+ ]
540
+ # Remove quotes
541
+ if docstring.startswith(
542
+ '"""'
543
+ ) or docstring.startswith("'''"):
544
+ return docstring[3:-3].strip()
545
+ elif docstring.startswith(
546
+ '"'
547
+ ) or docstring.startswith("'"):
548
+ return docstring[1:-1].strip()
549
+ return docstring
550
+ break
551
+ break
552
+ return None
553
+
554
+ def _extract_function_body(self, node: "tree_sitter.Node", source_code: str) -> str:
555
+ """Extract function body"""
556
+ for child in node.children:
557
+ if child.type == "block":
558
+ return source_code[child.start_byte : child.end_byte]
559
+ return ""
560
+
561
+ def _extract_superclasses_from_node(
562
+ self, node: "tree_sitter.Node", source_code: str
563
+ ) -> list[str]:
564
+ """Extract superclasses from class node"""
565
+ superclasses: list[str] = []
566
+ for child in node.children:
567
+ if child.type == "argument_list":
568
+ for arg in child.children:
569
+ if arg.type == "identifier":
570
+ superclasses.append(source_code[arg.start_byte : arg.end_byte])
571
+ return superclasses
572
+
573
+ def _calculate_complexity(self, body: str) -> int:
574
+ """Calculate cyclomatic complexity (simplified)"""
575
+ complexity = 1 # Base complexity
576
+ keywords = ["if", "elif", "for", "while", "try", "except", "with", "and", "or"]
577
+ for keyword in keywords:
578
+ complexity += body.count(f" {keyword} ") + body.count(f"\n{keyword} ")
579
+ return complexity
580
+
581
+
582
+ class PythonPlugin(LanguagePlugin):
583
+ """Python language plugin for the new architecture"""
584
+
585
+ def __init__(self) -> None:
586
+ """Initialize the Python plugin"""
587
+ super().__init__()
588
+ self._language_cache: tree_sitter.Language | None = None
589
+ self._extractor: PythonElementExtractor | None = None
590
+
591
+ def get_language_name(self) -> str:
592
+ """Return the name of the programming language this plugin supports"""
593
+ return "python"
594
+
595
+ def get_file_extensions(self) -> list[str]:
596
+ """Return list of file extensions this plugin supports"""
597
+ return [".py", ".pyw", ".pyi"]
598
+
599
+ def create_extractor(self) -> ElementExtractor:
600
+ """Create and return an element extractor for this language"""
601
+ return PythonElementExtractor()
602
+
603
+ def get_extractor(self) -> ElementExtractor:
604
+ """Get the cached extractor instance, creating it if necessary"""
605
+ if self._extractor is None:
606
+ self._extractor = PythonElementExtractor()
607
+ return self._extractor
608
+
609
+ def get_tree_sitter_language(self) -> Optional["tree_sitter.Language"]:
610
+ """Get the Tree-sitter language object for Python"""
611
+ if self._language_cache is None:
612
+ try:
613
+ import tree_sitter
614
+ import tree_sitter_python as tspython
615
+
616
+ # PyCapsuleオブジェクトをLanguageオブジェクトに変換
617
+ language_capsule = tspython.language()
618
+ self._language_cache = tree_sitter.Language(language_capsule)
619
+ except ImportError:
620
+ log_error("tree-sitter-python not available")
621
+ return None
622
+ except Exception as e:
623
+ log_error(f"Failed to load Python language: {e}")
624
+ return None
625
+ return self._language_cache
626
+
627
+ def get_supported_queries(self) -> list[str]:
628
+ """Get list of supported query names for this language"""
629
+ return ["class", "function", "variable", "import"]
630
+
631
+ def is_applicable(self, file_path: str) -> bool:
632
+ """Check if this plugin is applicable for the given file"""
633
+ return any(
634
+ file_path.lower().endswith(ext.lower())
635
+ for ext in self.get_file_extensions()
636
+ )
637
+
638
+ def get_plugin_info(self) -> dict:
639
+ """Get information about this plugin"""
640
+ return {
641
+ "name": "Python Plugin",
642
+ "language": self.get_language_name(),
643
+ "extensions": self.get_file_extensions(),
644
+ "version": "2.0.0",
645
+ "supported_queries": self.get_supported_queries(),
646
+ }
647
+
648
+ async def analyze_file(
649
+ self, file_path: str, request: "AnalysisRequest"
650
+ ) -> "AnalysisResult":
651
+ """
652
+ Analyze a Python file and return analysis results.
653
+
654
+ Args:
655
+ file_path: Path to the Python file to analyze
656
+ request: Analysis request object
657
+
658
+ Returns:
659
+ AnalysisResult object containing the analysis results
660
+ """
661
+ try:
662
+ from ..core.parser import Parser
663
+ from ..models import AnalysisResult
664
+
665
+ # Read file content
666
+ with open(file_path, encoding="utf-8") as f:
667
+ source_code = f.read()
668
+
669
+ # Parse the file
670
+ parser = Parser()
671
+ parse_result = parser.parse_code(source_code, "python")
672
+
673
+ if not parse_result.success:
674
+ return AnalysisResult(
675
+ file_path=file_path,
676
+ language="python",
677
+ line_count=len(source_code.splitlines()),
678
+ elements=[],
679
+ node_count=0,
680
+ query_results={},
681
+ source_code=source_code,
682
+ success=False,
683
+ error_message=parse_result.error_message,
684
+ )
685
+
686
+ # Extract elements
687
+ extractor = self.create_extractor()
688
+ if parse_result.tree:
689
+ functions = extractor.extract_functions(parse_result.tree, source_code)
690
+ classes = extractor.extract_classes(parse_result.tree, source_code)
691
+ variables = extractor.extract_variables(parse_result.tree, source_code)
692
+ imports = extractor.extract_imports(parse_result.tree, source_code)
693
+ else:
694
+ functions = []
695
+ classes = []
696
+ variables = []
697
+ imports = []
698
+
699
+ # Combine all elements
700
+ all_elements: list[CodeElement] = []
701
+ all_elements.extend(functions)
702
+ all_elements.extend(classes)
703
+ all_elements.extend(variables)
704
+ all_elements.extend(imports)
705
+
706
+ return AnalysisResult(
707
+ file_path=file_path,
708
+ language="python",
709
+ line_count=len(source_code.splitlines()),
710
+ elements=all_elements,
711
+ node_count=(
712
+ parse_result.tree.root_node.child_count if parse_result.tree else 0
713
+ ),
714
+ query_results={},
715
+ source_code=source_code,
716
+ success=True,
717
+ error_message=None,
718
+ )
719
+
720
+ except Exception as e:
721
+ log_error(f"Failed to analyze Python file {file_path}: {e}")
722
+ return AnalysisResult(
723
+ file_path=file_path,
724
+ language="python",
725
+ line_count=0,
726
+ elements=[],
727
+ node_count=0,
728
+ query_results={},
729
+ source_code="",
730
+ success=False,
731
+ error_message=str(e),
732
+ )
733
+
734
+ def execute_query(self, tree: "tree_sitter.Tree", query_name: str) -> dict:
735
+ """Execute a specific query on the tree"""
736
+ try:
737
+ language = self.get_tree_sitter_language()
738
+ if not language:
739
+ return {"error": "Language not available"}
740
+
741
+ # Simple query execution for testing
742
+ if query_name == "function":
743
+ query_string = "(function_definition) @function"
744
+ elif query_name == "class":
745
+ query_string = "(class_definition) @class"
746
+ else:
747
+ return {"error": f"Unknown query: {query_name}"}
748
+
749
+ query = language.query(query_string)
750
+ captures = query.captures(tree.root_node)
751
+ return {"captures": captures, "query": query_string}
752
+
753
+ except Exception as e:
754
+ log_error(f"Query execution failed: {e}")
755
+ return {"error": str(e)}