tree-sitter-analyzer 1.9.17.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.
- tree_sitter_analyzer/__init__.py +132 -0
- tree_sitter_analyzer/__main__.py +11 -0
- tree_sitter_analyzer/api.py +853 -0
- tree_sitter_analyzer/cli/__init__.py +39 -0
- tree_sitter_analyzer/cli/__main__.py +12 -0
- tree_sitter_analyzer/cli/argument_validator.py +89 -0
- tree_sitter_analyzer/cli/commands/__init__.py +26 -0
- tree_sitter_analyzer/cli/commands/advanced_command.py +226 -0
- tree_sitter_analyzer/cli/commands/base_command.py +181 -0
- tree_sitter_analyzer/cli/commands/default_command.py +18 -0
- tree_sitter_analyzer/cli/commands/find_and_grep_cli.py +188 -0
- tree_sitter_analyzer/cli/commands/list_files_cli.py +133 -0
- tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -0
- tree_sitter_analyzer/cli/commands/query_command.py +109 -0
- tree_sitter_analyzer/cli/commands/search_content_cli.py +161 -0
- tree_sitter_analyzer/cli/commands/structure_command.py +156 -0
- tree_sitter_analyzer/cli/commands/summary_command.py +116 -0
- tree_sitter_analyzer/cli/commands/table_command.py +414 -0
- tree_sitter_analyzer/cli/info_commands.py +124 -0
- tree_sitter_analyzer/cli_main.py +472 -0
- tree_sitter_analyzer/constants.py +85 -0
- tree_sitter_analyzer/core/__init__.py +15 -0
- tree_sitter_analyzer/core/analysis_engine.py +580 -0
- tree_sitter_analyzer/core/cache_service.py +333 -0
- tree_sitter_analyzer/core/engine.py +585 -0
- tree_sitter_analyzer/core/parser.py +293 -0
- tree_sitter_analyzer/core/query.py +605 -0
- tree_sitter_analyzer/core/query_filter.py +200 -0
- tree_sitter_analyzer/core/query_service.py +340 -0
- tree_sitter_analyzer/encoding_utils.py +530 -0
- tree_sitter_analyzer/exceptions.py +747 -0
- tree_sitter_analyzer/file_handler.py +246 -0
- tree_sitter_analyzer/formatters/__init__.py +1 -0
- tree_sitter_analyzer/formatters/base_formatter.py +201 -0
- tree_sitter_analyzer/formatters/csharp_formatter.py +367 -0
- tree_sitter_analyzer/formatters/formatter_config.py +197 -0
- tree_sitter_analyzer/formatters/formatter_factory.py +84 -0
- tree_sitter_analyzer/formatters/formatter_registry.py +377 -0
- tree_sitter_analyzer/formatters/formatter_selector.py +96 -0
- tree_sitter_analyzer/formatters/go_formatter.py +368 -0
- tree_sitter_analyzer/formatters/html_formatter.py +498 -0
- tree_sitter_analyzer/formatters/java_formatter.py +423 -0
- tree_sitter_analyzer/formatters/javascript_formatter.py +611 -0
- tree_sitter_analyzer/formatters/kotlin_formatter.py +268 -0
- tree_sitter_analyzer/formatters/language_formatter_factory.py +123 -0
- tree_sitter_analyzer/formatters/legacy_formatter_adapters.py +228 -0
- tree_sitter_analyzer/formatters/markdown_formatter.py +725 -0
- tree_sitter_analyzer/formatters/php_formatter.py +301 -0
- tree_sitter_analyzer/formatters/python_formatter.py +830 -0
- tree_sitter_analyzer/formatters/ruby_formatter.py +278 -0
- tree_sitter_analyzer/formatters/rust_formatter.py +233 -0
- tree_sitter_analyzer/formatters/sql_formatter_wrapper.py +689 -0
- tree_sitter_analyzer/formatters/sql_formatters.py +536 -0
- tree_sitter_analyzer/formatters/typescript_formatter.py +543 -0
- tree_sitter_analyzer/formatters/yaml_formatter.py +462 -0
- tree_sitter_analyzer/interfaces/__init__.py +9 -0
- tree_sitter_analyzer/interfaces/cli.py +535 -0
- tree_sitter_analyzer/interfaces/cli_adapter.py +359 -0
- tree_sitter_analyzer/interfaces/mcp_adapter.py +224 -0
- tree_sitter_analyzer/interfaces/mcp_server.py +428 -0
- tree_sitter_analyzer/language_detector.py +553 -0
- tree_sitter_analyzer/language_loader.py +271 -0
- tree_sitter_analyzer/languages/__init__.py +10 -0
- tree_sitter_analyzer/languages/csharp_plugin.py +1076 -0
- tree_sitter_analyzer/languages/css_plugin.py +449 -0
- tree_sitter_analyzer/languages/go_plugin.py +836 -0
- tree_sitter_analyzer/languages/html_plugin.py +496 -0
- tree_sitter_analyzer/languages/java_plugin.py +1299 -0
- tree_sitter_analyzer/languages/javascript_plugin.py +1622 -0
- tree_sitter_analyzer/languages/kotlin_plugin.py +656 -0
- tree_sitter_analyzer/languages/markdown_plugin.py +1928 -0
- tree_sitter_analyzer/languages/php_plugin.py +862 -0
- tree_sitter_analyzer/languages/python_plugin.py +1636 -0
- tree_sitter_analyzer/languages/ruby_plugin.py +757 -0
- tree_sitter_analyzer/languages/rust_plugin.py +673 -0
- tree_sitter_analyzer/languages/sql_plugin.py +2444 -0
- tree_sitter_analyzer/languages/typescript_plugin.py +1892 -0
- tree_sitter_analyzer/languages/yaml_plugin.py +695 -0
- tree_sitter_analyzer/legacy_table_formatter.py +860 -0
- tree_sitter_analyzer/mcp/__init__.py +34 -0
- tree_sitter_analyzer/mcp/resources/__init__.py +43 -0
- tree_sitter_analyzer/mcp/resources/code_file_resource.py +208 -0
- tree_sitter_analyzer/mcp/resources/project_stats_resource.py +586 -0
- tree_sitter_analyzer/mcp/server.py +869 -0
- tree_sitter_analyzer/mcp/tools/__init__.py +28 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +779 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +291 -0
- tree_sitter_analyzer/mcp/tools/base_tool.py +139 -0
- tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +816 -0
- tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +686 -0
- tree_sitter_analyzer/mcp/tools/list_files_tool.py +413 -0
- tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
- tree_sitter_analyzer/mcp/tools/query_tool.py +443 -0
- tree_sitter_analyzer/mcp/tools/read_partial_tool.py +464 -0
- tree_sitter_analyzer/mcp/tools/search_content_tool.py +836 -0
- tree_sitter_analyzer/mcp/tools/table_format_tool.py +572 -0
- tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +653 -0
- tree_sitter_analyzer/mcp/utils/__init__.py +113 -0
- tree_sitter_analyzer/mcp/utils/error_handler.py +569 -0
- tree_sitter_analyzer/mcp/utils/file_output_factory.py +217 -0
- tree_sitter_analyzer/mcp/utils/file_output_manager.py +322 -0
- tree_sitter_analyzer/mcp/utils/gitignore_detector.py +358 -0
- tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -0
- tree_sitter_analyzer/mcp/utils/search_cache.py +343 -0
- tree_sitter_analyzer/models.py +840 -0
- tree_sitter_analyzer/mypy_current_errors.txt +2 -0
- tree_sitter_analyzer/output_manager.py +255 -0
- tree_sitter_analyzer/platform_compat/__init__.py +3 -0
- tree_sitter_analyzer/platform_compat/adapter.py +324 -0
- tree_sitter_analyzer/platform_compat/compare.py +224 -0
- tree_sitter_analyzer/platform_compat/detector.py +67 -0
- tree_sitter_analyzer/platform_compat/fixtures.py +228 -0
- tree_sitter_analyzer/platform_compat/profiles.py +217 -0
- tree_sitter_analyzer/platform_compat/record.py +55 -0
- tree_sitter_analyzer/platform_compat/recorder.py +155 -0
- tree_sitter_analyzer/platform_compat/report.py +92 -0
- tree_sitter_analyzer/plugins/__init__.py +280 -0
- tree_sitter_analyzer/plugins/base.py +647 -0
- tree_sitter_analyzer/plugins/manager.py +384 -0
- tree_sitter_analyzer/project_detector.py +328 -0
- tree_sitter_analyzer/queries/__init__.py +27 -0
- tree_sitter_analyzer/queries/csharp.py +216 -0
- tree_sitter_analyzer/queries/css.py +615 -0
- tree_sitter_analyzer/queries/go.py +275 -0
- tree_sitter_analyzer/queries/html.py +543 -0
- tree_sitter_analyzer/queries/java.py +402 -0
- tree_sitter_analyzer/queries/javascript.py +724 -0
- tree_sitter_analyzer/queries/kotlin.py +192 -0
- tree_sitter_analyzer/queries/markdown.py +258 -0
- tree_sitter_analyzer/queries/php.py +95 -0
- tree_sitter_analyzer/queries/python.py +859 -0
- tree_sitter_analyzer/queries/ruby.py +92 -0
- tree_sitter_analyzer/queries/rust.py +223 -0
- tree_sitter_analyzer/queries/sql.py +555 -0
- tree_sitter_analyzer/queries/typescript.py +871 -0
- tree_sitter_analyzer/queries/yaml.py +236 -0
- tree_sitter_analyzer/query_loader.py +272 -0
- tree_sitter_analyzer/security/__init__.py +22 -0
- tree_sitter_analyzer/security/boundary_manager.py +277 -0
- tree_sitter_analyzer/security/regex_checker.py +297 -0
- tree_sitter_analyzer/security/validator.py +599 -0
- tree_sitter_analyzer/table_formatter.py +782 -0
- tree_sitter_analyzer/utils/__init__.py +53 -0
- tree_sitter_analyzer/utils/logging.py +433 -0
- tree_sitter_analyzer/utils/tree_sitter_compat.py +289 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/METADATA +485 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/RECORD +149 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/WHEEL +4 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/entry_points.txt +25 -0
|
@@ -0,0 +1,586 @@
|
|
|
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
|
+
@property
|
|
63
|
+
def project_root(self) -> str | None:
|
|
64
|
+
"""Get the current project root path"""
|
|
65
|
+
return self._project_path
|
|
66
|
+
|
|
67
|
+
@project_root.setter
|
|
68
|
+
def project_root(self, value: str | None) -> None:
|
|
69
|
+
"""Set the current project root path"""
|
|
70
|
+
self._project_path = value
|
|
71
|
+
|
|
72
|
+
def get_resource_info(self) -> dict[str, Any]:
|
|
73
|
+
"""
|
|
74
|
+
Get resource information for MCP registration
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Dict containing resource metadata
|
|
78
|
+
"""
|
|
79
|
+
return {
|
|
80
|
+
"name": "project_stats",
|
|
81
|
+
"description": "Access to project statistics and analysis data",
|
|
82
|
+
"uri_template": "code://stats/{stats_type}",
|
|
83
|
+
"mime_type": "application/json",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
def matches_uri(self, uri: str) -> bool:
|
|
87
|
+
"""
|
|
88
|
+
Check if the URI matches this resource pattern
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
uri: The URI to check
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
True if the URI matches the project stats pattern
|
|
95
|
+
"""
|
|
96
|
+
return bool(self._uri_pattern.match(uri))
|
|
97
|
+
|
|
98
|
+
def _extract_stats_type(self, uri: str) -> str:
|
|
99
|
+
"""
|
|
100
|
+
Extract statistics type from URI
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
uri: The URI to extract stats type from
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
The extracted statistics type
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
ValueError: If URI format is invalid
|
|
110
|
+
"""
|
|
111
|
+
match = self._uri_pattern.match(uri)
|
|
112
|
+
if not match:
|
|
113
|
+
raise ValueError(f"Invalid URI format: {uri}")
|
|
114
|
+
|
|
115
|
+
return match.group(1)
|
|
116
|
+
|
|
117
|
+
def set_project_path(self, project_path: str) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Set the project path for analysis
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
project_path: Path to the project directory
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
TypeError: If project_path is not a string
|
|
126
|
+
ValueError: If project_path is empty
|
|
127
|
+
"""
|
|
128
|
+
if not isinstance(project_path, str):
|
|
129
|
+
raise TypeError("Project path must be a string")
|
|
130
|
+
|
|
131
|
+
if not project_path:
|
|
132
|
+
raise ValueError("Project path cannot be empty")
|
|
133
|
+
|
|
134
|
+
self._project_path = project_path
|
|
135
|
+
|
|
136
|
+
# Note: analysis_engine is already initialized in __init__
|
|
137
|
+
# No need to reinitialize here
|
|
138
|
+
|
|
139
|
+
logger.debug(f"Set project path to: {project_path}")
|
|
140
|
+
|
|
141
|
+
def _validate_project_path(self) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Validate that project path is set and exists
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
ValueError: If project path is not set
|
|
147
|
+
FileNotFoundError: If project path doesn't exist
|
|
148
|
+
"""
|
|
149
|
+
if not self._project_path:
|
|
150
|
+
raise ValueError("Project path not set. Call set_project_path() first.")
|
|
151
|
+
|
|
152
|
+
if self._project_path is None:
|
|
153
|
+
raise ValueError("Project path is not set")
|
|
154
|
+
project_dir = Path(self._project_path)
|
|
155
|
+
if not project_dir.exists():
|
|
156
|
+
raise FileNotFoundError(
|
|
157
|
+
f"Project directory does not exist: {self._project_path}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if not project_dir.is_dir():
|
|
161
|
+
raise FileNotFoundError(
|
|
162
|
+
f"Project path is not a directory: {self._project_path}"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def _is_supported_code_file(self, file_path: Path) -> bool:
|
|
166
|
+
"""
|
|
167
|
+
Check if the file is a supported code file using language detection
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
file_path: Path to the file
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
True if the file is a supported code file
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
language = detect_language_from_file(str(file_path))
|
|
177
|
+
return is_language_supported(language)
|
|
178
|
+
except Exception:
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
def _get_language_from_file(self, file_path: Path) -> str:
|
|
182
|
+
"""
|
|
183
|
+
Get language from file using language detector
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
file_path: Path to the file
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Detected language name
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
return detect_language_from_file(str(file_path))
|
|
193
|
+
except Exception:
|
|
194
|
+
return "unknown"
|
|
195
|
+
|
|
196
|
+
async def _generate_overview_stats(self) -> dict[str, Any]:
|
|
197
|
+
"""
|
|
198
|
+
Generate overview statistics for the project
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Dictionary containing overview statistics
|
|
202
|
+
"""
|
|
203
|
+
logger.debug("Generating overview statistics")
|
|
204
|
+
|
|
205
|
+
# Scan project directory for actual file counts
|
|
206
|
+
if self._project_path is None:
|
|
207
|
+
raise ValueError("Project path is not set")
|
|
208
|
+
project_dir = Path(self._project_path)
|
|
209
|
+
total_files = 0
|
|
210
|
+
total_lines = 0
|
|
211
|
+
language_counts: dict[str, int] = {}
|
|
212
|
+
|
|
213
|
+
for file_path in project_dir.rglob("*"):
|
|
214
|
+
if file_path.is_file() and self._is_supported_code_file(file_path):
|
|
215
|
+
total_files += 1
|
|
216
|
+
try:
|
|
217
|
+
from ...encoding_utils import read_file_safe
|
|
218
|
+
|
|
219
|
+
content, _ = read_file_safe(file_path)
|
|
220
|
+
file_lines = len(content.splitlines())
|
|
221
|
+
total_lines += file_lines
|
|
222
|
+
except Exception as e:
|
|
223
|
+
logger.debug(
|
|
224
|
+
f"Skipping unreadable file during overview scan: {file_path} ({e})"
|
|
225
|
+
)
|
|
226
|
+
continue
|
|
227
|
+
language = self._get_language_from_file(file_path)
|
|
228
|
+
if language != "unknown":
|
|
229
|
+
language_counts[language] = language_counts.get(language, 0) + 1
|
|
230
|
+
|
|
231
|
+
analysis_result = {
|
|
232
|
+
"total_files": total_files,
|
|
233
|
+
"total_lines": total_lines,
|
|
234
|
+
"languages": [
|
|
235
|
+
{"name": lang, "file_count": count}
|
|
236
|
+
for lang, count in language_counts.items()
|
|
237
|
+
],
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# Extract overview information
|
|
241
|
+
languages_data = analysis_result.get("languages", [])
|
|
242
|
+
if languages_data is None:
|
|
243
|
+
languages_data = []
|
|
244
|
+
|
|
245
|
+
# Ensure languages_data is a list for iteration
|
|
246
|
+
if not isinstance(languages_data, list):
|
|
247
|
+
languages_data = []
|
|
248
|
+
|
|
249
|
+
overview = {
|
|
250
|
+
"total_files": analysis_result.get("total_files", 0),
|
|
251
|
+
"total_lines": analysis_result.get("total_lines", 0),
|
|
252
|
+
"languages": [
|
|
253
|
+
str(lang["name"])
|
|
254
|
+
for lang in languages_data
|
|
255
|
+
if isinstance(lang, dict) and "name" in lang
|
|
256
|
+
],
|
|
257
|
+
"project_path": self._project_path,
|
|
258
|
+
"last_updated": datetime.now().isoformat(),
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
logger.debug(f"Generated overview with {overview['total_files']} files")
|
|
262
|
+
return overview
|
|
263
|
+
|
|
264
|
+
async def _generate_languages_stats(self) -> dict[str, Any]:
|
|
265
|
+
"""
|
|
266
|
+
Generate language-specific statistics
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Dictionary containing language statistics
|
|
270
|
+
"""
|
|
271
|
+
logger.debug("Generating language statistics")
|
|
272
|
+
|
|
273
|
+
# Scan project directory for actual language counts
|
|
274
|
+
if self._project_path is None:
|
|
275
|
+
raise ValueError("Project path is not set")
|
|
276
|
+
project_dir = Path(self._project_path)
|
|
277
|
+
total_files = 0
|
|
278
|
+
total_lines = 0
|
|
279
|
+
language_data = {}
|
|
280
|
+
|
|
281
|
+
for file_path in project_dir.rglob("*"):
|
|
282
|
+
if file_path.is_file() and self._is_supported_code_file(file_path):
|
|
283
|
+
total_files += 1
|
|
284
|
+
try:
|
|
285
|
+
from ...encoding_utils import read_file_safe
|
|
286
|
+
|
|
287
|
+
content, _ = read_file_safe(file_path)
|
|
288
|
+
file_lines = len(content.splitlines())
|
|
289
|
+
total_lines += file_lines
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.debug(f"Failed to count lines for {file_path}: {e}")
|
|
292
|
+
file_lines = 0
|
|
293
|
+
|
|
294
|
+
language = self._get_language_from_file(file_path)
|
|
295
|
+
if language != "unknown":
|
|
296
|
+
if language not in language_data:
|
|
297
|
+
language_data[language] = {"file_count": 0, "line_count": 0}
|
|
298
|
+
language_data[language]["file_count"] += 1
|
|
299
|
+
language_data[language]["line_count"] += file_lines
|
|
300
|
+
|
|
301
|
+
# Convert to list format and calculate percentages
|
|
302
|
+
languages_list = []
|
|
303
|
+
for lang, data in language_data.items():
|
|
304
|
+
percentage = (
|
|
305
|
+
round((data["line_count"] / total_lines) * 100, 2)
|
|
306
|
+
if total_lines > 0
|
|
307
|
+
else 0.0
|
|
308
|
+
)
|
|
309
|
+
languages_list.append(
|
|
310
|
+
{
|
|
311
|
+
"name": lang,
|
|
312
|
+
"file_count": data["file_count"],
|
|
313
|
+
"line_count": data["line_count"],
|
|
314
|
+
"percentage": percentage,
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
languages_stats = {
|
|
319
|
+
"languages": languages_list,
|
|
320
|
+
"total_languages": len(languages_list),
|
|
321
|
+
"last_updated": datetime.now().isoformat(),
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
logger.debug(f"Generated stats for {len(languages_list)} languages")
|
|
325
|
+
return languages_stats
|
|
326
|
+
|
|
327
|
+
async def _generate_complexity_stats(self) -> dict[str, Any]:
|
|
328
|
+
"""
|
|
329
|
+
Generate complexity statistics
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Dictionary containing complexity statistics
|
|
333
|
+
"""
|
|
334
|
+
logger.debug("Generating complexity statistics")
|
|
335
|
+
|
|
336
|
+
# Analyze files for complexity
|
|
337
|
+
if self._project_path is None:
|
|
338
|
+
raise ValueError("Project path is not set")
|
|
339
|
+
project_dir = Path(self._project_path)
|
|
340
|
+
complexity_data = []
|
|
341
|
+
total_complexity = 0
|
|
342
|
+
max_complexity = 0
|
|
343
|
+
file_count = 0
|
|
344
|
+
|
|
345
|
+
# Analyze each supported code file
|
|
346
|
+
for file_path in project_dir.rglob("*"):
|
|
347
|
+
if file_path.is_file() and self._is_supported_code_file(file_path):
|
|
348
|
+
try:
|
|
349
|
+
language = self._get_language_from_file(file_path)
|
|
350
|
+
|
|
351
|
+
# Use appropriate analyzer based on language
|
|
352
|
+
if language == "java":
|
|
353
|
+
# Use analysis engine for Java
|
|
354
|
+
file_analysis = await self.analysis_engine.analyze_file(
|
|
355
|
+
str(file_path)
|
|
356
|
+
)
|
|
357
|
+
if file_analysis and hasattr(file_analysis, "methods"):
|
|
358
|
+
# Extract complexity from methods if available
|
|
359
|
+
complexity = sum(
|
|
360
|
+
method.complexity_score or 0
|
|
361
|
+
for method in file_analysis.methods
|
|
362
|
+
)
|
|
363
|
+
elif file_analysis and hasattr(file_analysis, "elements"):
|
|
364
|
+
# Extract complexity from elements for new architecture
|
|
365
|
+
methods = [
|
|
366
|
+
e
|
|
367
|
+
for e in file_analysis.elements
|
|
368
|
+
if hasattr(e, "complexity_score")
|
|
369
|
+
]
|
|
370
|
+
complexity = sum(
|
|
371
|
+
getattr(method, "complexity_score", 0) or 0
|
|
372
|
+
for method in methods
|
|
373
|
+
)
|
|
374
|
+
else:
|
|
375
|
+
complexity = 0
|
|
376
|
+
else:
|
|
377
|
+
# Use universal analyzer for other languages
|
|
378
|
+
request = AnalysisRequest(
|
|
379
|
+
file_path=str(file_path), language=language
|
|
380
|
+
)
|
|
381
|
+
file_analysis_result = await self.analysis_engine.analyze(
|
|
382
|
+
request
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
complexity = 0
|
|
386
|
+
if file_analysis_result and file_analysis_result.success:
|
|
387
|
+
analysis_dict = file_analysis_result.to_dict()
|
|
388
|
+
# Assuming complexity is part of the metrics in the new structure
|
|
389
|
+
if (
|
|
390
|
+
"metrics" in analysis_dict
|
|
391
|
+
and "complexity" in analysis_dict["metrics"]
|
|
392
|
+
):
|
|
393
|
+
complexity = analysis_dict["metrics"]["complexity"].get(
|
|
394
|
+
"total", 0
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
if complexity > 0:
|
|
398
|
+
complexity_data.append(
|
|
399
|
+
{
|
|
400
|
+
"file": str(file_path.relative_to(project_dir)),
|
|
401
|
+
"language": language,
|
|
402
|
+
"complexity": complexity,
|
|
403
|
+
}
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
total_complexity += complexity
|
|
407
|
+
max_complexity = max(max_complexity, complexity)
|
|
408
|
+
file_count += 1
|
|
409
|
+
|
|
410
|
+
except Exception as e:
|
|
411
|
+
logger.warning(f"Failed to analyze complexity for {file_path}: {e}")
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
# Calculate average complexity
|
|
415
|
+
avg_complexity = total_complexity / file_count if file_count > 0 else 0
|
|
416
|
+
|
|
417
|
+
complexity_stats = {
|
|
418
|
+
"average_complexity": round(avg_complexity, 2),
|
|
419
|
+
"max_complexity": max_complexity,
|
|
420
|
+
"total_files_analyzed": file_count,
|
|
421
|
+
"files_by_complexity": sorted(
|
|
422
|
+
complexity_data,
|
|
423
|
+
key=lambda x: int(cast(int, x.get("complexity", 0))),
|
|
424
|
+
reverse=True,
|
|
425
|
+
),
|
|
426
|
+
"last_updated": datetime.now().isoformat(),
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
logger.debug(f"Generated complexity stats for {file_count} files")
|
|
430
|
+
return complexity_stats
|
|
431
|
+
|
|
432
|
+
async def _generate_files_stats(self) -> dict[str, Any]:
|
|
433
|
+
"""
|
|
434
|
+
Generate file-level statistics
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Dictionary containing file statistics
|
|
438
|
+
"""
|
|
439
|
+
logger.debug("Generating file statistics")
|
|
440
|
+
|
|
441
|
+
# Get detailed file information
|
|
442
|
+
files_data = []
|
|
443
|
+
if self._project_path is None:
|
|
444
|
+
raise ValueError("Project path is not set")
|
|
445
|
+
project_dir = Path(self._project_path)
|
|
446
|
+
|
|
447
|
+
# Analyze each supported code file
|
|
448
|
+
for file_path in project_dir.rglob("*"):
|
|
449
|
+
if file_path.is_file() and self._is_supported_code_file(file_path):
|
|
450
|
+
try:
|
|
451
|
+
# Get file stats
|
|
452
|
+
file_stats = file_path.stat()
|
|
453
|
+
|
|
454
|
+
# Determine language using language detector
|
|
455
|
+
language = self._get_language_from_file(file_path)
|
|
456
|
+
|
|
457
|
+
# Count lines
|
|
458
|
+
try:
|
|
459
|
+
from ...encoding_utils import read_file_safe
|
|
460
|
+
|
|
461
|
+
content, _ = read_file_safe(file_path)
|
|
462
|
+
line_count = len(content.splitlines())
|
|
463
|
+
except Exception:
|
|
464
|
+
line_count = 0
|
|
465
|
+
|
|
466
|
+
files_data.append(
|
|
467
|
+
{
|
|
468
|
+
"path": str(file_path.relative_to(project_dir)),
|
|
469
|
+
"language": language,
|
|
470
|
+
"line_count": line_count,
|
|
471
|
+
"size_bytes": file_stats.st_size,
|
|
472
|
+
"modified": datetime.fromtimestamp(
|
|
473
|
+
file_stats.st_mtime
|
|
474
|
+
).isoformat(),
|
|
475
|
+
}
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
except Exception as e:
|
|
479
|
+
logger.warning(f"Failed to get stats for {file_path}: {e}")
|
|
480
|
+
continue
|
|
481
|
+
|
|
482
|
+
files_stats = {
|
|
483
|
+
"files": sorted(
|
|
484
|
+
files_data,
|
|
485
|
+
key=lambda x: int(cast(int, x.get("line_count", 0))),
|
|
486
|
+
reverse=True,
|
|
487
|
+
),
|
|
488
|
+
"total_count": len(files_data),
|
|
489
|
+
"last_updated": datetime.now().isoformat(),
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
logger.debug(f"Generated stats for {len(files_data)} files")
|
|
493
|
+
return files_stats
|
|
494
|
+
|
|
495
|
+
async def read_resource(self, uri: str) -> str:
|
|
496
|
+
"""
|
|
497
|
+
Read resource content from URI
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
uri: The resource URI to read
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
Resource content as JSON string
|
|
504
|
+
|
|
505
|
+
Raises:
|
|
506
|
+
ValueError: If URI format is invalid or stats type is unsupported
|
|
507
|
+
FileNotFoundError: If project path doesn't exist
|
|
508
|
+
"""
|
|
509
|
+
logger.debug(f"Reading resource: {uri}")
|
|
510
|
+
|
|
511
|
+
# Validate URI format
|
|
512
|
+
if not self.matches_uri(uri):
|
|
513
|
+
raise ValueError(f"URI does not match project stats pattern: {uri}")
|
|
514
|
+
|
|
515
|
+
# Extract statistics type
|
|
516
|
+
stats_type = self._extract_stats_type(uri)
|
|
517
|
+
|
|
518
|
+
# Validate statistics type
|
|
519
|
+
if stats_type not in self._supported_stats_types:
|
|
520
|
+
raise ValueError(
|
|
521
|
+
f"Unsupported statistics type: {stats_type}. "
|
|
522
|
+
f"Supported types: {', '.join(self._supported_stats_types)}"
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# Validate project path
|
|
526
|
+
self._validate_project_path()
|
|
527
|
+
|
|
528
|
+
# Generate statistics based on type
|
|
529
|
+
try:
|
|
530
|
+
if stats_type == "overview":
|
|
531
|
+
stats_data = await self._generate_overview_stats()
|
|
532
|
+
elif stats_type == "languages":
|
|
533
|
+
stats_data = await self._generate_languages_stats()
|
|
534
|
+
elif stats_type == "complexity":
|
|
535
|
+
stats_data = await self._generate_complexity_stats()
|
|
536
|
+
elif stats_type == "files":
|
|
537
|
+
stats_data = await self._generate_files_stats()
|
|
538
|
+
else:
|
|
539
|
+
raise ValueError(f"Unknown statistics type: {stats_type}")
|
|
540
|
+
|
|
541
|
+
# Convert to JSON
|
|
542
|
+
json_content = json.dumps(stats_data, indent=2, ensure_ascii=False)
|
|
543
|
+
logger.debug(f"Successfully generated {stats_type} statistics")
|
|
544
|
+
return json_content
|
|
545
|
+
|
|
546
|
+
except Exception as e:
|
|
547
|
+
logger.error(f"Failed to generate {stats_type} statistics: {e}")
|
|
548
|
+
raise
|
|
549
|
+
|
|
550
|
+
def get_supported_schemes(self) -> list[str]:
|
|
551
|
+
"""
|
|
552
|
+
Get list of supported URI schemes
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
List of supported schemes
|
|
556
|
+
"""
|
|
557
|
+
return ["code"]
|
|
558
|
+
|
|
559
|
+
def get_supported_resource_types(self) -> list[str]:
|
|
560
|
+
"""
|
|
561
|
+
Get list of supported resource types
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
List of supported resource types
|
|
565
|
+
"""
|
|
566
|
+
return ["stats"]
|
|
567
|
+
|
|
568
|
+
def get_supported_stats_types(self) -> list[str]:
|
|
569
|
+
"""
|
|
570
|
+
Get list of supported statistics types
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
List of supported statistics types
|
|
574
|
+
"""
|
|
575
|
+
return list(self._supported_stats_types)
|
|
576
|
+
|
|
577
|
+
def __str__(self) -> str:
|
|
578
|
+
"""String representation of the resource"""
|
|
579
|
+
return "ProjectStatsResource(pattern=code://stats/{stats_type})"
|
|
580
|
+
|
|
581
|
+
def __repr__(self) -> str:
|
|
582
|
+
"""Detailed string representation of the resource"""
|
|
583
|
+
return (
|
|
584
|
+
f"ProjectStatsResource(uri_pattern={self._uri_pattern.pattern}, "
|
|
585
|
+
f"project_path={self._project_path})"
|
|
586
|
+
)
|