tree-sitter-analyzer 0.7.0__py3-none-any.whl → 0.8.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.

Potentially problematic release.


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

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