tree-sitter-analyzer 0.2.0__py3-none-any.whl → 0.3.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 +133 -121
  2. tree_sitter_analyzer/__main__.py +11 -12
  3. tree_sitter_analyzer/api.py +531 -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 +232 -233
  15. tree_sitter_analyzer/cli/info_commands.py +120 -121
  16. tree_sitter_analyzer/cli_main.py +277 -276
  17. tree_sitter_analyzer/core/__init__.py +15 -20
  18. tree_sitter_analyzer/core/analysis_engine.py +591 -574
  19. tree_sitter_analyzer/core/cache_service.py +320 -330
  20. tree_sitter_analyzer/core/engine.py +557 -560
  21. tree_sitter_analyzer/core/parser.py +293 -288
  22. tree_sitter_analyzer/core/query.py +494 -502
  23. tree_sitter_analyzer/encoding_utils.py +458 -460
  24. tree_sitter_analyzer/exceptions.py +337 -340
  25. tree_sitter_analyzer/file_handler.py +217 -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 +287 -270
  30. tree_sitter_analyzer/formatters/python_formatter.py +255 -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 +322 -319
  34. tree_sitter_analyzer/interfaces/mcp_adapter.py +180 -170
  35. tree_sitter_analyzer/interfaces/mcp_server.py +405 -416
  36. tree_sitter_analyzer/java_analyzer.py +218 -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 +1129 -1113
  41. tree_sitter_analyzer/languages/python_plugin.py +737 -712
  42. tree_sitter_analyzer/mcp/__init__.py +31 -32
  43. tree_sitter_analyzer/mcp/resources/__init__.py +44 -47
  44. tree_sitter_analyzer/mcp/resources/code_file_resource.py +212 -213
  45. tree_sitter_analyzer/mcp/resources/project_stats_resource.py +560 -550
  46. tree_sitter_analyzer/mcp/server.py +333 -345
  47. tree_sitter_analyzer/mcp/tools/__init__.py +30 -31
  48. tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +621 -557
  49. tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +242 -245
  50. tree_sitter_analyzer/mcp/tools/base_tool.py +54 -55
  51. tree_sitter_analyzer/mcp/tools/read_partial_tool.py +300 -302
  52. tree_sitter_analyzer/mcp/tools/table_format_tool.py +362 -359
  53. tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +543 -476
  54. tree_sitter_analyzer/mcp/utils/__init__.py +105 -106
  55. tree_sitter_analyzer/mcp/utils/error_handler.py +549 -549
  56. tree_sitter_analyzer/models.py +470 -481
  57. tree_sitter_analyzer/output_manager.py +261 -264
  58. tree_sitter_analyzer/plugins/__init__.py +333 -334
  59. tree_sitter_analyzer/plugins/base.py +477 -446
  60. tree_sitter_analyzer/plugins/java_plugin.py +608 -625
  61. tree_sitter_analyzer/plugins/javascript_plugin.py +446 -439
  62. tree_sitter_analyzer/plugins/manager.py +362 -355
  63. tree_sitter_analyzer/plugins/plugin_loader.py +85 -83
  64. tree_sitter_analyzer/plugins/python_plugin.py +606 -598
  65. tree_sitter_analyzer/plugins/registry.py +374 -366
  66. tree_sitter_analyzer/queries/__init__.py +26 -27
  67. tree_sitter_analyzer/queries/java.py +391 -394
  68. tree_sitter_analyzer/queries/javascript.py +148 -149
  69. tree_sitter_analyzer/queries/python.py +285 -286
  70. tree_sitter_analyzer/queries/typescript.py +229 -230
  71. tree_sitter_analyzer/query_loader.py +254 -260
  72. tree_sitter_analyzer/table_formatter.py +468 -448
  73. tree_sitter_analyzer/utils.py +277 -277
  74. {tree_sitter_analyzer-0.2.0.dist-info → tree_sitter_analyzer-0.3.0.dist-info}/METADATA +21 -6
  75. tree_sitter_analyzer-0.3.0.dist-info/RECORD +77 -0
  76. tree_sitter_analyzer-0.2.0.dist-info/RECORD +0 -77
  77. {tree_sitter_analyzer-0.2.0.dist-info → tree_sitter_analyzer-0.3.0.dist-info}/WHEEL +0 -0
  78. {tree_sitter_analyzer-0.2.0.dist-info → tree_sitter_analyzer-0.3.0.dist-info}/entry_points.txt +0 -0
@@ -1,550 +1,560 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- Project Statistics Resource for MCP
5
-
6
- This module provides MCP resource implementation for accessing project
7
- statistics and analysis data. The resource allows dynamic access to
8
- project analysis results through URI-based identification.
9
- """
10
-
11
- import json
12
- import logging
13
- import re
14
- from datetime import datetime
15
- from pathlib import Path
16
- from typing import Any, Dict, List, Optional, Union, cast
17
-
18
- from tree_sitter_analyzer.core.analysis_engine import get_analysis_engine, AnalysisRequest
19
- from tree_sitter_analyzer.core.analysis_engine import get_analysis_engine, AnalysisRequest
20
- from tree_sitter_analyzer.language_detector import (
21
- detect_language_from_file,
22
- is_language_supported,
23
- )
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
-
28
- class ProjectStatsResource:
29
- """
30
- MCP resource for accessing project statistics and analysis data
31
-
32
- This resource provides access to project analysis results through the MCP protocol.
33
- It supports various types of statistics including overview, language breakdown,
34
- complexity metrics, and file-level information.
35
-
36
- URI Format: code://stats/{stats_type}
37
-
38
- Supported stats types:
39
- - overview: General project overview
40
- - languages: Language breakdown and statistics
41
- - complexity: Complexity metrics and analysis
42
- - files: File-level statistics and information
43
-
44
- Examples:
45
- - code://stats/overview
46
- - code://stats/languages
47
- - code://stats/complexity
48
- - code://stats/files
49
- """
50
-
51
- def __init__(self) -> None:
52
- """Initialize the project statistics resource"""
53
- self._uri_pattern = re.compile(r"^code://stats/(.+)$")
54
- self._project_path: Optional[str] = None
55
- self.analysis_engine = get_analysis_engine()
56
- # Use unified analysis engine instead of deprecated AdvancedAnalyzer
57
-
58
- # Supported statistics types
59
- self._supported_stats_types = {"overview", "languages", "complexity", "files"}
60
-
61
- def get_resource_info(self) -> Dict[str, Any]:
62
- """
63
- Get resource information for MCP registration
64
-
65
- Returns:
66
- Dict containing resource metadata
67
- """
68
- return {
69
- "name": "project_stats",
70
- "description": "Access to project statistics and analysis data",
71
- "uri_template": "code://stats/{stats_type}",
72
- "mime_type": "application/json",
73
- }
74
-
75
- def matches_uri(self, uri: str) -> bool:
76
- """
77
- Check if the URI matches this resource pattern
78
-
79
- Args:
80
- uri: The URI to check
81
-
82
- Returns:
83
- True if the URI matches the project stats pattern
84
- """
85
- if not isinstance(uri, str):
86
- return False
87
-
88
- return bool(self._uri_pattern.match(uri))
89
-
90
- def _extract_stats_type(self, uri: str) -> str:
91
- """
92
- Extract statistics type from URI
93
-
94
- Args:
95
- uri: The URI to extract stats type from
96
-
97
- Returns:
98
- The extracted statistics type
99
-
100
- Raises:
101
- ValueError: If URI format is invalid
102
- """
103
- match = self._uri_pattern.match(uri)
104
- if not match:
105
- raise ValueError(f"Invalid URI format: {uri}")
106
-
107
- return match.group(1)
108
-
109
- def set_project_path(self, project_path: str) -> None:
110
- """
111
- Set the project path for analysis
112
-
113
- Args:
114
- project_path: Path to the project directory
115
-
116
- Raises:
117
- TypeError: If project_path is not a string
118
- ValueError: If project_path is empty
119
- """
120
- if not isinstance(project_path, str):
121
- raise TypeError("Project path must be a string")
122
-
123
- if not project_path:
124
- raise ValueError("Project path cannot be empty")
125
-
126
- self._project_path = project_path
127
-
128
- # Initialize analyzers with the project path
129
- self._advanced_analyzer = get_analysis_engine()
130
-
131
- logger.debug(f"Set project path to: {project_path}")
132
-
133
- def _validate_project_path(self) -> None:
134
- """
135
- Validate that project path is set and exists
136
-
137
- Raises:
138
- ValueError: If project path is not set
139
- FileNotFoundError: If project path doesn't exist
140
- """
141
- if not self._project_path:
142
- raise ValueError("Project path not set. Call set_project_path() first.")
143
-
144
- if self._project_path is None:
145
- raise ValueError("Project path is not set")
146
- project_dir = Path(self._project_path)
147
- if not project_dir.exists():
148
- raise FileNotFoundError(
149
- f"Project directory does not exist: {self._project_path}"
150
- )
151
-
152
- if not project_dir.is_dir():
153
- raise FileNotFoundError(
154
- f"Project path is not a directory: {self._project_path}"
155
- )
156
-
157
- def _is_supported_code_file(self, file_path: Path) -> bool:
158
- """
159
- Check if the file is a supported code file using language detection
160
-
161
- Args:
162
- file_path: Path to the file
163
-
164
- Returns:
165
- True if the file is a supported code file
166
- """
167
- try:
168
- language = detect_language_from_file(str(file_path))
169
- return is_language_supported(language)
170
- except Exception:
171
- return False
172
-
173
- def _get_language_from_file(self, file_path: Path) -> str:
174
- """
175
- Get language from file using language detector
176
-
177
- Args:
178
- file_path: Path to the file
179
-
180
- Returns:
181
- Detected language name
182
- """
183
- try:
184
- return detect_language_from_file(str(file_path))
185
- except Exception:
186
- return "unknown"
187
-
188
- async def _generate_overview_stats(self) -> Dict[str, Any]:
189
- """
190
- Generate overview statistics for the project
191
-
192
- Returns:
193
- Dictionary containing overview statistics
194
- """
195
- logger.debug("Generating overview statistics")
196
-
197
- # Scan project directory for actual file counts
198
- if self._project_path is None:
199
- raise ValueError("Project path is not set")
200
- project_dir = Path(self._project_path)
201
- total_files = 0
202
- total_lines = 0
203
- language_counts: Dict[str, int] = {}
204
-
205
- for file_path in project_dir.rglob("*"):
206
- if file_path.is_file() and self._is_supported_code_file(file_path):
207
- total_files += 1
208
- try:
209
- with open(file_path, "r", encoding="utf-8") as f:
210
- file_lines = sum(1 for _ in f)
211
- total_lines += file_lines
212
- except Exception:
213
- continue
214
- language = self._get_language_from_file(file_path)
215
- if language != "unknown":
216
- language_counts[language] = language_counts.get(language, 0) + 1
217
-
218
- analysis_result = {
219
- "total_files": total_files,
220
- "total_lines": total_lines,
221
- "languages": [
222
- {"name": lang, "file_count": count}
223
- for lang, count in language_counts.items()
224
- ],
225
- }
226
-
227
- # Extract overview information
228
- languages_data = analysis_result.get("languages", [])
229
- if languages_data is None:
230
- languages_data = []
231
-
232
- # Ensure languages_data is a list for iteration
233
- if not isinstance(languages_data, list):
234
- languages_data = []
235
-
236
- overview = {
237
- "total_files": analysis_result.get("total_files", 0),
238
- "total_lines": analysis_result.get("total_lines", 0),
239
- "languages": [
240
- str(lang["name"])
241
- for lang in languages_data
242
- if isinstance(lang, dict) and "name" in lang
243
- ],
244
- "project_path": self._project_path,
245
- "last_updated": datetime.now().isoformat(),
246
- }
247
-
248
- logger.debug(f"Generated overview with {overview['total_files']} files")
249
- return overview
250
-
251
- async def _generate_languages_stats(self) -> Dict[str, Any]:
252
- """
253
- Generate language-specific statistics
254
-
255
- Returns:
256
- Dictionary containing language statistics
257
- """
258
- logger.debug("Generating language statistics")
259
-
260
- # Scan project directory for actual language counts
261
- if self._project_path is None:
262
- raise ValueError("Project path is not set")
263
- project_dir = Path(self._project_path)
264
- total_files = 0
265
- total_lines = 0
266
- language_data = {}
267
-
268
- for file_path in project_dir.rglob("*"):
269
- if file_path.is_file() and self._is_supported_code_file(file_path):
270
- total_files += 1
271
- try:
272
- with open(file_path, "r", encoding="utf-8") as f:
273
- file_lines = sum(1 for _ in f)
274
- total_lines += file_lines
275
- except Exception:
276
- file_lines = 0
277
-
278
- language = self._get_language_from_file(file_path)
279
- if language != "unknown":
280
- if language not in language_data:
281
- language_data[language] = {"file_count": 0, "line_count": 0}
282
- language_data[language]["file_count"] += 1
283
- language_data[language]["line_count"] += file_lines
284
-
285
- # Convert to list format and calculate percentages
286
- languages_list = []
287
- for lang, data in language_data.items():
288
- percentage = (
289
- round((data["line_count"] / total_lines) * 100, 2)
290
- if total_lines > 0
291
- else 0.0
292
- )
293
- languages_list.append(
294
- {
295
- "name": lang,
296
- "file_count": data["file_count"],
297
- "line_count": data["line_count"],
298
- "percentage": percentage,
299
- }
300
- )
301
-
302
- languages_stats = {
303
- "languages": languages_list,
304
- "total_languages": len(languages_list),
305
- "last_updated": datetime.now().isoformat(),
306
- }
307
-
308
- logger.debug(f"Generated stats for {len(languages_list)} languages")
309
- return languages_stats
310
-
311
- async def _generate_complexity_stats(self) -> Dict[str, Any]:
312
- """
313
- Generate complexity statistics
314
-
315
- Returns:
316
- Dictionary containing complexity statistics
317
- """
318
- logger.debug("Generating complexity statistics")
319
-
320
- # Analyze files for complexity
321
- if self._project_path is None:
322
- raise ValueError("Project path is not set")
323
- project_dir = Path(self._project_path)
324
- complexity_data = []
325
- total_complexity = 0
326
- max_complexity = 0
327
- file_count = 0
328
-
329
- # Analyze each supported code file
330
- for file_path in project_dir.rglob("*"):
331
- if file_path.is_file() and self._is_supported_code_file(file_path):
332
- try:
333
- language = self._get_language_from_file(file_path)
334
-
335
- # Use appropriate analyzer based on language
336
- if language == "java":
337
- # Use advanced analyzer for Java
338
- if self._advanced_analyzer is None:
339
- self._advanced_analyzer = get_analysis_engine()
340
- # 使用 await 调用异步方法
341
- file_analysis = await self._advanced_analyzer.analyze_file(
342
- str(file_path)
343
- )
344
- if file_analysis and hasattr(file_analysis, "methods"):
345
- complexity = sum(
346
- method.complexity_score or 0
347
- for method in file_analysis.methods
348
- )
349
- else:
350
- complexity = 0
351
- else:
352
- # Use universal analyzer for other languages
353
- request = AnalysisRequest(file_path=str(file_path), language=language)
354
- file_analysis_result = await self.analysis_engine.analyze(request)
355
-
356
- complexity = 0
357
- if file_analysis_result and file_analysis_result.success:
358
- analysis_dict = file_analysis_result.to_dict()
359
- # Assuming complexity is part of the metrics in the new structure
360
- if 'metrics' in analysis_dict and 'complexity' in analysis_dict['metrics']:
361
- complexity = analysis_dict['metrics']['complexity'].get('total', 0)
362
-
363
- if complexity > 0:
364
- complexity_data.append(
365
- {
366
- "file": str(file_path.relative_to(project_dir)),
367
- "language": language,
368
- "complexity": complexity,
369
- }
370
- )
371
-
372
- total_complexity += complexity
373
- max_complexity = max(max_complexity, complexity)
374
- file_count += 1
375
-
376
- except Exception as e:
377
- logger.warning(f"Failed to analyze complexity for {file_path}: {e}")
378
- continue
379
-
380
- # Calculate average complexity
381
- avg_complexity = total_complexity / file_count if file_count > 0 else 0
382
-
383
- complexity_stats = {
384
- "average_complexity": round(avg_complexity, 2),
385
- "max_complexity": max_complexity,
386
- "total_files_analyzed": file_count,
387
- "files_by_complexity": sorted(
388
- complexity_data,
389
- key=lambda x: int(cast(int, x.get("complexity", 0))),
390
- reverse=True,
391
- ),
392
- "last_updated": datetime.now().isoformat(),
393
- }
394
-
395
- logger.debug(f"Generated complexity stats for {file_count} files")
396
- return complexity_stats
397
-
398
- async def _generate_files_stats(self) -> Dict[str, Any]:
399
- """
400
- Generate file-level statistics
401
-
402
- Returns:
403
- Dictionary containing file statistics
404
- """
405
- logger.debug("Generating file statistics")
406
-
407
- # Get detailed file information
408
- files_data = []
409
- if self._project_path is None:
410
- raise ValueError("Project path is not set")
411
- project_dir = Path(self._project_path)
412
-
413
- # Analyze each supported code file
414
- for file_path in project_dir.rglob("*"):
415
- if file_path.is_file() and self._is_supported_code_file(file_path):
416
- try:
417
- # Get file stats
418
- file_stats = file_path.stat()
419
-
420
- # Determine language using language detector
421
- language = self._get_language_from_file(file_path)
422
-
423
- # Count lines
424
- try:
425
- with open(file_path, "r", encoding="utf-8") as f:
426
- line_count = sum(1 for _ in f)
427
- except Exception:
428
- line_count = 0
429
-
430
- files_data.append(
431
- {
432
- "path": str(file_path.relative_to(project_dir)),
433
- "language": language,
434
- "line_count": line_count,
435
- "size_bytes": file_stats.st_size,
436
- "modified": datetime.fromtimestamp(
437
- file_stats.st_mtime
438
- ).isoformat(),
439
- }
440
- )
441
-
442
- except Exception as e:
443
- logger.warning(f"Failed to get stats for {file_path}: {e}")
444
- continue
445
-
446
- files_stats = {
447
- "files": sorted(
448
- files_data,
449
- key=lambda x: int(cast(int, x.get("line_count", 0))),
450
- reverse=True,
451
- ),
452
- "total_count": len(files_data),
453
- "last_updated": datetime.now().isoformat(),
454
- }
455
-
456
- logger.debug(f"Generated stats for {len(files_data)} files")
457
- return files_stats
458
-
459
- async def read_resource(self, uri: str) -> str:
460
- """
461
- Read resource content from URI
462
-
463
- Args:
464
- uri: The resource URI to read
465
-
466
- Returns:
467
- Resource content as JSON string
468
-
469
- Raises:
470
- ValueError: If URI format is invalid or stats type is unsupported
471
- FileNotFoundError: If project path doesn't exist
472
- """
473
- logger.debug(f"Reading resource: {uri}")
474
-
475
- # Validate URI format
476
- if not self.matches_uri(uri):
477
- raise ValueError(f"URI does not match project stats pattern: {uri}")
478
-
479
- # Extract statistics type
480
- stats_type = self._extract_stats_type(uri)
481
-
482
- # Validate statistics type
483
- if stats_type not in self._supported_stats_types:
484
- raise ValueError(
485
- f"Unsupported statistics type: {stats_type}. "
486
- f"Supported types: {', '.join(self._supported_stats_types)}"
487
- )
488
-
489
- # Validate project path
490
- self._validate_project_path()
491
-
492
- # Generate statistics based on type
493
- try:
494
- if stats_type == "overview":
495
- stats_data = await self._generate_overview_stats()
496
- elif stats_type == "languages":
497
- stats_data = await self._generate_languages_stats()
498
- elif stats_type == "complexity":
499
- stats_data = await self._generate_complexity_stats()
500
- elif stats_type == "files":
501
- stats_data = await self._generate_files_stats()
502
- else:
503
- raise ValueError(f"Unknown statistics type: {stats_type}")
504
-
505
- # Convert to JSON
506
- json_content = json.dumps(stats_data, indent=2, ensure_ascii=False)
507
- logger.debug(f"Successfully generated {stats_type} statistics")
508
- return json_content
509
-
510
- except Exception as e:
511
- logger.error(f"Failed to generate {stats_type} statistics: {e}")
512
- raise
513
-
514
- def get_supported_schemes(self) -> List[str]:
515
- """
516
- Get list of supported URI schemes
517
-
518
- Returns:
519
- List of supported schemes
520
- """
521
- return ["code"]
522
-
523
- def get_supported_resource_types(self) -> List[str]:
524
- """
525
- Get list of supported resource types
526
-
527
- Returns:
528
- List of supported resource types
529
- """
530
- return ["stats"]
531
-
532
- def get_supported_stats_types(self) -> List[str]:
533
- """
534
- Get list of supported statistics types
535
-
536
- Returns:
537
- List of supported statistics types
538
- """
539
- return list(self._supported_stats_types)
540
-
541
- def __str__(self) -> str:
542
- """String representation of the resource"""
543
- return f"ProjectStatsResource(pattern=code://stats/{{stats_type}})"
544
-
545
- def __repr__(self) -> str:
546
- """Detailed string representation of the resource"""
547
- return (
548
- f"ProjectStatsResource(uri_pattern={self._uri_pattern.pattern}, "
549
- f"project_path={self._project_path})"
550
- )
1
+ #!/usr/bin/env python3
2
+ """
3
+ Project Statistics Resource for MCP
4
+
5
+ This module provides MCP resource implementation for accessing project
6
+ statistics and analysis data. The resource allows dynamic access to
7
+ project analysis results through URI-based identification.
8
+ """
9
+
10
+ import json
11
+ import logging
12
+ import re
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+ from typing import Any, cast
16
+
17
+ from tree_sitter_analyzer.core.analysis_engine import (
18
+ AnalysisRequest,
19
+ get_analysis_engine,
20
+ )
21
+ from tree_sitter_analyzer.language_detector import (
22
+ detect_language_from_file,
23
+ is_language_supported,
24
+ )
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class ProjectStatsResource:
30
+ """
31
+ MCP resource for accessing project statistics and analysis data
32
+
33
+ This resource provides access to project analysis results through the MCP protocol.
34
+ It supports various types of statistics including overview, language breakdown,
35
+ complexity metrics, and file-level information.
36
+
37
+ URI Format: code://stats/{stats_type}
38
+
39
+ Supported stats types:
40
+ - overview: General project overview
41
+ - languages: Language breakdown and statistics
42
+ - complexity: Complexity metrics and analysis
43
+ - files: File-level statistics and information
44
+
45
+ Examples:
46
+ - code://stats/overview
47
+ - code://stats/languages
48
+ - code://stats/complexity
49
+ - code://stats/files
50
+ """
51
+
52
+ def __init__(self) -> None:
53
+ """Initialize the project statistics resource"""
54
+ self._uri_pattern = re.compile(r"^code://stats/(.+)$")
55
+ self._project_path: str | None = None
56
+ self.analysis_engine = get_analysis_engine()
57
+ # Use unified analysis engine instead of deprecated AdvancedAnalyzer
58
+
59
+ # Supported statistics types
60
+ self._supported_stats_types = {"overview", "languages", "complexity", "files"}
61
+
62
+ def get_resource_info(self) -> dict[str, Any]:
63
+ """
64
+ Get resource information for MCP registration
65
+
66
+ Returns:
67
+ Dict containing resource metadata
68
+ """
69
+ return {
70
+ "name": "project_stats",
71
+ "description": "Access to project statistics and analysis data",
72
+ "uri_template": "code://stats/{stats_type}",
73
+ "mime_type": "application/json",
74
+ }
75
+
76
+ def matches_uri(self, uri: str) -> bool:
77
+ """
78
+ Check if the URI matches this resource pattern
79
+
80
+ Args:
81
+ uri: The URI to check
82
+
83
+ Returns:
84
+ True if the URI matches the project stats pattern
85
+ """
86
+ if not isinstance(uri, str):
87
+ return False
88
+
89
+ return bool(self._uri_pattern.match(uri))
90
+
91
+ def _extract_stats_type(self, uri: str) -> str:
92
+ """
93
+ Extract statistics type from URI
94
+
95
+ Args:
96
+ uri: The URI to extract stats type from
97
+
98
+ Returns:
99
+ The extracted statistics type
100
+
101
+ Raises:
102
+ ValueError: If URI format is invalid
103
+ """
104
+ match = self._uri_pattern.match(uri)
105
+ if not match:
106
+ raise ValueError(f"Invalid URI format: {uri}")
107
+
108
+ return match.group(1)
109
+
110
+ def set_project_path(self, project_path: str) -> None:
111
+ """
112
+ Set the project path for analysis
113
+
114
+ Args:
115
+ project_path: Path to the project directory
116
+
117
+ Raises:
118
+ TypeError: If project_path is not a string
119
+ ValueError: If project_path is empty
120
+ """
121
+ if not isinstance(project_path, str):
122
+ raise TypeError("Project path must be a string")
123
+
124
+ if not project_path:
125
+ raise ValueError("Project path cannot be empty")
126
+
127
+ self._project_path = project_path
128
+
129
+ # Initialize analyzers with the project path
130
+ self._advanced_analyzer = get_analysis_engine()
131
+
132
+ logger.debug(f"Set project path to: {project_path}")
133
+
134
+ def _validate_project_path(self) -> None:
135
+ """
136
+ Validate that project path is set and exists
137
+
138
+ Raises:
139
+ ValueError: If project path is not set
140
+ FileNotFoundError: If project path doesn't exist
141
+ """
142
+ if not self._project_path:
143
+ raise ValueError("Project path not set. Call set_project_path() first.")
144
+
145
+ if self._project_path is None:
146
+ raise ValueError("Project path is not set")
147
+ project_dir = Path(self._project_path)
148
+ if not project_dir.exists():
149
+ raise FileNotFoundError(
150
+ f"Project directory does not exist: {self._project_path}"
151
+ )
152
+
153
+ if not project_dir.is_dir():
154
+ raise FileNotFoundError(
155
+ f"Project path is not a directory: {self._project_path}"
156
+ )
157
+
158
+ def _is_supported_code_file(self, file_path: Path) -> bool:
159
+ """
160
+ Check if the file is a supported code file using language detection
161
+
162
+ Args:
163
+ file_path: Path to the file
164
+
165
+ Returns:
166
+ True if the file is a supported code file
167
+ """
168
+ try:
169
+ language = detect_language_from_file(str(file_path))
170
+ return is_language_supported(language)
171
+ except Exception:
172
+ return False
173
+
174
+ def _get_language_from_file(self, file_path: Path) -> str:
175
+ """
176
+ Get language from file using language detector
177
+
178
+ Args:
179
+ file_path: Path to the file
180
+
181
+ Returns:
182
+ Detected language name
183
+ """
184
+ try:
185
+ return detect_language_from_file(str(file_path))
186
+ except Exception:
187
+ return "unknown"
188
+
189
+ async def _generate_overview_stats(self) -> dict[str, Any]:
190
+ """
191
+ Generate overview statistics for the project
192
+
193
+ Returns:
194
+ Dictionary containing overview statistics
195
+ """
196
+ logger.debug("Generating overview statistics")
197
+
198
+ # Scan project directory for actual file counts
199
+ if self._project_path is None:
200
+ raise ValueError("Project path is not set")
201
+ project_dir = Path(self._project_path)
202
+ total_files = 0
203
+ total_lines = 0
204
+ language_counts: dict[str, int] = {}
205
+
206
+ for file_path in project_dir.rglob("*"):
207
+ if file_path.is_file() and self._is_supported_code_file(file_path):
208
+ total_files += 1
209
+ try:
210
+ with open(file_path, encoding="utf-8") as f:
211
+ file_lines = sum(1 for _ in f)
212
+ total_lines += file_lines
213
+ except Exception:
214
+ continue
215
+ language = self._get_language_from_file(file_path)
216
+ if language != "unknown":
217
+ language_counts[language] = language_counts.get(language, 0) + 1
218
+
219
+ analysis_result = {
220
+ "total_files": total_files,
221
+ "total_lines": total_lines,
222
+ "languages": [
223
+ {"name": lang, "file_count": count}
224
+ for lang, count in language_counts.items()
225
+ ],
226
+ }
227
+
228
+ # Extract overview information
229
+ languages_data = analysis_result.get("languages", [])
230
+ if languages_data is None:
231
+ languages_data = []
232
+
233
+ # Ensure languages_data is a list for iteration
234
+ if not isinstance(languages_data, list):
235
+ languages_data = []
236
+
237
+ overview = {
238
+ "total_files": analysis_result.get("total_files", 0),
239
+ "total_lines": analysis_result.get("total_lines", 0),
240
+ "languages": [
241
+ str(lang["name"])
242
+ for lang in languages_data
243
+ if isinstance(lang, dict) and "name" in lang
244
+ ],
245
+ "project_path": self._project_path,
246
+ "last_updated": datetime.now().isoformat(),
247
+ }
248
+
249
+ logger.debug(f"Generated overview with {overview['total_files']} files")
250
+ return overview
251
+
252
+ async def _generate_languages_stats(self) -> dict[str, Any]:
253
+ """
254
+ Generate language-specific statistics
255
+
256
+ Returns:
257
+ Dictionary containing language statistics
258
+ """
259
+ logger.debug("Generating language statistics")
260
+
261
+ # Scan project directory for actual language counts
262
+ if self._project_path is None:
263
+ raise ValueError("Project path is not set")
264
+ project_dir = Path(self._project_path)
265
+ total_files = 0
266
+ total_lines = 0
267
+ language_data = {}
268
+
269
+ for file_path in project_dir.rglob("*"):
270
+ if file_path.is_file() and self._is_supported_code_file(file_path):
271
+ total_files += 1
272
+ try:
273
+ with open(file_path, encoding="utf-8") as f:
274
+ file_lines = sum(1 for _ in f)
275
+ total_lines += file_lines
276
+ except Exception:
277
+ file_lines = 0
278
+
279
+ language = self._get_language_from_file(file_path)
280
+ if language != "unknown":
281
+ if language not in language_data:
282
+ language_data[language] = {"file_count": 0, "line_count": 0}
283
+ language_data[language]["file_count"] += 1
284
+ language_data[language]["line_count"] += file_lines
285
+
286
+ # Convert to list format and calculate percentages
287
+ languages_list = []
288
+ for lang, data in language_data.items():
289
+ percentage = (
290
+ round((data["line_count"] / total_lines) * 100, 2)
291
+ if total_lines > 0
292
+ else 0.0
293
+ )
294
+ languages_list.append(
295
+ {
296
+ "name": lang,
297
+ "file_count": data["file_count"],
298
+ "line_count": data["line_count"],
299
+ "percentage": percentage,
300
+ }
301
+ )
302
+
303
+ languages_stats = {
304
+ "languages": languages_list,
305
+ "total_languages": len(languages_list),
306
+ "last_updated": datetime.now().isoformat(),
307
+ }
308
+
309
+ logger.debug(f"Generated stats for {len(languages_list)} languages")
310
+ return languages_stats
311
+
312
+ async def _generate_complexity_stats(self) -> dict[str, Any]:
313
+ """
314
+ Generate complexity statistics
315
+
316
+ Returns:
317
+ Dictionary containing complexity statistics
318
+ """
319
+ logger.debug("Generating complexity statistics")
320
+
321
+ # Analyze files for complexity
322
+ if self._project_path is None:
323
+ raise ValueError("Project path is not set")
324
+ project_dir = Path(self._project_path)
325
+ complexity_data = []
326
+ total_complexity = 0
327
+ max_complexity = 0
328
+ file_count = 0
329
+
330
+ # Analyze each supported code file
331
+ for file_path in project_dir.rglob("*"):
332
+ if file_path.is_file() and self._is_supported_code_file(file_path):
333
+ try:
334
+ language = self._get_language_from_file(file_path)
335
+
336
+ # Use appropriate analyzer based on language
337
+ if language == "java":
338
+ # Use advanced analyzer for Java
339
+ if self._advanced_analyzer is None:
340
+ self._advanced_analyzer = get_analysis_engine()
341
+ # 使用 await 调用异步方法
342
+ file_analysis = await self._advanced_analyzer.analyze_file(
343
+ str(file_path)
344
+ )
345
+ if file_analysis and hasattr(file_analysis, "methods"):
346
+ complexity = sum(
347
+ method.complexity_score or 0
348
+ for method in file_analysis.methods
349
+ )
350
+ else:
351
+ complexity = 0
352
+ else:
353
+ # Use universal analyzer for other languages
354
+ request = AnalysisRequest(
355
+ file_path=str(file_path), language=language
356
+ )
357
+ file_analysis_result = await self.analysis_engine.analyze(
358
+ request
359
+ )
360
+
361
+ complexity = 0
362
+ if file_analysis_result and file_analysis_result.success:
363
+ analysis_dict = file_analysis_result.to_dict()
364
+ # Assuming complexity is part of the metrics in the new structure
365
+ if (
366
+ "metrics" in analysis_dict
367
+ and "complexity" in analysis_dict["metrics"]
368
+ ):
369
+ complexity = analysis_dict["metrics"]["complexity"].get(
370
+ "total", 0
371
+ )
372
+
373
+ if complexity > 0:
374
+ complexity_data.append(
375
+ {
376
+ "file": str(file_path.relative_to(project_dir)),
377
+ "language": language,
378
+ "complexity": complexity,
379
+ }
380
+ )
381
+
382
+ total_complexity += complexity
383
+ max_complexity = max(max_complexity, complexity)
384
+ file_count += 1
385
+
386
+ except Exception as e:
387
+ logger.warning(f"Failed to analyze complexity for {file_path}: {e}")
388
+ continue
389
+
390
+ # Calculate average complexity
391
+ avg_complexity = total_complexity / file_count if file_count > 0 else 0
392
+
393
+ complexity_stats = {
394
+ "average_complexity": round(avg_complexity, 2),
395
+ "max_complexity": max_complexity,
396
+ "total_files_analyzed": file_count,
397
+ "files_by_complexity": sorted(
398
+ complexity_data,
399
+ key=lambda x: int(cast(int, x.get("complexity", 0))),
400
+ reverse=True,
401
+ ),
402
+ "last_updated": datetime.now().isoformat(),
403
+ }
404
+
405
+ logger.debug(f"Generated complexity stats for {file_count} files")
406
+ return complexity_stats
407
+
408
+ async def _generate_files_stats(self) -> dict[str, Any]:
409
+ """
410
+ Generate file-level statistics
411
+
412
+ Returns:
413
+ Dictionary containing file statistics
414
+ """
415
+ logger.debug("Generating file statistics")
416
+
417
+ # Get detailed file information
418
+ files_data = []
419
+ if self._project_path is None:
420
+ raise ValueError("Project path is not set")
421
+ project_dir = Path(self._project_path)
422
+
423
+ # Analyze each supported code file
424
+ for file_path in project_dir.rglob("*"):
425
+ if file_path.is_file() and self._is_supported_code_file(file_path):
426
+ try:
427
+ # Get file stats
428
+ file_stats = file_path.stat()
429
+
430
+ # Determine language using language detector
431
+ language = self._get_language_from_file(file_path)
432
+
433
+ # Count lines
434
+ try:
435
+ with open(file_path, encoding="utf-8") as f:
436
+ line_count = sum(1 for _ in f)
437
+ except Exception:
438
+ line_count = 0
439
+
440
+ files_data.append(
441
+ {
442
+ "path": str(file_path.relative_to(project_dir)),
443
+ "language": language,
444
+ "line_count": line_count,
445
+ "size_bytes": file_stats.st_size,
446
+ "modified": datetime.fromtimestamp(
447
+ file_stats.st_mtime
448
+ ).isoformat(),
449
+ }
450
+ )
451
+
452
+ except Exception as e:
453
+ logger.warning(f"Failed to get stats for {file_path}: {e}")
454
+ continue
455
+
456
+ files_stats = {
457
+ "files": sorted(
458
+ files_data,
459
+ key=lambda x: int(cast(int, x.get("line_count", 0))),
460
+ reverse=True,
461
+ ),
462
+ "total_count": len(files_data),
463
+ "last_updated": datetime.now().isoformat(),
464
+ }
465
+
466
+ logger.debug(f"Generated stats for {len(files_data)} files")
467
+ return files_stats
468
+
469
+ async def read_resource(self, uri: str) -> str:
470
+ """
471
+ Read resource content from URI
472
+
473
+ Args:
474
+ uri: The resource URI to read
475
+
476
+ Returns:
477
+ Resource content as JSON string
478
+
479
+ Raises:
480
+ ValueError: If URI format is invalid or stats type is unsupported
481
+ FileNotFoundError: If project path doesn't exist
482
+ """
483
+ logger.debug(f"Reading resource: {uri}")
484
+
485
+ # Validate URI format
486
+ if not self.matches_uri(uri):
487
+ raise ValueError(f"URI does not match project stats pattern: {uri}")
488
+
489
+ # Extract statistics type
490
+ stats_type = self._extract_stats_type(uri)
491
+
492
+ # Validate statistics type
493
+ if stats_type not in self._supported_stats_types:
494
+ raise ValueError(
495
+ f"Unsupported statistics type: {stats_type}. "
496
+ f"Supported types: {', '.join(self._supported_stats_types)}"
497
+ )
498
+
499
+ # Validate project path
500
+ self._validate_project_path()
501
+
502
+ # Generate statistics based on type
503
+ try:
504
+ if stats_type == "overview":
505
+ stats_data = await self._generate_overview_stats()
506
+ elif stats_type == "languages":
507
+ stats_data = await self._generate_languages_stats()
508
+ elif stats_type == "complexity":
509
+ stats_data = await self._generate_complexity_stats()
510
+ elif stats_type == "files":
511
+ stats_data = await self._generate_files_stats()
512
+ else:
513
+ raise ValueError(f"Unknown statistics type: {stats_type}")
514
+
515
+ # Convert to JSON
516
+ json_content = json.dumps(stats_data, indent=2, ensure_ascii=False)
517
+ logger.debug(f"Successfully generated {stats_type} statistics")
518
+ return json_content
519
+
520
+ except Exception as e:
521
+ logger.error(f"Failed to generate {stats_type} statistics: {e}")
522
+ raise
523
+
524
+ def get_supported_schemes(self) -> list[str]:
525
+ """
526
+ Get list of supported URI schemes
527
+
528
+ Returns:
529
+ List of supported schemes
530
+ """
531
+ return ["code"]
532
+
533
+ def get_supported_resource_types(self) -> list[str]:
534
+ """
535
+ Get list of supported resource types
536
+
537
+ Returns:
538
+ List of supported resource types
539
+ """
540
+ return ["stats"]
541
+
542
+ def get_supported_stats_types(self) -> list[str]:
543
+ """
544
+ Get list of supported statistics types
545
+
546
+ Returns:
547
+ List of supported statistics types
548
+ """
549
+ return list(self._supported_stats_types)
550
+
551
+ def __str__(self) -> str:
552
+ """String representation of the resource"""
553
+ return "ProjectStatsResource(pattern=code://stats/{stats_type})"
554
+
555
+ def __repr__(self) -> str:
556
+ """Detailed string representation of the resource"""
557
+ return (
558
+ f"ProjectStatsResource(uri_pattern={self._uri_pattern.pattern}, "
559
+ f"project_path={self._project_path})"
560
+ )