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,559 +1,563 @@
1
- #!/usr/bin/env python3
2
- """
3
- Universal Code Analysis Tool for MCP
4
-
5
- This tool provides universal code analysis capabilities for multiple programming
6
- languages using the existing language detection and analysis infrastructure.
7
- """
8
-
9
- import logging
10
- from pathlib import Path
11
- from typing import Any
12
-
13
- from ...core.analysis_engine import AnalysisRequest, get_analysis_engine
14
- from ...language_detector import detect_language_from_file, is_language_supported
15
- from ...security import SecurityValidator
16
- from ..utils import get_performance_monitor
17
- from ..utils.error_handler import handle_mcp_errors
18
-
19
- logger = logging.getLogger(__name__)
20
-
21
-
22
- class UniversalAnalyzeTool:
23
- """
24
- Universal code analysis tool for multiple programming languages
25
-
26
- This tool automatically detects the programming language and applies
27
- the appropriate analyzer to provide comprehensive code analysis.
28
- """
29
-
30
- def __init__(self, project_root: str = None) -> None:
31
- """Initialize the universal analysis tool"""
32
- # Use unified analysis engine instead of deprecated AdvancedAnalyzer
33
- self.project_root = project_root
34
- self.analysis_engine = get_analysis_engine(project_root)
35
- self.security_validator = SecurityValidator(project_root)
36
- logger.info("UniversalAnalyzeTool initialized with security validation")
37
-
38
- def get_tool_definition(self) -> dict[str, Any]:
39
- """
40
- Get MCP tool definition for universal code analysis
41
-
42
- Returns:
43
- Tool definition dictionary
44
- """
45
- return {
46
- "name": "analyze_code_universal",
47
- "description": "Universal code analysis for multiple programming languages with automatic language detection",
48
- "inputSchema": {
49
- "type": "object",
50
- "properties": {
51
- "file_path": {
52
- "type": "string",
53
- "description": "Path to the code file to analyze",
54
- },
55
- "language": {
56
- "type": "string",
57
- "description": "Programming language (optional, auto-detected if not specified)",
58
- },
59
- "analysis_type": {
60
- "type": "string",
61
- "enum": ["basic", "detailed", "structure", "metrics"],
62
- "description": "Type of analysis to perform",
63
- "default": "basic",
64
- },
65
- "include_ast": {
66
- "type": "boolean",
67
- "description": "Include AST information in the analysis",
68
- "default": False,
69
- },
70
- "include_queries": {
71
- "type": "boolean",
72
- "description": "Include available query information",
73
- "default": False,
74
- },
75
- },
76
- "required": ["file_path"],
77
- "additionalProperties": False,
78
- },
79
- }
80
-
81
- @handle_mcp_errors("universal_analyze")
82
- async def execute(self, arguments: dict[str, Any]) -> dict[str, Any]:
83
- """
84
- Execute universal code analysis
85
-
86
- Args:
87
- arguments: Tool arguments containing file_path and optional parameters
88
-
89
- Returns:
90
- Dictionary containing analysis results
91
-
92
- Raises:
93
- ValueError: If required arguments are missing or invalid
94
- FileNotFoundError: If the specified file doesn't exist
95
- """
96
- # Validate required arguments
97
- if "file_path" not in arguments:
98
- raise ValueError("file_path is required")
99
-
100
- file_path = arguments["file_path"]
101
- language = arguments.get("language")
102
- analysis_type = arguments.get("analysis_type", "basic")
103
-
104
- # Security validation
105
- is_valid, error_msg = self.security_validator.validate_file_path(file_path)
106
- if not is_valid:
107
- logger.warning(f"Security validation failed for file path: {file_path} - {error_msg}")
108
- raise ValueError(f"Invalid file path: {error_msg}")
109
-
110
- # Sanitize inputs
111
- if language:
112
- language = self.security_validator.sanitize_input(language, max_length=50)
113
- if analysis_type:
114
- analysis_type = self.security_validator.sanitize_input(analysis_type, max_length=50)
115
- include_ast = arguments.get("include_ast", False)
116
- include_queries = arguments.get("include_queries", False)
117
-
118
- # Validate file exists
119
- if not Path(file_path).exists():
120
- raise FileNotFoundError(f"File not found: {file_path}")
121
-
122
- # Detect language if not specified
123
- if not language:
124
- language = detect_language_from_file(file_path)
125
- if language == "unknown":
126
- raise ValueError(f"Could not detect language for file: {file_path}")
127
-
128
- # Check if language is supported
129
- if not is_language_supported(language):
130
- raise ValueError(f"Language '{language}' is not supported by tree-sitter")
131
-
132
- # Validate analysis_type
133
- valid_analysis_types = ["basic", "detailed", "structure", "metrics"]
134
- if analysis_type not in valid_analysis_types:
135
- raise ValueError(
136
- f"Invalid analysis_type '{analysis_type}'. Valid types: {', '.join(valid_analysis_types)}"
137
- )
138
-
139
- logger.info(
140
- f"Analyzing {file_path} (language: {language}, type: {analysis_type})"
141
- )
142
-
143
- try:
144
- monitor = get_performance_monitor()
145
- with monitor.measure_operation("universal_analyze"):
146
- # Get appropriate analyzer
147
- if language == "java":
148
- # Use advanced analyzer for Java
149
- result = await self._analyze_with_advanced_analyzer(
150
- file_path, language, analysis_type, include_ast
151
- )
152
- else:
153
- # Use universal analyzer for other languages
154
- result = await self._analyze_with_universal_analyzer(
155
- file_path, language, analysis_type, include_ast
156
- )
157
-
158
- # Add query information if requested
159
- if include_queries:
160
- result["available_queries"] = await self._get_available_queries(
161
- language
162
- )
163
-
164
- logger.info(f"Successfully analyzed {file_path}")
165
- return result
166
-
167
- except Exception as e:
168
- logger.error(f"Error analyzing {file_path}: {e}")
169
- raise
170
-
171
- async def _analyze_with_advanced_analyzer(
172
- self, file_path: str, language: str, analysis_type: str, include_ast: bool
173
- ) -> dict[str, Any]:
174
- """
175
- Analyze using the advanced analyzer (Java-specific)
176
-
177
- Args:
178
- file_path: Path to the file to analyze
179
- language: Programming language
180
- analysis_type: Type of analysis to perform
181
- include_ast: Whether to include AST information
182
-
183
- Returns:
184
- Analysis results dictionary
185
- """
186
- # Use unified analysis engine instead of deprecated advanced_analyzer
187
- request = AnalysisRequest(
188
- file_path=file_path,
189
- language=language,
190
- include_complexity=True,
191
- include_details=True,
192
- )
193
- analysis_result = await self.analysis_engine.analyze(request)
194
-
195
- if analysis_result is None:
196
- raise RuntimeError(f"Failed to analyze file: {file_path}")
197
-
198
- # Build base result
199
- result: dict[str, Any] = {
200
- "file_path": file_path,
201
- "language": language,
202
- "analyzer_type": "advanced",
203
- "analysis_type": analysis_type,
204
- }
205
-
206
- if analysis_type == "basic":
207
- result.update(self._extract_basic_metrics(analysis_result))
208
- elif analysis_type == "detailed":
209
- result.update(self._extract_detailed_metrics(analysis_result))
210
- elif analysis_type == "structure":
211
- result.update(self._extract_structure_info(analysis_result))
212
- elif analysis_type == "metrics":
213
- result.update(self._extract_comprehensive_metrics(analysis_result))
214
-
215
- if include_ast:
216
- result["ast_info"] = {
217
- "node_count": getattr(
218
- analysis_result, "line_count", 0
219
- ), # Approximation
220
- "depth": 0, # Advanced analyzer doesn't provide this, use 0 instead of string
221
- }
222
-
223
- return result
224
-
225
- async def _analyze_with_universal_analyzer(
226
- self, file_path: str, language: str, analysis_type: str, include_ast: bool
227
- ) -> dict[str, Any]:
228
- """
229
- Analyze using the universal analyzer
230
-
231
- Args:
232
- file_path: Path to the file to analyze
233
- language: Programming language
234
- analysis_type: Type of analysis to perform
235
- include_ast: Whether to include AST information
236
-
237
- Returns:
238
- Analysis results dictionary
239
- """
240
- request = AnalysisRequest(
241
- file_path=file_path,
242
- language=language,
243
- include_details=(analysis_type == "detailed"),
244
- )
245
- analysis_result = await self.analysis_engine.analyze(request)
246
-
247
- if not analysis_result or not analysis_result.success:
248
- error_message = (
249
- analysis_result.error_message if analysis_result else "Unknown error"
250
- )
251
- raise RuntimeError(f"Failed to analyze file: {file_path} - {error_message}")
252
-
253
- # Convert AnalysisResult to dictionary for consistent processing
254
- analysis_dict = analysis_result.to_dict()
255
-
256
- # Build base result
257
- result: dict[str, Any] = {
258
- "file_path": file_path,
259
- "language": language,
260
- "analyzer_type": "universal",
261
- "analysis_type": analysis_type,
262
- }
263
-
264
- if analysis_type == "basic":
265
- result.update(self._extract_universal_basic_metrics(analysis_dict))
266
- elif analysis_type == "detailed":
267
- result.update(self._extract_universal_detailed_metrics(analysis_dict))
268
- elif analysis_type == "structure":
269
- result.update(self._extract_universal_structure_info(analysis_dict))
270
- elif analysis_type == "metrics":
271
- result.update(self._extract_universal_comprehensive_metrics(analysis_dict))
272
-
273
- if include_ast:
274
- result["ast_info"] = analysis_dict.get("ast_info", {})
275
-
276
- return result
277
-
278
- def _extract_basic_metrics(self, analysis_result: Any) -> dict[str, Any]:
279
- """Extract basic metrics from advanced analyzer result"""
280
- stats = analysis_result.get_statistics()
281
-
282
- return {
283
- "metrics": {
284
- "lines_total": analysis_result.line_count,
285
- "lines_code": stats.get("lines_of_code", 0),
286
- "lines_comment": stats.get("comment_lines", 0),
287
- "lines_blank": stats.get("blank_lines", 0),
288
- "elements": {
289
- "classes": len(
290
- [
291
- e
292
- for e in analysis_result.elements
293
- if e.__class__.__name__ == "Class"
294
- ]
295
- ),
296
- "methods": len(
297
- [
298
- e
299
- for e in analysis_result.elements
300
- if e.__class__.__name__ == "Function"
301
- ]
302
- ),
303
- "fields": len(
304
- [
305
- e
306
- for e in analysis_result.elements
307
- if e.__class__.__name__ == "Variable"
308
- ]
309
- ),
310
- "imports": len(
311
- [
312
- e
313
- for e in analysis_result.elements
314
- if e.__class__.__name__ == "Import"
315
- ]
316
- ),
317
- "annotations": len(getattr(analysis_result, "annotations", [])),
318
- },
319
- }
320
- }
321
-
322
- def _extract_detailed_metrics(self, analysis_result: Any) -> dict[str, Any]:
323
- """Extract detailed metrics from advanced analyzer result"""
324
- basic = self._extract_basic_metrics(analysis_result)
325
-
326
- # Add complexity metrics
327
- methods = [
328
- e for e in analysis_result.elements if e.__class__.__name__ == "Function"
329
- ]
330
- total_complexity = sum(
331
- getattr(method, "complexity_score", 0) or 0 for method in methods
332
- )
333
-
334
- basic["metrics"]["complexity"] = {
335
- "total": total_complexity,
336
- "average": (total_complexity / len(methods) if methods else 0),
337
- "max": max(
338
- (getattr(method, "complexity_score", 0) or 0 for method in methods),
339
- default=0,
340
- ),
341
- }
342
-
343
- return basic
344
-
345
- def _extract_structure_info(self, analysis_result: Any) -> dict[str, Any]:
346
- """Extract structure information from advanced analyzer result"""
347
- return {
348
- "structure": {
349
- "package": (
350
- analysis_result.package.name if analysis_result.package else None
351
- ),
352
- "classes": [
353
- (
354
- cls.to_summary_item()
355
- if hasattr(cls, "to_summary_item")
356
- else {"name": getattr(cls, "name", "unknown")}
357
- )
358
- for cls in [
359
- e
360
- for e in analysis_result.elements
361
- if e.__class__.__name__ == "Class"
362
- ]
363
- ],
364
- "methods": [
365
- (
366
- method.to_summary_item()
367
- if hasattr(method, "to_summary_item")
368
- else {"name": getattr(method, "name", "unknown")}
369
- )
370
- for method in [
371
- e
372
- for e in analysis_result.elements
373
- if e.__class__.__name__ == "Function"
374
- ]
375
- ],
376
- "fields": [
377
- (
378
- field.to_summary_item()
379
- if hasattr(field, "to_summary_item")
380
- else {"name": getattr(field, "name", "unknown")}
381
- )
382
- for field in [
383
- e
384
- for e in analysis_result.elements
385
- if e.__class__.__name__ == "Variable"
386
- ]
387
- ],
388
- "imports": [
389
- (
390
- imp.to_summary_item()
391
- if hasattr(imp, "to_summary_item")
392
- else {"name": getattr(imp, "name", "unknown")}
393
- )
394
- for imp in [
395
- e
396
- for e in analysis_result.elements
397
- if e.__class__.__name__ == "Import"
398
- ]
399
- ],
400
- "annotations": [
401
- (
402
- ann.to_summary_item()
403
- if hasattr(ann, "to_summary_item")
404
- else {"name": getattr(ann, "name", "unknown")}
405
- )
406
- for ann in getattr(analysis_result, "annotations", [])
407
- ],
408
- }
409
- }
410
-
411
- def _extract_comprehensive_metrics(self, analysis_result: Any) -> dict[str, Any]:
412
- """Extract comprehensive metrics from advanced analyzer result"""
413
- detailed = self._extract_detailed_metrics(analysis_result)
414
- structure = self._extract_structure_info(analysis_result)
415
-
416
- # Combine both
417
- result = detailed.copy()
418
- result.update(structure)
419
-
420
- return result
421
-
422
- def _extract_universal_basic_metrics(
423
- self, analysis_result: dict[str, Any]
424
- ) -> dict[str, Any]:
425
- """Extract basic metrics from universal analyzer result"""
426
- elements = analysis_result.get("elements", [])
427
- return {
428
- "metrics": {
429
- "lines_total": analysis_result.get("line_count", 0),
430
- "lines_code": analysis_result.get("line_count", 0), # Approximation
431
- "lines_comment": 0, # Not available in universal analyzer
432
- "lines_blank": 0, # Not available in universal analyzer
433
- "elements": {
434
- "classes": len(
435
- [e for e in elements if e.get("__class__", "") == "Class"]
436
- ),
437
- "methods": len(
438
- [e for e in elements if e.get("__class__", "") == "Function"]
439
- ),
440
- "fields": len(
441
- [e for e in elements if e.get("__class__", "") == "Variable"]
442
- ),
443
- "imports": len(
444
- [e for e in elements if e.get("__class__", "") == "Import"]
445
- ),
446
- "annotations": 0, # Not available in universal analyzer
447
- },
448
- }
449
- }
450
-
451
- def _extract_universal_detailed_metrics(
452
- self, analysis_result: dict[str, Any]
453
- ) -> dict[str, Any]:
454
- """Extract detailed metrics from universal analyzer result"""
455
- basic = self._extract_universal_basic_metrics(analysis_result)
456
-
457
- # Add query results if available
458
- if "query_results" in analysis_result:
459
- basic["query_results"] = analysis_result["query_results"]
460
-
461
- return basic
462
-
463
- def _extract_universal_structure_info(
464
- self, analysis_result: dict[str, Any]
465
- ) -> dict[str, Any]:
466
- """Extract structure information from universal analyzer result"""
467
- return {
468
- "structure": analysis_result.get("structure", {}),
469
- "queries_executed": analysis_result.get("queries_executed", []),
470
- }
471
-
472
- def _extract_universal_comprehensive_metrics(
473
- self, analysis_result: dict[str, Any]
474
- ) -> dict[str, Any]:
475
- """Extract comprehensive metrics from universal analyzer result"""
476
- detailed = self._extract_universal_detailed_metrics(analysis_result)
477
- structure = self._extract_universal_structure_info(analysis_result)
478
-
479
- # Combine both
480
- result = detailed.copy()
481
- result.update(structure)
482
-
483
- return result
484
-
485
- async def _get_available_queries(self, language: str) -> dict[str, Any]:
486
- """
487
- Get available queries for the specified language
488
-
489
- Args:
490
- language: Programming language
491
-
492
- Returns:
493
- Dictionary containing available queries information
494
- """
495
- try:
496
- if language == "java":
497
- # For Java, we don't have predefined queries in the advanced analyzer
498
- return {
499
- "language": language,
500
- "queries": [],
501
- "note": "Advanced analyzer uses built-in analysis logic",
502
- }
503
- else:
504
- # For other languages, get from universal analyzer
505
- queries = self.analysis_engine.get_supported_languages()
506
- return {"language": language, "queries": queries, "count": len(queries)}
507
- except Exception as e:
508
- logger.warning(f"Failed to get queries for {language}: {e}")
509
- return {"language": language, "queries": [], "error": str(e)}
510
-
511
- def validate_arguments(self, arguments: dict[str, Any]) -> bool:
512
- """
513
- Validate tool arguments against the schema.
514
-
515
- Args:
516
- arguments: Arguments to validate
517
-
518
- Returns:
519
- True if arguments are valid
520
-
521
- Raises:
522
- ValueError: If arguments are invalid
523
- """
524
- # Check required fields
525
- if "file_path" not in arguments:
526
- raise ValueError("Required field 'file_path' is missing")
527
-
528
- # Validate file_path
529
- file_path = arguments["file_path"]
530
- if not isinstance(file_path, str):
531
- raise ValueError("file_path must be a string")
532
- if not file_path.strip():
533
- raise ValueError("file_path cannot be empty")
534
-
535
- # Validate optional fields
536
- if "language" in arguments:
537
- language = arguments["language"]
538
- if not isinstance(language, str):
539
- raise ValueError("language must be a string")
540
-
541
- if "analysis_type" in arguments:
542
- analysis_type = arguments["analysis_type"]
543
- if not isinstance(analysis_type, str):
544
- raise ValueError("analysis_type must be a string")
545
- valid_types = ["basic", "detailed", "structure", "metrics"]
546
- if analysis_type not in valid_types:
547
- raise ValueError(f"analysis_type must be one of {valid_types}")
548
-
549
- if "include_ast" in arguments:
550
- include_ast = arguments["include_ast"]
551
- if not isinstance(include_ast, bool):
552
- raise ValueError("include_ast must be a boolean")
553
-
554
- if "include_queries" in arguments:
555
- include_queries = arguments["include_queries"]
556
- if not isinstance(include_queries, bool):
557
- raise ValueError("include_queries must be a boolean")
558
-
559
- return True
1
+ #!/usr/bin/env python3
2
+ """
3
+ Universal Code Analysis Tool for MCP
4
+
5
+ This tool provides universal code analysis capabilities for multiple programming
6
+ languages using the existing language detection and analysis infrastructure.
7
+ """
8
+
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from ...core.analysis_engine import AnalysisRequest, get_analysis_engine
14
+ from ...language_detector import detect_language_from_file, is_language_supported
15
+ from ...security import SecurityValidator
16
+ from ..utils import get_performance_monitor
17
+ from ..utils.error_handler import handle_mcp_errors
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class UniversalAnalyzeTool:
23
+ """
24
+ Universal code analysis tool for multiple programming languages
25
+
26
+ This tool automatically detects the programming language and applies
27
+ the appropriate analyzer to provide comprehensive code analysis.
28
+ """
29
+
30
+ def __init__(self, project_root: str = None) -> None:
31
+ """Initialize the universal analysis tool"""
32
+ # Use unified analysis engine instead of deprecated AdvancedAnalyzer
33
+ self.project_root = project_root
34
+ self.analysis_engine = get_analysis_engine(project_root)
35
+ self.security_validator = SecurityValidator(project_root)
36
+ logger.info("UniversalAnalyzeTool initialized with security validation")
37
+
38
+ def get_tool_definition(self) -> dict[str, Any]:
39
+ """
40
+ Get MCP tool definition for universal code analysis
41
+
42
+ Returns:
43
+ Tool definition dictionary
44
+ """
45
+ return {
46
+ "name": "analyze_code_universal",
47
+ "description": "Universal code analysis for multiple programming languages with automatic language detection",
48
+ "inputSchema": {
49
+ "type": "object",
50
+ "properties": {
51
+ "file_path": {
52
+ "type": "string",
53
+ "description": "Path to the code file to analyze",
54
+ },
55
+ "language": {
56
+ "type": "string",
57
+ "description": "Programming language (optional, auto-detected if not specified)",
58
+ },
59
+ "analysis_type": {
60
+ "type": "string",
61
+ "enum": ["basic", "detailed", "structure", "metrics"],
62
+ "description": "Type of analysis to perform",
63
+ "default": "basic",
64
+ },
65
+ "include_ast": {
66
+ "type": "boolean",
67
+ "description": "Include AST information in the analysis",
68
+ "default": False,
69
+ },
70
+ "include_queries": {
71
+ "type": "boolean",
72
+ "description": "Include available query information",
73
+ "default": False,
74
+ },
75
+ },
76
+ "required": ["file_path"],
77
+ "additionalProperties": False,
78
+ },
79
+ }
80
+
81
+ @handle_mcp_errors("universal_analyze")
82
+ async def execute(self, arguments: dict[str, Any]) -> dict[str, Any]:
83
+ """
84
+ Execute universal code analysis
85
+
86
+ Args:
87
+ arguments: Tool arguments containing file_path and optional parameters
88
+
89
+ Returns:
90
+ Dictionary containing analysis results
91
+
92
+ Raises:
93
+ ValueError: If required arguments are missing or invalid
94
+ FileNotFoundError: If the specified file doesn't exist
95
+ """
96
+ # Validate required arguments
97
+ if "file_path" not in arguments:
98
+ raise ValueError("file_path is required")
99
+
100
+ file_path = arguments["file_path"]
101
+ language = arguments.get("language")
102
+ analysis_type = arguments.get("analysis_type", "basic")
103
+
104
+ # Security validation
105
+ is_valid, error_msg = self.security_validator.validate_file_path(file_path)
106
+ if not is_valid:
107
+ logger.warning(
108
+ f"Security validation failed for file path: {file_path} - {error_msg}"
109
+ )
110
+ raise ValueError(f"Invalid file path: {error_msg}")
111
+
112
+ # Sanitize inputs
113
+ if language:
114
+ language = self.security_validator.sanitize_input(language, max_length=50)
115
+ if analysis_type:
116
+ analysis_type = self.security_validator.sanitize_input(
117
+ analysis_type, max_length=50
118
+ )
119
+ include_ast = arguments.get("include_ast", False)
120
+ include_queries = arguments.get("include_queries", False)
121
+
122
+ # Validate file exists
123
+ if not Path(file_path).exists():
124
+ raise ValueError("Invalid file path: file does not exist")
125
+
126
+ # Detect language if not specified
127
+ if not language:
128
+ language = detect_language_from_file(file_path)
129
+ if language == "unknown":
130
+ raise ValueError(f"Could not detect language for file: {file_path}")
131
+
132
+ # Check if language is supported
133
+ if not is_language_supported(language):
134
+ raise ValueError(f"Language '{language}' is not supported by tree-sitter")
135
+
136
+ # Validate analysis_type
137
+ valid_analysis_types = ["basic", "detailed", "structure", "metrics"]
138
+ if analysis_type not in valid_analysis_types:
139
+ raise ValueError(
140
+ f"Invalid analysis_type '{analysis_type}'. Valid types: {', '.join(valid_analysis_types)}"
141
+ )
142
+
143
+ logger.info(
144
+ f"Analyzing {file_path} (language: {language}, type: {analysis_type})"
145
+ )
146
+
147
+ try:
148
+ monitor = get_performance_monitor()
149
+ with monitor.measure_operation("universal_analyze"):
150
+ # Get appropriate analyzer
151
+ if language == "java":
152
+ # Use advanced analyzer for Java
153
+ result = await self._analyze_with_advanced_analyzer(
154
+ file_path, language, analysis_type, include_ast
155
+ )
156
+ else:
157
+ # Use universal analyzer for other languages
158
+ result = await self._analyze_with_universal_analyzer(
159
+ file_path, language, analysis_type, include_ast
160
+ )
161
+
162
+ # Add query information if requested
163
+ if include_queries:
164
+ result["available_queries"] = await self._get_available_queries(
165
+ language
166
+ )
167
+
168
+ logger.info(f"Successfully analyzed {file_path}")
169
+ return result
170
+
171
+ except Exception as e:
172
+ logger.error(f"Error analyzing {file_path}: {e}")
173
+ raise
174
+
175
+ async def _analyze_with_advanced_analyzer(
176
+ self, file_path: str, language: str, analysis_type: str, include_ast: bool
177
+ ) -> dict[str, Any]:
178
+ """
179
+ Analyze using the advanced analyzer (Java-specific)
180
+
181
+ Args:
182
+ file_path: Path to the file to analyze
183
+ language: Programming language
184
+ analysis_type: Type of analysis to perform
185
+ include_ast: Whether to include AST information
186
+
187
+ Returns:
188
+ Analysis results dictionary
189
+ """
190
+ # Use unified analysis engine instead of deprecated advanced_analyzer
191
+ request = AnalysisRequest(
192
+ file_path=file_path,
193
+ language=language,
194
+ include_complexity=True,
195
+ include_details=True,
196
+ )
197
+ analysis_result = await self.analysis_engine.analyze(request)
198
+
199
+ if analysis_result is None:
200
+ raise RuntimeError(f"Failed to analyze file: {file_path}")
201
+
202
+ # Build base result
203
+ result: dict[str, Any] = {
204
+ "file_path": file_path,
205
+ "language": language,
206
+ "analyzer_type": "advanced",
207
+ "analysis_type": analysis_type,
208
+ }
209
+
210
+ if analysis_type == "basic":
211
+ result.update(self._extract_basic_metrics(analysis_result))
212
+ elif analysis_type == "detailed":
213
+ result.update(self._extract_detailed_metrics(analysis_result))
214
+ elif analysis_type == "structure":
215
+ result.update(self._extract_structure_info(analysis_result))
216
+ elif analysis_type == "metrics":
217
+ result.update(self._extract_comprehensive_metrics(analysis_result))
218
+
219
+ if include_ast:
220
+ result["ast_info"] = {
221
+ "node_count": getattr(
222
+ analysis_result, "line_count", 0
223
+ ), # Approximation
224
+ "depth": 0, # Advanced analyzer doesn't provide this, use 0 instead of string
225
+ }
226
+
227
+ return result
228
+
229
+ async def _analyze_with_universal_analyzer(
230
+ self, file_path: str, language: str, analysis_type: str, include_ast: bool
231
+ ) -> dict[str, Any]:
232
+ """
233
+ Analyze using the universal analyzer
234
+
235
+ Args:
236
+ file_path: Path to the file to analyze
237
+ language: Programming language
238
+ analysis_type: Type of analysis to perform
239
+ include_ast: Whether to include AST information
240
+
241
+ Returns:
242
+ Analysis results dictionary
243
+ """
244
+ request = AnalysisRequest(
245
+ file_path=file_path,
246
+ language=language,
247
+ include_details=(analysis_type == "detailed"),
248
+ )
249
+ analysis_result = await self.analysis_engine.analyze(request)
250
+
251
+ if not analysis_result or not analysis_result.success:
252
+ error_message = (
253
+ analysis_result.error_message if analysis_result else "Unknown error"
254
+ )
255
+ raise RuntimeError(f"Failed to analyze file: {file_path} - {error_message}")
256
+
257
+ # Convert AnalysisResult to dictionary for consistent processing
258
+ analysis_dict = analysis_result.to_dict()
259
+
260
+ # Build base result
261
+ result: dict[str, Any] = {
262
+ "file_path": file_path,
263
+ "language": language,
264
+ "analyzer_type": "universal",
265
+ "analysis_type": analysis_type,
266
+ }
267
+
268
+ if analysis_type == "basic":
269
+ result.update(self._extract_universal_basic_metrics(analysis_dict))
270
+ elif analysis_type == "detailed":
271
+ result.update(self._extract_universal_detailed_metrics(analysis_dict))
272
+ elif analysis_type == "structure":
273
+ result.update(self._extract_universal_structure_info(analysis_dict))
274
+ elif analysis_type == "metrics":
275
+ result.update(self._extract_universal_comprehensive_metrics(analysis_dict))
276
+
277
+ if include_ast:
278
+ result["ast_info"] = analysis_dict.get("ast_info", {})
279
+
280
+ return result
281
+
282
+ def _extract_basic_metrics(self, analysis_result: Any) -> dict[str, Any]:
283
+ """Extract basic metrics from advanced analyzer result"""
284
+ stats = analysis_result.get_statistics()
285
+
286
+ return {
287
+ "metrics": {
288
+ "lines_total": analysis_result.line_count,
289
+ "lines_code": stats.get("lines_of_code", 0),
290
+ "lines_comment": stats.get("comment_lines", 0),
291
+ "lines_blank": stats.get("blank_lines", 0),
292
+ "elements": {
293
+ "classes": len(
294
+ [
295
+ e
296
+ for e in analysis_result.elements
297
+ if e.__class__.__name__ == "Class"
298
+ ]
299
+ ),
300
+ "methods": len(
301
+ [
302
+ e
303
+ for e in analysis_result.elements
304
+ if e.__class__.__name__ == "Function"
305
+ ]
306
+ ),
307
+ "fields": len(
308
+ [
309
+ e
310
+ for e in analysis_result.elements
311
+ if e.__class__.__name__ == "Variable"
312
+ ]
313
+ ),
314
+ "imports": len(
315
+ [
316
+ e
317
+ for e in analysis_result.elements
318
+ if e.__class__.__name__ == "Import"
319
+ ]
320
+ ),
321
+ "annotations": len(getattr(analysis_result, "annotations", [])),
322
+ },
323
+ }
324
+ }
325
+
326
+ def _extract_detailed_metrics(self, analysis_result: Any) -> dict[str, Any]:
327
+ """Extract detailed metrics from advanced analyzer result"""
328
+ basic = self._extract_basic_metrics(analysis_result)
329
+
330
+ # Add complexity metrics
331
+ methods = [
332
+ e for e in analysis_result.elements if e.__class__.__name__ == "Function"
333
+ ]
334
+ total_complexity = sum(
335
+ getattr(method, "complexity_score", 0) or 0 for method in methods
336
+ )
337
+
338
+ basic["metrics"]["complexity"] = {
339
+ "total": total_complexity,
340
+ "average": (total_complexity / len(methods) if methods else 0),
341
+ "max": max(
342
+ (getattr(method, "complexity_score", 0) or 0 for method in methods),
343
+ default=0,
344
+ ),
345
+ }
346
+
347
+ return basic
348
+
349
+ def _extract_structure_info(self, analysis_result: Any) -> dict[str, Any]:
350
+ """Extract structure information from advanced analyzer result"""
351
+ return {
352
+ "structure": {
353
+ "package": (
354
+ analysis_result.package.name if analysis_result.package else None
355
+ ),
356
+ "classes": [
357
+ (
358
+ cls.to_summary_item()
359
+ if hasattr(cls, "to_summary_item")
360
+ else {"name": getattr(cls, "name", "unknown")}
361
+ )
362
+ for cls in [
363
+ e
364
+ for e in analysis_result.elements
365
+ if e.__class__.__name__ == "Class"
366
+ ]
367
+ ],
368
+ "methods": [
369
+ (
370
+ method.to_summary_item()
371
+ if hasattr(method, "to_summary_item")
372
+ else {"name": getattr(method, "name", "unknown")}
373
+ )
374
+ for method in [
375
+ e
376
+ for e in analysis_result.elements
377
+ if e.__class__.__name__ == "Function"
378
+ ]
379
+ ],
380
+ "fields": [
381
+ (
382
+ field.to_summary_item()
383
+ if hasattr(field, "to_summary_item")
384
+ else {"name": getattr(field, "name", "unknown")}
385
+ )
386
+ for field in [
387
+ e
388
+ for e in analysis_result.elements
389
+ if e.__class__.__name__ == "Variable"
390
+ ]
391
+ ],
392
+ "imports": [
393
+ (
394
+ imp.to_summary_item()
395
+ if hasattr(imp, "to_summary_item")
396
+ else {"name": getattr(imp, "name", "unknown")}
397
+ )
398
+ for imp in [
399
+ e
400
+ for e in analysis_result.elements
401
+ if e.__class__.__name__ == "Import"
402
+ ]
403
+ ],
404
+ "annotations": [
405
+ (
406
+ ann.to_summary_item()
407
+ if hasattr(ann, "to_summary_item")
408
+ else {"name": getattr(ann, "name", "unknown")}
409
+ )
410
+ for ann in getattr(analysis_result, "annotations", [])
411
+ ],
412
+ }
413
+ }
414
+
415
+ def _extract_comprehensive_metrics(self, analysis_result: Any) -> dict[str, Any]:
416
+ """Extract comprehensive metrics from advanced analyzer result"""
417
+ detailed = self._extract_detailed_metrics(analysis_result)
418
+ structure = self._extract_structure_info(analysis_result)
419
+
420
+ # Combine both
421
+ result = detailed.copy()
422
+ result.update(structure)
423
+
424
+ return result
425
+
426
+ def _extract_universal_basic_metrics(
427
+ self, analysis_result: dict[str, Any]
428
+ ) -> dict[str, Any]:
429
+ """Extract basic metrics from universal analyzer result"""
430
+ elements = analysis_result.get("elements", [])
431
+ return {
432
+ "metrics": {
433
+ "lines_total": analysis_result.get("line_count", 0),
434
+ "lines_code": analysis_result.get("line_count", 0), # Approximation
435
+ "lines_comment": 0, # Not available in universal analyzer
436
+ "lines_blank": 0, # Not available in universal analyzer
437
+ "elements": {
438
+ "classes": len(
439
+ [e for e in elements if e.get("__class__", "") == "Class"]
440
+ ),
441
+ "methods": len(
442
+ [e for e in elements if e.get("__class__", "") == "Function"]
443
+ ),
444
+ "fields": len(
445
+ [e for e in elements if e.get("__class__", "") == "Variable"]
446
+ ),
447
+ "imports": len(
448
+ [e for e in elements if e.get("__class__", "") == "Import"]
449
+ ),
450
+ "annotations": 0, # Not available in universal analyzer
451
+ },
452
+ }
453
+ }
454
+
455
+ def _extract_universal_detailed_metrics(
456
+ self, analysis_result: dict[str, Any]
457
+ ) -> dict[str, Any]:
458
+ """Extract detailed metrics from universal analyzer result"""
459
+ basic = self._extract_universal_basic_metrics(analysis_result)
460
+
461
+ # Add query results if available
462
+ if "query_results" in analysis_result:
463
+ basic["query_results"] = analysis_result["query_results"]
464
+
465
+ return basic
466
+
467
+ def _extract_universal_structure_info(
468
+ self, analysis_result: dict[str, Any]
469
+ ) -> dict[str, Any]:
470
+ """Extract structure information from universal analyzer result"""
471
+ return {
472
+ "structure": analysis_result.get("structure", {}),
473
+ "queries_executed": analysis_result.get("queries_executed", []),
474
+ }
475
+
476
+ def _extract_universal_comprehensive_metrics(
477
+ self, analysis_result: dict[str, Any]
478
+ ) -> dict[str, Any]:
479
+ """Extract comprehensive metrics from universal analyzer result"""
480
+ detailed = self._extract_universal_detailed_metrics(analysis_result)
481
+ structure = self._extract_universal_structure_info(analysis_result)
482
+
483
+ # Combine both
484
+ result = detailed.copy()
485
+ result.update(structure)
486
+
487
+ return result
488
+
489
+ async def _get_available_queries(self, language: str) -> dict[str, Any]:
490
+ """
491
+ Get available queries for the specified language
492
+
493
+ Args:
494
+ language: Programming language
495
+
496
+ Returns:
497
+ Dictionary containing available queries information
498
+ """
499
+ try:
500
+ if language == "java":
501
+ # For Java, we don't have predefined queries in the advanced analyzer
502
+ return {
503
+ "language": language,
504
+ "queries": [],
505
+ "note": "Advanced analyzer uses built-in analysis logic",
506
+ }
507
+ else:
508
+ # For other languages, get from universal analyzer
509
+ queries = self.analysis_engine.get_supported_languages()
510
+ return {"language": language, "queries": queries, "count": len(queries)}
511
+ except Exception as e:
512
+ logger.warning(f"Failed to get queries for {language}: {e}")
513
+ return {"language": language, "queries": [], "error": str(e)}
514
+
515
+ def validate_arguments(self, arguments: dict[str, Any]) -> bool:
516
+ """
517
+ Validate tool arguments against the schema.
518
+
519
+ Args:
520
+ arguments: Arguments to validate
521
+
522
+ Returns:
523
+ True if arguments are valid
524
+
525
+ Raises:
526
+ ValueError: If arguments are invalid
527
+ """
528
+ # Check required fields
529
+ if "file_path" not in arguments:
530
+ raise ValueError("Required field 'file_path' is missing")
531
+
532
+ # Validate file_path
533
+ file_path = arguments["file_path"]
534
+ if not isinstance(file_path, str):
535
+ raise ValueError("file_path must be a string")
536
+ if not file_path.strip():
537
+ raise ValueError("file_path cannot be empty")
538
+
539
+ # Validate optional fields
540
+ if "language" in arguments:
541
+ language = arguments["language"]
542
+ if not isinstance(language, str):
543
+ raise ValueError("language must be a string")
544
+
545
+ if "analysis_type" in arguments:
546
+ analysis_type = arguments["analysis_type"]
547
+ if not isinstance(analysis_type, str):
548
+ raise ValueError("analysis_type must be a string")
549
+ valid_types = ["basic", "detailed", "structure", "metrics"]
550
+ if analysis_type not in valid_types:
551
+ raise ValueError(f"analysis_type must be one of {valid_types}")
552
+
553
+ if "include_ast" in arguments:
554
+ include_ast = arguments["include_ast"]
555
+ if not isinstance(include_ast, bool):
556
+ raise ValueError("include_ast must be a boolean")
557
+
558
+ if "include_queries" in arguments:
559
+ include_queries = arguments["include_queries"]
560
+ if not isinstance(include_queries, bool):
561
+ raise ValueError("include_queries must be a boolean")
562
+
563
+ return True