tree-sitter-analyzer 0.2.0__py3-none-any.whl → 0.4.0__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 (78) hide show
  1. tree_sitter_analyzer/__init__.py +134 -121
  2. tree_sitter_analyzer/__main__.py +11 -12
  3. tree_sitter_analyzer/api.py +533 -539
  4. tree_sitter_analyzer/cli/__init__.py +39 -39
  5. tree_sitter_analyzer/cli/__main__.py +12 -13
  6. tree_sitter_analyzer/cli/commands/__init__.py +26 -27
  7. tree_sitter_analyzer/cli/commands/advanced_command.py +88 -88
  8. tree_sitter_analyzer/cli/commands/base_command.py +160 -155
  9. tree_sitter_analyzer/cli/commands/default_command.py +18 -19
  10. tree_sitter_analyzer/cli/commands/partial_read_command.py +141 -133
  11. tree_sitter_analyzer/cli/commands/query_command.py +81 -82
  12. tree_sitter_analyzer/cli/commands/structure_command.py +138 -121
  13. tree_sitter_analyzer/cli/commands/summary_command.py +101 -93
  14. tree_sitter_analyzer/cli/commands/table_command.py +235 -233
  15. tree_sitter_analyzer/cli/info_commands.py +120 -121
  16. tree_sitter_analyzer/cli_main.py +278 -276
  17. tree_sitter_analyzer/core/__init__.py +15 -20
  18. tree_sitter_analyzer/core/analysis_engine.py +555 -574
  19. tree_sitter_analyzer/core/cache_service.py +320 -330
  20. tree_sitter_analyzer/core/engine.py +559 -560
  21. tree_sitter_analyzer/core/parser.py +293 -288
  22. tree_sitter_analyzer/core/query.py +502 -502
  23. tree_sitter_analyzer/encoding_utils.py +456 -460
  24. tree_sitter_analyzer/exceptions.py +337 -340
  25. tree_sitter_analyzer/file_handler.py +210 -222
  26. tree_sitter_analyzer/formatters/__init__.py +1 -1
  27. tree_sitter_analyzer/formatters/base_formatter.py +167 -168
  28. tree_sitter_analyzer/formatters/formatter_factory.py +78 -74
  29. tree_sitter_analyzer/formatters/java_formatter.py +291 -270
  30. tree_sitter_analyzer/formatters/python_formatter.py +259 -235
  31. tree_sitter_analyzer/interfaces/__init__.py +9 -10
  32. tree_sitter_analyzer/interfaces/cli.py +528 -557
  33. tree_sitter_analyzer/interfaces/cli_adapter.py +343 -319
  34. tree_sitter_analyzer/interfaces/mcp_adapter.py +206 -170
  35. tree_sitter_analyzer/interfaces/mcp_server.py +405 -416
  36. tree_sitter_analyzer/java_analyzer.py +187 -219
  37. tree_sitter_analyzer/language_detector.py +398 -400
  38. tree_sitter_analyzer/language_loader.py +224 -228
  39. tree_sitter_analyzer/languages/__init__.py +10 -11
  40. tree_sitter_analyzer/languages/java_plugin.py +1174 -1113
  41. tree_sitter_analyzer/{plugins → languages}/javascript_plugin.py +446 -439
  42. tree_sitter_analyzer/languages/python_plugin.py +747 -712
  43. tree_sitter_analyzer/mcp/__init__.py +31 -32
  44. tree_sitter_analyzer/mcp/resources/__init__.py +44 -47
  45. tree_sitter_analyzer/mcp/resources/code_file_resource.py +209 -213
  46. tree_sitter_analyzer/mcp/resources/project_stats_resource.py +555 -550
  47. tree_sitter_analyzer/mcp/server.py +333 -345
  48. tree_sitter_analyzer/mcp/tools/__init__.py +30 -31
  49. tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +654 -557
  50. tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +247 -245
  51. tree_sitter_analyzer/mcp/tools/base_tool.py +54 -55
  52. tree_sitter_analyzer/mcp/tools/read_partial_tool.py +300 -302
  53. tree_sitter_analyzer/mcp/tools/table_format_tool.py +362 -359
  54. tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +543 -476
  55. tree_sitter_analyzer/mcp/utils/__init__.py +107 -106
  56. tree_sitter_analyzer/mcp/utils/error_handler.py +549 -549
  57. tree_sitter_analyzer/models.py +470 -481
  58. tree_sitter_analyzer/output_manager.py +255 -264
  59. tree_sitter_analyzer/plugins/__init__.py +280 -334
  60. tree_sitter_analyzer/plugins/base.py +496 -446
  61. tree_sitter_analyzer/plugins/manager.py +379 -355
  62. tree_sitter_analyzer/queries/__init__.py +26 -27
  63. tree_sitter_analyzer/queries/java.py +391 -394
  64. tree_sitter_analyzer/queries/javascript.py +148 -149
  65. tree_sitter_analyzer/queries/python.py +285 -286
  66. tree_sitter_analyzer/queries/typescript.py +229 -230
  67. tree_sitter_analyzer/query_loader.py +257 -260
  68. tree_sitter_analyzer/table_formatter.py +471 -448
  69. tree_sitter_analyzer/utils.py +277 -277
  70. {tree_sitter_analyzer-0.2.0.dist-info → tree_sitter_analyzer-0.4.0.dist-info}/METADATA +23 -8
  71. tree_sitter_analyzer-0.4.0.dist-info/RECORD +73 -0
  72. {tree_sitter_analyzer-0.2.0.dist-info → tree_sitter_analyzer-0.4.0.dist-info}/entry_points.txt +2 -1
  73. tree_sitter_analyzer/plugins/java_plugin.py +0 -625
  74. tree_sitter_analyzer/plugins/plugin_loader.py +0 -83
  75. tree_sitter_analyzer/plugins/python_plugin.py +0 -598
  76. tree_sitter_analyzer/plugins/registry.py +0 -366
  77. tree_sitter_analyzer-0.2.0.dist-info/RECORD +0 -77
  78. {tree_sitter_analyzer-0.2.0.dist-info → tree_sitter_analyzer-0.4.0.dist-info}/WHEEL +0 -0
@@ -1,574 +1,555 @@
1
- #!/usr/bin/env python3
2
- """
3
- 統一解析エンジン - CLI・MCP共通解析システム(修正版)
4
-
5
- このモジュールは、すべての解析処理の中心となる統一エンジンを提供します。
6
- CLI、MCP、その他のインターフェースから共通して使用されます。
7
-
8
- Roo Code規約準拠:
9
- - 型ヒント: 全関数に型ヒント必須
10
- - MCPログ: 各ステップでログ出力
11
- - docstring: Google Style docstring
12
- - パフォーマンス重視: シングルトンパターンとキャッシュ共有
13
- """
14
-
15
- import asyncio
16
- import hashlib
17
- import threading
18
- from dataclasses import dataclass
19
- from typing import Any, Dict, Optional, Protocol
20
- from abc import ABC, abstractmethod
21
-
22
- from ..utils import log_debug, log_info, log_error, log_performance
23
- from .cache_service import CacheService
24
- from ..models import AnalysisResult
25
-
26
-
27
- class UnsupportedLanguageError(Exception):
28
- """サポートされていない言語エラー"""
29
- pass
30
-
31
-
32
- class PluginRegistry(Protocol):
33
- """プラグイン登録管理のプロトコル"""
34
-
35
- def get_plugin(self, language: str) -> Optional['LanguagePlugin']:
36
- """言語プラグインを取得"""
37
- ...
38
-
39
-
40
- class LanguagePlugin(Protocol):
41
- """言語プラグインのプロトコル"""
42
-
43
- async def analyze_file(self, file_path: str, request: 'AnalysisRequest') -> AnalysisResult:
44
- """ファイル解析"""
45
- ...
46
-
47
-
48
- class PerformanceMonitor:
49
- """パフォーマンス監視(簡易版)"""
50
-
51
- def __init__(self) -> None:
52
- self._last_duration: float = 0.0
53
- self._monitoring_active: bool = False
54
- self._operation_stats: Dict[str, Any] = {}
55
- self._total_operations: int = 0
56
-
57
- def measure_operation(self, operation_name: str) -> 'PerformanceContext':
58
- """操作の測定コンテキストを返す"""
59
- return PerformanceContext(operation_name, self)
60
-
61
- def get_last_duration(self) -> float:
62
- """最後の操作時間を取得"""
63
- return self._last_duration
64
-
65
- def _set_duration(self, duration: float) -> None:
66
- """操作時間を設定(内部用)"""
67
- self._last_duration = duration
68
-
69
- def start_monitoring(self) -> None:
70
- """パフォーマンス監視を開始"""
71
- self._monitoring_active = True
72
- log_info("Performance monitoring started")
73
-
74
- def stop_monitoring(self) -> None:
75
- """パフォーマンス監視を停止"""
76
- self._monitoring_active = False
77
- log_info("Performance monitoring stopped")
78
-
79
- def get_operation_stats(self) -> Dict[str, Any]:
80
- """操作統計を取得"""
81
- return self._operation_stats.copy()
82
-
83
- def get_performance_summary(self) -> Dict[str, Any]:
84
- """パフォーマンス要約を取得"""
85
- return {
86
- "total_operations": self._total_operations,
87
- "monitoring_active": self._monitoring_active,
88
- "last_duration": self._last_duration,
89
- "operation_count": len(self._operation_stats)
90
- }
91
-
92
- def record_operation(self, operation_name: str, duration: float) -> None:
93
- """操作を記録"""
94
- if self._monitoring_active:
95
- if operation_name not in self._operation_stats:
96
- self._operation_stats[operation_name] = {
97
- "count": 0,
98
- "total_time": 0.0,
99
- "avg_time": 0.0,
100
- "min_time": float('inf'),
101
- "max_time": 0.0
102
- }
103
-
104
- stats = self._operation_stats[operation_name]
105
- stats["count"] += 1
106
- stats["total_time"] += duration
107
- stats["avg_time"] = stats["total_time"] / stats["count"]
108
- stats["min_time"] = min(stats["min_time"], duration)
109
- stats["max_time"] = max(stats["max_time"], duration)
110
-
111
- self._total_operations += 1
112
-
113
- def clear_metrics(self) -> None:
114
- """メトリクスをクリア"""
115
- self._operation_stats.clear()
116
- self._total_operations = 0
117
- self._last_duration = 0.0
118
- log_info("Performance metrics cleared")
119
-
120
-
121
- class PerformanceContext:
122
- """パフォーマンス測定コンテキスト"""
123
-
124
- def __init__(self, operation_name: str, monitor: PerformanceMonitor) -> None:
125
- self.operation_name = operation_name
126
- self.monitor = monitor
127
- self.start_time: float = 0.0
128
-
129
- def __enter__(self) -> 'PerformanceContext':
130
- import time
131
- self.start_time = time.time()
132
- return self
133
-
134
- def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
135
- import time
136
- duration = time.time() - self.start_time
137
- self.monitor._set_duration(duration)
138
- self.monitor.record_operation(self.operation_name, duration)
139
- log_performance(self.operation_name, duration, f"Operation completed")
140
-
141
-
142
- @dataclass(frozen=True)
143
- class AnalysisRequest:
144
- """
145
- 解析リクエスト
146
-
147
- Attributes:
148
- file_path: 解析対象ファイルパス
149
- language: プログラミング言語(Noneの場合は自動検出)
150
- include_complexity: 複雑度計算を含むか
151
- include_details: 詳細情報を含むか
152
- format_type: 出力フォーマット
153
- """
154
- file_path: str
155
- language: Optional[str] = None
156
- include_complexity: bool = True
157
- include_details: bool = False
158
- format_type: str = "json"
159
-
160
- @classmethod
161
- def from_mcp_arguments(cls, arguments: Dict[str, Any]) -> 'AnalysisRequest':
162
- """
163
- MCP引数から解析リクエストを作成
164
-
165
- Args:
166
- arguments: MCP引数辞書
167
-
168
- Returns:
169
- 解析リクエスト
170
- """
171
- return cls(
172
- file_path=arguments.get("file_path", ""),
173
- language=arguments.get("language"),
174
- include_complexity=arguments.get("include_complexity", True),
175
- include_details=arguments.get("include_details", False),
176
- format_type=arguments.get("format_type", "json")
177
- )
178
-
179
-
180
- class SimplePluginRegistry:
181
- """簡易プラグイン登録管理"""
182
-
183
- def __init__(self) -> None:
184
- self._plugins: Dict[str, LanguagePlugin] = {}
185
-
186
- def register_plugin(self, language: str, plugin: LanguagePlugin) -> None:
187
- """プラグインを登録"""
188
- self._plugins[language] = plugin
189
- log_info(f"Plugin registered for language: {language}")
190
-
191
- def get_plugin(self, language: str) -> Optional[LanguagePlugin]:
192
- """プラグインを取得"""
193
- return self._plugins.get(language)
194
-
195
- def get_supported_languages(self) -> list[str]:
196
- """サポートされている言語一覧を取得"""
197
- return list(self._plugins.keys())
198
-
199
-
200
- class UnifiedAnalysisEngine:
201
- """
202
- 統一解析エンジン(修正版)
203
-
204
- CLI・MCP・その他のインターフェースから共通して使用される
205
- 中央集権的な解析エンジン。シングルトンパターンで実装し、
206
- リソースの効率的な利用とキャッシュの共有を実現。
207
-
208
- 修正点:
209
- - デストラクタでの非同期処理問題を解決
210
- - 明示的なクリーンアップメソッドを提供
211
-
212
- Attributes:
213
- _cache_service: キャッシュサービス
214
- _plugin_registry: プラグイン登録管理
215
- _performance_monitor: パフォーマンス監視
216
- """
217
-
218
- _instance: Optional['UnifiedAnalysisEngine'] = None
219
- _lock: threading.Lock = threading.Lock()
220
-
221
- def __new__(cls) -> 'UnifiedAnalysisEngine':
222
- """シングルトンパターンでインスタンス共有"""
223
- if cls._instance is None:
224
- with cls._lock:
225
- if cls._instance is None:
226
- cls._instance = super().__new__(cls)
227
- return cls._instance
228
-
229
- def __init__(self) -> None:
230
- """初期化(一度のみ実行)"""
231
- if hasattr(self, '_initialized'):
232
- return
233
-
234
- self._cache_service = CacheService()
235
- self._plugin_registry = SimplePluginRegistry()
236
- self._performance_monitor = PerformanceMonitor()
237
-
238
- # プラグインを自動ロード
239
- self._load_plugins()
240
-
241
- self._initialized = True
242
-
243
- log_info("UnifiedAnalysisEngine initialized")
244
-
245
- def _load_plugins(self) -> None:
246
- """利用可能なプラグインを自動ロード"""
247
- log_info("Loading plugins...")
248
-
249
- try:
250
- # Javaプラグインの登録
251
- log_debug("Attempting to load Java plugin...")
252
- from ..languages.java_plugin import JavaPlugin
253
- java_plugin = JavaPlugin()
254
- self._plugin_registry.register_plugin("java", java_plugin)
255
- log_debug("Loaded Java plugin")
256
- except Exception as e:
257
- log_error(f"Failed to load Java plugin: {e}")
258
- import traceback
259
- log_error(f"Java plugin traceback: {traceback.format_exc()}")
260
-
261
- try:
262
- # JavaScriptプラグインの登録
263
- log_debug("Attempting to load JavaScript plugin...")
264
- from ..plugins.javascript_plugin import JavaScriptPlugin
265
- js_plugin = JavaScriptPlugin()
266
- self._plugin_registry.register_plugin("javascript", js_plugin)
267
- log_debug("Loaded JavaScript plugin")
268
- except Exception as e:
269
- log_error(f"Failed to load JavaScript plugin: {e}")
270
- import traceback
271
- log_error(f"JavaScript plugin traceback: {traceback.format_exc()}")
272
-
273
- try:
274
- # Pythonプラグインの登録
275
- log_debug("Attempting to load Python plugin...")
276
- from ..languages.python_plugin import PythonPlugin
277
- python_plugin = PythonPlugin()
278
- self._plugin_registry.register_plugin("python", python_plugin)
279
- log_debug("Loaded Python plugin")
280
- except Exception as e:
281
- log_error(f"Failed to load Python plugin: {e}")
282
- import traceback
283
- log_error(f"Python plugin traceback: {traceback.format_exc()}")
284
-
285
- final_languages = self._plugin_registry.get_supported_languages()
286
- log_info(f"Successfully loaded {len(final_languages)} language plugins: {', '.join(final_languages)}")
287
-
288
- async def analyze(self, request: AnalysisRequest) -> AnalysisResult:
289
- """
290
- 統一解析メソッド
291
-
292
- Args:
293
- request: 解析リクエスト
294
-
295
- Returns:
296
- 解析結果
297
-
298
- Raises:
299
- UnsupportedLanguageError: サポートされていない言語
300
- FileNotFoundError: ファイルが見つからない
301
- """
302
- log_info(f"Starting analysis for {request.file_path}")
303
-
304
- # キャッシュチェック(CLI・MCP間で共有)
305
- cache_key = self._generate_cache_key(request)
306
- cached_result = await self._cache_service.get(cache_key)
307
- if cached_result:
308
- log_info(f"Cache hit for {request.file_path}")
309
- return cached_result
310
-
311
- # 言語検出
312
- language = request.language or self._detect_language(request.file_path)
313
- log_debug(f"Detected language: {language}")
314
-
315
- # デバッグ:登録されているプラグインを確認
316
- supported_languages = self._plugin_registry.get_supported_languages()
317
- log_debug(f"Supported languages: {supported_languages}")
318
- log_debug(f"Looking for plugin for language: {language}")
319
-
320
- # プラグイン取得
321
- plugin = self._plugin_registry.get_plugin(language)
322
- if not plugin:
323
- error_msg = f"Language {language} not supported"
324
- log_error(error_msg)
325
- raise UnsupportedLanguageError(error_msg)
326
-
327
- log_debug(f"Found plugin for {language}: {type(plugin)}")
328
-
329
- # 解析実行(パフォーマンス監視付き)
330
- with self._performance_monitor.measure_operation(f"analyze_{language}"):
331
- log_debug(f"Calling plugin.analyze_file for {request.file_path}")
332
- result = await plugin.analyze_file(request.file_path, request)
333
- log_debug(f"Plugin returned result: success={result.success}, elements={len(result.elements) if result.elements else 0}")
334
-
335
- # 言語情報を確実に設定
336
- if result.language == "unknown" or not result.language:
337
- result.language = language
338
-
339
- # キャッシュ保存
340
- await self._cache_service.set(cache_key, result)
341
-
342
- log_performance(
343
- "unified_analysis",
344
- self._performance_monitor.get_last_duration(),
345
- f"Analyzed {request.file_path} ({language})"
346
- )
347
-
348
- return result
349
-
350
- async def analyze_file(self, file_path: str) -> AnalysisResult:
351
- """
352
- Backward compatibility method for analyze_file.
353
-
354
- Args:
355
- file_path: Path to the file to analyze
356
-
357
- Returns:
358
- Analysis result
359
- """
360
- request = AnalysisRequest(
361
- file_path=file_path,
362
- language=None, # Auto-detect
363
- include_complexity=True,
364
- include_details=True
365
- )
366
- return await self.analyze(request)
367
-
368
- def _generate_cache_key(self, request: AnalysisRequest) -> str:
369
- """
370
- キャッシュキーを生成
371
-
372
- Args:
373
- request: 解析リクエスト
374
-
375
- Returns:
376
- ハッシュ化されたキャッシュキー
377
- """
378
- # 一意なキーを生成するための文字列を構築
379
- key_components = [
380
- request.file_path,
381
- str(request.language),
382
- str(request.include_complexity),
383
- str(request.include_details),
384
- request.format_type
385
- ]
386
-
387
- key_string = ":".join(key_components)
388
-
389
- # SHA256でハッシュ化
390
- return hashlib.sha256(key_string.encode('utf-8')).hexdigest()
391
-
392
- def _detect_language(self, file_path: str) -> str:
393
- """
394
- 言語検出
395
-
396
- Args:
397
- file_path: ファイルパス
398
-
399
- Returns:
400
- 検出された言語
401
- """
402
- # 簡易的な拡張子ベース検出
403
- import os
404
- _, ext = os.path.splitext(file_path)
405
-
406
- language_map = {
407
- '.java': 'java',
408
- '.py': 'python',
409
- '.js': 'javascript',
410
- '.ts': 'typescript',
411
- '.c': 'c',
412
- '.cpp': 'cpp',
413
- '.cc': 'cpp',
414
- '.cxx': 'cpp',
415
- '.rs': 'rust',
416
- '.go': 'go'
417
- }
418
-
419
- detected = language_map.get(ext.lower(), 'unknown')
420
- log_debug(f"Language detection: {file_path} -> {detected}")
421
- return detected
422
-
423
- def clear_cache(self) -> None:
424
- """キャッシュクリア(テスト用)"""
425
- self._cache_service.clear()
426
- log_info("Analysis engine cache cleared")
427
-
428
- def register_plugin(self, language: str, plugin: LanguagePlugin) -> None:
429
- """
430
- プラグインを登録
431
-
432
- Args:
433
- language: 言語名
434
- plugin: 言語プラグイン
435
- """
436
- self._plugin_registry.register_plugin(language, plugin)
437
-
438
- def get_supported_languages(self) -> list[str]:
439
- """
440
- サポートされている言語一覧を取得
441
-
442
- Returns:
443
- サポート言語のリスト
444
- """
445
- return self._plugin_registry.get_supported_languages()
446
-
447
- def get_cache_stats(self) -> Dict[str, Any]:
448
- """
449
- キャッシュ統計を取得
450
-
451
- Returns:
452
- キャッシュ統計情報
453
- """
454
- return self._cache_service.get_stats()
455
-
456
- async def invalidate_cache_pattern(self, pattern: str) -> int:
457
- """
458
- パターンに一致するキャッシュを無効化
459
-
460
- Args:
461
- pattern: 無効化するキーのパターン
462
-
463
- Returns:
464
- 無効化されたキー数
465
- """
466
- return await self._cache_service.invalidate_pattern(pattern)
467
-
468
- def measure_operation(self, operation_name: str) -> 'PerformanceContext':
469
- """
470
- パフォーマンス計測のためのコンテキストマネージャ
471
-
472
- Args:
473
- operation_name: 操作名
474
-
475
- Returns:
476
- パフォーマンス測定コンテキスト
477
- """
478
- return self._performance_monitor.measure_operation(operation_name)
479
-
480
- def start_monitoring(self) -> None:
481
- """パフォーマンス監視を開始"""
482
- self._performance_monitor.start_monitoring()
483
-
484
- def stop_monitoring(self) -> None:
485
- """パフォーマンス監視を停止"""
486
- self._performance_monitor.stop_monitoring()
487
-
488
- def get_operation_stats(self) -> Dict[str, Any]:
489
- """操作統計を取得"""
490
- return self._performance_monitor.get_operation_stats()
491
-
492
- def get_performance_summary(self) -> Dict[str, Any]:
493
- """パフォーマンス要約を取得"""
494
- return self._performance_monitor.get_performance_summary()
495
-
496
- def clear_metrics(self) -> None:
497
- """
498
- 収集したパフォーマンスメトリクスをクリア
499
-
500
- パフォーマンス監視で収集されたメトリクスをリセットします。
501
- テストやデバッグ時に使用されます。
502
- """
503
- # 新しいパフォーマンスモニターインスタンスを作成してリセット
504
- self._performance_monitor = PerformanceMonitor()
505
- log_info("Performance metrics cleared")
506
-
507
- def cleanup(self) -> None:
508
- """
509
- 明示的なリソースクリーンアップ
510
-
511
- テスト終了時などに明示的に呼び出してリソースをクリーンアップします。
512
- デストラクタでの非同期処理問題を避けるため、明示的な呼び出しが必要です。
513
- """
514
- try:
515
- if hasattr(self, '_cache_service'):
516
- self._cache_service.clear()
517
- if hasattr(self, '_performance_monitor'):
518
- self._performance_monitor.clear_metrics()
519
- log_debug("UnifiedAnalysisEngine cleaned up")
520
- except Exception as e:
521
- log_error(f"Error during UnifiedAnalysisEngine cleanup: {e}")
522
-
523
- def __del__(self) -> None:
524
- """
525
- デストラクタ - 非同期コンテキストでの問題を避けるため最小限の処理
526
-
527
- デストラクタでは何もしません。これは非同期コンテキストでの
528
- ガベージコレクション時に発生する問題を避けるためです。
529
- 明示的なクリーンアップはcleanup()メソッドを使用してください。
530
- """
531
- # デストラクタでは何もしない(非同期コンテキストでの問題を避けるため)
532
- pass
533
-
534
-
535
- # 簡易的なプラグイン実装(テスト用)
536
- class MockLanguagePlugin:
537
- """テスト用のモックプラグイン"""
538
-
539
- def __init__(self, language: str) -> None:
540
- self.language = language
541
-
542
- async def analyze_file(self, file_path: str, request: AnalysisRequest) -> AnalysisResult:
543
- """モック解析実装"""
544
- log_info(f"Mock analysis for {file_path} ({self.language})")
545
-
546
- # 簡易的な解析結果を返す
547
- return AnalysisResult(
548
- file_path=file_path,
549
- line_count=10, # 新しいアーキテクチャ用
550
- elements=[], # 新しいアーキテクチャ用
551
- node_count=5, # 新しいアーキテクチャ用
552
- query_results={}, # 新しいアーキテクチャ用
553
- source_code="// Mock source code", # 新しいアーキテクチャ用
554
- language=self.language, # 言語を設定
555
- package=None,
556
- imports=[],
557
- classes=[],
558
- methods=[],
559
- fields=[],
560
- annotations=[],
561
- analysis_time=0.1,
562
- success=True,
563
- error_message=None
564
- )
565
-
566
-
567
- def get_analysis_engine() -> UnifiedAnalysisEngine:
568
- """
569
- 統一解析エンジンのインスタンスを取得
570
-
571
- Returns:
572
- 統一解析エンジンのシングルトンインスタンス
573
- """
574
- return UnifiedAnalysisEngine()
1
+ #!/usr/bin/env python3
2
+ """
3
+ 統一解析エンジン - CLI・MCP共通解析システム(修正版)
4
+
5
+ このモジュールは、すべての解析処理の中心となる統一エンジンを提供します。
6
+ CLI、MCP、その他のインターフェースから共通して使用されます。
7
+
8
+ Roo Code規約準拠:
9
+ - 型ヒント: 全関数に型ヒント必須
10
+ - MCPログ: 各ステップでログ出力
11
+ - docstring: Google Style docstring
12
+ - パフォーマンス重視: シングルトンパターンとキャッシュ共有
13
+ """
14
+
15
+ import hashlib
16
+ import threading
17
+ from dataclasses import dataclass
18
+ from typing import Any, Optional, Protocol
19
+
20
+ from ..models import AnalysisResult
21
+ from ..plugins.base import LanguagePlugin as BaseLanguagePlugin
22
+ from ..plugins.manager import PluginManager
23
+ from ..utils import log_debug, log_error, log_info, log_performance
24
+ from .cache_service import CacheService
25
+
26
+
27
+ class UnsupportedLanguageError(Exception):
28
+ """サポートされていない言語エラー"""
29
+
30
+ pass
31
+
32
+
33
+ class PluginRegistry(Protocol):
34
+ """プラグイン登録管理のプロトコル"""
35
+
36
+ def get_plugin(self, language: str) -> Optional["LanguagePlugin"]:
37
+ """言語プラグインを取得"""
38
+ ...
39
+
40
+
41
+ class LanguagePlugin(Protocol):
42
+ """言語プラグインのプロトコル"""
43
+
44
+ async def analyze_file(
45
+ self, file_path: str, request: "AnalysisRequest"
46
+ ) -> AnalysisResult:
47
+ """ファイル解析"""
48
+ ...
49
+
50
+
51
+ class PerformanceMonitor:
52
+ """パフォーマンス監視(簡易版)"""
53
+
54
+ def __init__(self) -> None:
55
+ self._last_duration: float = 0.0
56
+ self._monitoring_active: bool = False
57
+ self._operation_stats: dict[str, Any] = {}
58
+ self._total_operations: int = 0
59
+
60
+ def measure_operation(self, operation_name: str) -> "PerformanceContext":
61
+ """操作の測定コンテキストを返す"""
62
+ return PerformanceContext(operation_name, self)
63
+
64
+ def get_last_duration(self) -> float:
65
+ """最後の操作時間を取得"""
66
+ return self._last_duration
67
+
68
+ def _set_duration(self, duration: float) -> None:
69
+ """操作時間を設定(内部用)"""
70
+ self._last_duration = duration
71
+
72
+ def start_monitoring(self) -> None:
73
+ """パフォーマンス監視を開始"""
74
+ self._monitoring_active = True
75
+ log_info("Performance monitoring started")
76
+
77
+ def stop_monitoring(self) -> None:
78
+ """パフォーマンス監視を停止"""
79
+ self._monitoring_active = False
80
+ log_info("Performance monitoring stopped")
81
+
82
+ def get_operation_stats(self) -> dict[str, Any]:
83
+ """操作統計を取得"""
84
+ return self._operation_stats.copy()
85
+
86
+ def get_performance_summary(self) -> dict[str, Any]:
87
+ """パフォーマンス要約を取得"""
88
+ return {
89
+ "total_operations": self._total_operations,
90
+ "monitoring_active": self._monitoring_active,
91
+ "last_duration": self._last_duration,
92
+ "operation_count": len(self._operation_stats),
93
+ }
94
+
95
+ def record_operation(self, operation_name: str, duration: float) -> None:
96
+ """操作を記録"""
97
+ if self._monitoring_active:
98
+ if operation_name not in self._operation_stats:
99
+ self._operation_stats[operation_name] = {
100
+ "count": 0,
101
+ "total_time": 0.0,
102
+ "avg_time": 0.0,
103
+ "min_time": float("inf"),
104
+ "max_time": 0.0,
105
+ }
106
+
107
+ stats = self._operation_stats[operation_name]
108
+ stats["count"] += 1
109
+ stats["total_time"] += duration
110
+ stats["avg_time"] = stats["total_time"] / stats["count"]
111
+ stats["min_time"] = min(stats["min_time"], duration)
112
+ stats["max_time"] = max(stats["max_time"], duration)
113
+
114
+ self._total_operations += 1
115
+
116
+ def clear_metrics(self) -> None:
117
+ """メトリクスをクリア"""
118
+ self._operation_stats.clear()
119
+ self._total_operations = 0
120
+ self._last_duration = 0.0
121
+ log_info("Performance metrics cleared")
122
+
123
+
124
+ class PerformanceContext:
125
+ """パフォーマンス測定コンテキスト"""
126
+
127
+ def __init__(self, operation_name: str, monitor: PerformanceMonitor) -> None:
128
+ self.operation_name = operation_name
129
+ self.monitor = monitor
130
+ self.start_time: float = 0.0
131
+
132
+ def __enter__(self) -> "PerformanceContext":
133
+ import time
134
+
135
+ self.start_time = time.time()
136
+ return self
137
+
138
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
139
+ import time
140
+
141
+ duration = time.time() - self.start_time
142
+ self.monitor._set_duration(duration)
143
+ self.monitor.record_operation(self.operation_name, duration)
144
+ log_performance(self.operation_name, duration, "Operation completed")
145
+
146
+
147
+ @dataclass(frozen=True)
148
+ class AnalysisRequest:
149
+ """
150
+ 解析リクエスト
151
+
152
+ Attributes:
153
+ file_path: 解析対象ファイルパス
154
+ language: プログラミング言語(Noneの場合は自動検出)
155
+ include_complexity: 複雑度計算を含むか
156
+ include_details: 詳細情報を含むか
157
+ format_type: 出力フォーマット
158
+ """
159
+
160
+ file_path: str
161
+ language: str | None = None
162
+ include_complexity: bool = True
163
+ include_details: bool = False
164
+ format_type: str = "json"
165
+
166
+ @classmethod
167
+ def from_mcp_arguments(cls, arguments: dict[str, Any]) -> "AnalysisRequest":
168
+ """
169
+ MCP引数から解析リクエストを作成
170
+
171
+ Args:
172
+ arguments: MCP引数辞書
173
+
174
+ Returns:
175
+ 解析リクエスト
176
+ """
177
+ return cls(
178
+ file_path=arguments.get("file_path", ""),
179
+ language=arguments.get("language"),
180
+ include_complexity=arguments.get("include_complexity", True),
181
+ include_details=arguments.get("include_details", False),
182
+ format_type=arguments.get("format_type", "json"),
183
+ )
184
+
185
+
186
+ # SimplePluginRegistry removed - now using PluginManager
187
+
188
+
189
+ class UnifiedAnalysisEngine:
190
+ """
191
+ 統一解析エンジン(修正版)
192
+
193
+ CLI・MCP・その他のインターフェースから共通して使用される
194
+ 中央集権的な解析エンジン。シングルトンパターンで実装し、
195
+ リソースの効率的な利用とキャッシュの共有を実現。
196
+
197
+ 修正点:
198
+ - デストラクタでの非同期処理問題を解決
199
+ - 明示的なクリーンアップメソッドを提供
200
+
201
+ Attributes:
202
+ _cache_service: キャッシュサービス
203
+ _plugin_manager: プラグイン管理
204
+ _performance_monitor: パフォーマンス監視
205
+ """
206
+
207
+ _instance: Optional["UnifiedAnalysisEngine"] = None
208
+ _lock: threading.Lock = threading.Lock()
209
+
210
+ def __new__(cls) -> "UnifiedAnalysisEngine":
211
+ """シングルトンパターンでインスタンス共有"""
212
+ if cls._instance is None:
213
+ with cls._lock:
214
+ if cls._instance is None:
215
+ cls._instance = super().__new__(cls)
216
+ return cls._instance
217
+
218
+ def __init__(self) -> None:
219
+ """初期化(一度のみ実行)"""
220
+ if hasattr(self, "_initialized"):
221
+ return
222
+
223
+ self._cache_service = CacheService()
224
+ self._plugin_manager = PluginManager()
225
+ self._performance_monitor = PerformanceMonitor()
226
+
227
+ # プラグインを自動ロード
228
+ self._load_plugins()
229
+
230
+ self._initialized = True
231
+
232
+ log_info("UnifiedAnalysisEngine initialized")
233
+
234
+ def _load_plugins(self) -> None:
235
+ """利用可能なプラグインを自動ロード"""
236
+ log_info("Loading plugins using PluginManager...")
237
+
238
+ try:
239
+ # PluginManagerの自動ロード機能を使用
240
+ loaded_plugins = self._plugin_manager.load_plugins()
241
+
242
+ final_languages = [plugin.get_language_name() for plugin in loaded_plugins]
243
+ log_info(
244
+ f"Successfully loaded {len(final_languages)} language plugins: {', '.join(final_languages)}"
245
+ )
246
+ except Exception as e:
247
+ log_error(f"Failed to load plugins: {e}")
248
+ import traceback
249
+
250
+ log_error(f"Plugin loading traceback: {traceback.format_exc()}")
251
+
252
+ async def analyze(self, request: AnalysisRequest) -> AnalysisResult:
253
+ """
254
+ 統一解析メソッド
255
+
256
+ Args:
257
+ request: 解析リクエスト
258
+
259
+ Returns:
260
+ 解析結果
261
+
262
+ Raises:
263
+ UnsupportedLanguageError: サポートされていない言語
264
+ FileNotFoundError: ファイルが見つからない
265
+ """
266
+ log_info(f"Starting analysis for {request.file_path}")
267
+
268
+ # キャッシュチェック(CLI・MCP間で共有)
269
+ cache_key = self._generate_cache_key(request)
270
+ cached_result = await self._cache_service.get(cache_key)
271
+ if cached_result:
272
+ log_info(f"Cache hit for {request.file_path}")
273
+ return cached_result # type: ignore
274
+
275
+ # 言語検出
276
+ language = request.language or self._detect_language(request.file_path)
277
+ log_debug(f"Detected language: {language}")
278
+
279
+ # デバッグ:登録されているプラグインを確認
280
+ supported_languages = self._plugin_manager.get_supported_languages()
281
+ log_debug(f"Supported languages: {supported_languages}")
282
+ log_debug(f"Looking for plugin for language: {language}")
283
+
284
+ # プラグイン取得
285
+ plugin = self._plugin_manager.get_plugin(language)
286
+ if not plugin:
287
+ error_msg = f"Language {language} not supported"
288
+ log_error(error_msg)
289
+ raise UnsupportedLanguageError(error_msg)
290
+
291
+ log_debug(f"Found plugin for {language}: {type(plugin)}")
292
+
293
+ # 解析実行(パフォーマンス監視付き)
294
+ with self._performance_monitor.measure_operation(f"analyze_{language}"):
295
+ log_debug(f"Calling plugin.analyze_file for {request.file_path}")
296
+ result = await plugin.analyze_file(request.file_path, request)
297
+ log_debug(
298
+ f"Plugin returned result: success={result.success}, elements={len(result.elements) if result.elements else 0}"
299
+ )
300
+
301
+ # 言語情報を確実に設定
302
+ if result.language == "unknown" or not result.language:
303
+ result.language = language
304
+
305
+ # キャッシュ保存
306
+ await self._cache_service.set(cache_key, result)
307
+
308
+ log_performance(
309
+ "unified_analysis",
310
+ self._performance_monitor.get_last_duration(),
311
+ f"Analyzed {request.file_path} ({language})",
312
+ )
313
+
314
+ return result
315
+
316
+ async def analyze_file(self, file_path: str) -> AnalysisResult:
317
+ """
318
+ Backward compatibility method for analyze_file.
319
+
320
+ Args:
321
+ file_path: Path to the file to analyze
322
+
323
+ Returns:
324
+ Analysis result
325
+ """
326
+ request = AnalysisRequest(
327
+ file_path=file_path,
328
+ language=None, # Auto-detect
329
+ include_complexity=True,
330
+ include_details=True,
331
+ )
332
+ return await self.analyze(request)
333
+
334
+ def _generate_cache_key(self, request: AnalysisRequest) -> str:
335
+ """
336
+ キャッシュキーを生成
337
+
338
+ Args:
339
+ request: 解析リクエスト
340
+
341
+ Returns:
342
+ ハッシュ化されたキャッシュキー
343
+ """
344
+ # 一意なキーを生成するための文字列を構築
345
+ key_components = [
346
+ request.file_path,
347
+ str(request.language),
348
+ str(request.include_complexity),
349
+ str(request.include_details),
350
+ request.format_type,
351
+ ]
352
+
353
+ key_string = ":".join(key_components)
354
+
355
+ # SHA256でハッシュ化
356
+ return hashlib.sha256(key_string.encode("utf-8")).hexdigest()
357
+
358
+ def _detect_language(self, file_path: str) -> str:
359
+ """
360
+ 言語検出
361
+
362
+ Args:
363
+ file_path: ファイルパス
364
+
365
+ Returns:
366
+ 検出された言語
367
+ """
368
+ # 簡易的な拡張子ベース検出
369
+ import os
370
+
371
+ _, ext = os.path.splitext(file_path)
372
+
373
+ language_map = {
374
+ ".java": "java",
375
+ ".py": "python",
376
+ ".js": "javascript",
377
+ ".ts": "typescript",
378
+ ".c": "c",
379
+ ".cpp": "cpp",
380
+ ".cc": "cpp",
381
+ ".cxx": "cpp",
382
+ ".rs": "rust",
383
+ ".go": "go",
384
+ }
385
+
386
+ detected = language_map.get(ext.lower(), "unknown")
387
+ log_debug(f"Language detection: {file_path} -> {detected}")
388
+ return detected
389
+
390
+ def clear_cache(self) -> None:
391
+ """キャッシュクリア(テスト用)"""
392
+ self._cache_service.clear()
393
+ log_info("Analysis engine cache cleared")
394
+
395
+ def register_plugin(self, language: str, plugin: BaseLanguagePlugin) -> None:
396
+ """
397
+ プラグインを登録
398
+
399
+ Args:
400
+ language: 言語名(互換性のため保持、実際は使用されない)
401
+ plugin: 言語プラグイン
402
+ """
403
+ self._plugin_manager.register_plugin(plugin)
404
+
405
+ def get_supported_languages(self) -> list[str]:
406
+ """
407
+ サポートされている言語一覧を取得
408
+
409
+ Returns:
410
+ サポート言語のリスト
411
+ """
412
+ return self._plugin_manager.get_supported_languages()
413
+
414
+ def get_cache_stats(self) -> dict[str, Any]:
415
+ """
416
+ キャッシュ統計を取得
417
+
418
+ Returns:
419
+ キャッシュ統計情報
420
+ """
421
+ return self._cache_service.get_stats()
422
+
423
+ async def invalidate_cache_pattern(self, pattern: str) -> int:
424
+ """
425
+ パターンに一致するキャッシュを無効化
426
+
427
+ Args:
428
+ pattern: 無効化するキーのパターン
429
+
430
+ Returns:
431
+ 無効化されたキー数
432
+ """
433
+ return await self._cache_service.invalidate_pattern(pattern)
434
+
435
+ def measure_operation(self, operation_name: str) -> "PerformanceContext":
436
+ """
437
+ パフォーマンス計測のためのコンテキストマネージャ
438
+
439
+ Args:
440
+ operation_name: 操作名
441
+
442
+ Returns:
443
+ パフォーマンス測定コンテキスト
444
+ """
445
+ return self._performance_monitor.measure_operation(operation_name)
446
+
447
+ def start_monitoring(self) -> None:
448
+ """パフォーマンス監視を開始"""
449
+ self._performance_monitor.start_monitoring()
450
+
451
+ def stop_monitoring(self) -> None:
452
+ """パフォーマンス監視を停止"""
453
+ self._performance_monitor.stop_monitoring()
454
+
455
+ def get_operation_stats(self) -> dict[str, Any]:
456
+ """操作統計を取得"""
457
+ return self._performance_monitor.get_operation_stats()
458
+
459
+ def get_performance_summary(self) -> dict[str, Any]:
460
+ """パフォーマンス要約を取得"""
461
+ return self._performance_monitor.get_performance_summary()
462
+
463
+ def clear_metrics(self) -> None:
464
+ """
465
+ 収集したパフォーマンスメトリクスをクリア
466
+
467
+ パフォーマンス監視で収集されたメトリクスをリセットします。
468
+ テストやデバッグ時に使用されます。
469
+ """
470
+ # 新しいパフォーマンスモニターインスタンスを作成してリセット
471
+ self._performance_monitor = PerformanceMonitor()
472
+ log_info("Performance metrics cleared")
473
+
474
+ def cleanup(self) -> None:
475
+ """
476
+ 明示的なリソースクリーンアップ
477
+
478
+ テスト終了時などに明示的に呼び出してリソースをクリーンアップします。
479
+ デストラクタでの非同期処理問題を避けるため、明示的な呼び出しが必要です。
480
+ """
481
+ try:
482
+ if hasattr(self, "_cache_service"):
483
+ self._cache_service.clear()
484
+ if hasattr(self, "_performance_monitor"):
485
+ self._performance_monitor.clear_metrics()
486
+ log_debug("UnifiedAnalysisEngine cleaned up")
487
+ except Exception as e:
488
+ log_error(f"Error during UnifiedAnalysisEngine cleanup: {e}")
489
+
490
+ def __del__(self) -> None:
491
+ """
492
+ デストラクタ - 非同期コンテキストでの問題を避けるため最小限の処理
493
+
494
+ デストラクタでは何もしません。これは非同期コンテキストでの
495
+ ガベージコレクション時に発生する問題を避けるためです。
496
+ 明示的なクリーンアップはcleanup()メソッドを使用してください。
497
+ """
498
+ # デストラクタでは何もしない(非同期コンテキストでの問題を避けるため)
499
+ pass
500
+
501
+
502
+ # 簡易的なプラグイン実装(テスト用)
503
+ class MockLanguagePlugin:
504
+ """テスト用のモックプラグイン"""
505
+
506
+ def __init__(self, language: str) -> None:
507
+ self.language = language
508
+
509
+ def get_language_name(self) -> str:
510
+ """言語名を取得"""
511
+ return self.language
512
+
513
+ def get_file_extensions(self) -> list[str]:
514
+ """ファイル拡張子を取得"""
515
+ return [f".{self.language}"]
516
+
517
+ def create_extractor(self) -> None:
518
+ """エクストラクタを作成(モック)"""
519
+ return None
520
+
521
+ async def analyze_file(
522
+ self, file_path: str, request: AnalysisRequest
523
+ ) -> AnalysisResult:
524
+ """モック解析実装"""
525
+ log_info(f"Mock analysis for {file_path} ({self.language})")
526
+
527
+ # 簡易的な解析結果を返す
528
+ return AnalysisResult(
529
+ file_path=file_path,
530
+ line_count=10, # 新しいアーキテクチャ用
531
+ elements=[], # 新しいアーキテクチャ用
532
+ node_count=5, # 新しいアーキテクチャ用
533
+ query_results={}, # 新しいアーキテクチャ用
534
+ source_code="// Mock source code", # 新しいアーキテクチャ用
535
+ language=self.language, # 言語を設定
536
+ package=None,
537
+ imports=[],
538
+ classes=[],
539
+ methods=[],
540
+ fields=[],
541
+ annotations=[],
542
+ analysis_time=0.1,
543
+ success=True,
544
+ error_message=None,
545
+ )
546
+
547
+
548
+ def get_analysis_engine() -> UnifiedAnalysisEngine:
549
+ """
550
+ 統一解析エンジンのインスタンスを取得
551
+
552
+ Returns:
553
+ 統一解析エンジンのシングルトンインスタンス
554
+ """
555
+ return UnifiedAnalysisEngine()