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.
Files changed (149) hide show
  1. tree_sitter_analyzer/__init__.py +132 -0
  2. tree_sitter_analyzer/__main__.py +11 -0
  3. tree_sitter_analyzer/api.py +853 -0
  4. tree_sitter_analyzer/cli/__init__.py +39 -0
  5. tree_sitter_analyzer/cli/__main__.py +12 -0
  6. tree_sitter_analyzer/cli/argument_validator.py +89 -0
  7. tree_sitter_analyzer/cli/commands/__init__.py +26 -0
  8. tree_sitter_analyzer/cli/commands/advanced_command.py +226 -0
  9. tree_sitter_analyzer/cli/commands/base_command.py +181 -0
  10. tree_sitter_analyzer/cli/commands/default_command.py +18 -0
  11. tree_sitter_analyzer/cli/commands/find_and_grep_cli.py +188 -0
  12. tree_sitter_analyzer/cli/commands/list_files_cli.py +133 -0
  13. tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -0
  14. tree_sitter_analyzer/cli/commands/query_command.py +109 -0
  15. tree_sitter_analyzer/cli/commands/search_content_cli.py +161 -0
  16. tree_sitter_analyzer/cli/commands/structure_command.py +156 -0
  17. tree_sitter_analyzer/cli/commands/summary_command.py +116 -0
  18. tree_sitter_analyzer/cli/commands/table_command.py +414 -0
  19. tree_sitter_analyzer/cli/info_commands.py +124 -0
  20. tree_sitter_analyzer/cli_main.py +472 -0
  21. tree_sitter_analyzer/constants.py +85 -0
  22. tree_sitter_analyzer/core/__init__.py +15 -0
  23. tree_sitter_analyzer/core/analysis_engine.py +580 -0
  24. tree_sitter_analyzer/core/cache_service.py +333 -0
  25. tree_sitter_analyzer/core/engine.py +585 -0
  26. tree_sitter_analyzer/core/parser.py +293 -0
  27. tree_sitter_analyzer/core/query.py +605 -0
  28. tree_sitter_analyzer/core/query_filter.py +200 -0
  29. tree_sitter_analyzer/core/query_service.py +340 -0
  30. tree_sitter_analyzer/encoding_utils.py +530 -0
  31. tree_sitter_analyzer/exceptions.py +747 -0
  32. tree_sitter_analyzer/file_handler.py +246 -0
  33. tree_sitter_analyzer/formatters/__init__.py +1 -0
  34. tree_sitter_analyzer/formatters/base_formatter.py +201 -0
  35. tree_sitter_analyzer/formatters/csharp_formatter.py +367 -0
  36. tree_sitter_analyzer/formatters/formatter_config.py +197 -0
  37. tree_sitter_analyzer/formatters/formatter_factory.py +84 -0
  38. tree_sitter_analyzer/formatters/formatter_registry.py +377 -0
  39. tree_sitter_analyzer/formatters/formatter_selector.py +96 -0
  40. tree_sitter_analyzer/formatters/go_formatter.py +368 -0
  41. tree_sitter_analyzer/formatters/html_formatter.py +498 -0
  42. tree_sitter_analyzer/formatters/java_formatter.py +423 -0
  43. tree_sitter_analyzer/formatters/javascript_formatter.py +611 -0
  44. tree_sitter_analyzer/formatters/kotlin_formatter.py +268 -0
  45. tree_sitter_analyzer/formatters/language_formatter_factory.py +123 -0
  46. tree_sitter_analyzer/formatters/legacy_formatter_adapters.py +228 -0
  47. tree_sitter_analyzer/formatters/markdown_formatter.py +725 -0
  48. tree_sitter_analyzer/formatters/php_formatter.py +301 -0
  49. tree_sitter_analyzer/formatters/python_formatter.py +830 -0
  50. tree_sitter_analyzer/formatters/ruby_formatter.py +278 -0
  51. tree_sitter_analyzer/formatters/rust_formatter.py +233 -0
  52. tree_sitter_analyzer/formatters/sql_formatter_wrapper.py +689 -0
  53. tree_sitter_analyzer/formatters/sql_formatters.py +536 -0
  54. tree_sitter_analyzer/formatters/typescript_formatter.py +543 -0
  55. tree_sitter_analyzer/formatters/yaml_formatter.py +462 -0
  56. tree_sitter_analyzer/interfaces/__init__.py +9 -0
  57. tree_sitter_analyzer/interfaces/cli.py +535 -0
  58. tree_sitter_analyzer/interfaces/cli_adapter.py +359 -0
  59. tree_sitter_analyzer/interfaces/mcp_adapter.py +224 -0
  60. tree_sitter_analyzer/interfaces/mcp_server.py +428 -0
  61. tree_sitter_analyzer/language_detector.py +553 -0
  62. tree_sitter_analyzer/language_loader.py +271 -0
  63. tree_sitter_analyzer/languages/__init__.py +10 -0
  64. tree_sitter_analyzer/languages/csharp_plugin.py +1076 -0
  65. tree_sitter_analyzer/languages/css_plugin.py +449 -0
  66. tree_sitter_analyzer/languages/go_plugin.py +836 -0
  67. tree_sitter_analyzer/languages/html_plugin.py +496 -0
  68. tree_sitter_analyzer/languages/java_plugin.py +1299 -0
  69. tree_sitter_analyzer/languages/javascript_plugin.py +1622 -0
  70. tree_sitter_analyzer/languages/kotlin_plugin.py +656 -0
  71. tree_sitter_analyzer/languages/markdown_plugin.py +1928 -0
  72. tree_sitter_analyzer/languages/php_plugin.py +862 -0
  73. tree_sitter_analyzer/languages/python_plugin.py +1636 -0
  74. tree_sitter_analyzer/languages/ruby_plugin.py +757 -0
  75. tree_sitter_analyzer/languages/rust_plugin.py +673 -0
  76. tree_sitter_analyzer/languages/sql_plugin.py +2444 -0
  77. tree_sitter_analyzer/languages/typescript_plugin.py +1892 -0
  78. tree_sitter_analyzer/languages/yaml_plugin.py +695 -0
  79. tree_sitter_analyzer/legacy_table_formatter.py +860 -0
  80. tree_sitter_analyzer/mcp/__init__.py +34 -0
  81. tree_sitter_analyzer/mcp/resources/__init__.py +43 -0
  82. tree_sitter_analyzer/mcp/resources/code_file_resource.py +208 -0
  83. tree_sitter_analyzer/mcp/resources/project_stats_resource.py +586 -0
  84. tree_sitter_analyzer/mcp/server.py +869 -0
  85. tree_sitter_analyzer/mcp/tools/__init__.py +28 -0
  86. tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +779 -0
  87. tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +291 -0
  88. tree_sitter_analyzer/mcp/tools/base_tool.py +139 -0
  89. tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +816 -0
  90. tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +686 -0
  91. tree_sitter_analyzer/mcp/tools/list_files_tool.py +413 -0
  92. tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
  93. tree_sitter_analyzer/mcp/tools/query_tool.py +443 -0
  94. tree_sitter_analyzer/mcp/tools/read_partial_tool.py +464 -0
  95. tree_sitter_analyzer/mcp/tools/search_content_tool.py +836 -0
  96. tree_sitter_analyzer/mcp/tools/table_format_tool.py +572 -0
  97. tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +653 -0
  98. tree_sitter_analyzer/mcp/utils/__init__.py +113 -0
  99. tree_sitter_analyzer/mcp/utils/error_handler.py +569 -0
  100. tree_sitter_analyzer/mcp/utils/file_output_factory.py +217 -0
  101. tree_sitter_analyzer/mcp/utils/file_output_manager.py +322 -0
  102. tree_sitter_analyzer/mcp/utils/gitignore_detector.py +358 -0
  103. tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -0
  104. tree_sitter_analyzer/mcp/utils/search_cache.py +343 -0
  105. tree_sitter_analyzer/models.py +840 -0
  106. tree_sitter_analyzer/mypy_current_errors.txt +2 -0
  107. tree_sitter_analyzer/output_manager.py +255 -0
  108. tree_sitter_analyzer/platform_compat/__init__.py +3 -0
  109. tree_sitter_analyzer/platform_compat/adapter.py +324 -0
  110. tree_sitter_analyzer/platform_compat/compare.py +224 -0
  111. tree_sitter_analyzer/platform_compat/detector.py +67 -0
  112. tree_sitter_analyzer/platform_compat/fixtures.py +228 -0
  113. tree_sitter_analyzer/platform_compat/profiles.py +217 -0
  114. tree_sitter_analyzer/platform_compat/record.py +55 -0
  115. tree_sitter_analyzer/platform_compat/recorder.py +155 -0
  116. tree_sitter_analyzer/platform_compat/report.py +92 -0
  117. tree_sitter_analyzer/plugins/__init__.py +280 -0
  118. tree_sitter_analyzer/plugins/base.py +647 -0
  119. tree_sitter_analyzer/plugins/manager.py +384 -0
  120. tree_sitter_analyzer/project_detector.py +328 -0
  121. tree_sitter_analyzer/queries/__init__.py +27 -0
  122. tree_sitter_analyzer/queries/csharp.py +216 -0
  123. tree_sitter_analyzer/queries/css.py +615 -0
  124. tree_sitter_analyzer/queries/go.py +275 -0
  125. tree_sitter_analyzer/queries/html.py +543 -0
  126. tree_sitter_analyzer/queries/java.py +402 -0
  127. tree_sitter_analyzer/queries/javascript.py +724 -0
  128. tree_sitter_analyzer/queries/kotlin.py +192 -0
  129. tree_sitter_analyzer/queries/markdown.py +258 -0
  130. tree_sitter_analyzer/queries/php.py +95 -0
  131. tree_sitter_analyzer/queries/python.py +859 -0
  132. tree_sitter_analyzer/queries/ruby.py +92 -0
  133. tree_sitter_analyzer/queries/rust.py +223 -0
  134. tree_sitter_analyzer/queries/sql.py +555 -0
  135. tree_sitter_analyzer/queries/typescript.py +871 -0
  136. tree_sitter_analyzer/queries/yaml.py +236 -0
  137. tree_sitter_analyzer/query_loader.py +272 -0
  138. tree_sitter_analyzer/security/__init__.py +22 -0
  139. tree_sitter_analyzer/security/boundary_manager.py +277 -0
  140. tree_sitter_analyzer/security/regex_checker.py +297 -0
  141. tree_sitter_analyzer/security/validator.py +599 -0
  142. tree_sitter_analyzer/table_formatter.py +782 -0
  143. tree_sitter_analyzer/utils/__init__.py +53 -0
  144. tree_sitter_analyzer/utils/logging.py +433 -0
  145. tree_sitter_analyzer/utils/tree_sitter_compat.py +289 -0
  146. tree_sitter_analyzer-1.9.17.1.dist-info/METADATA +485 -0
  147. tree_sitter_analyzer-1.9.17.1.dist-info/RECORD +149 -0
  148. tree_sitter_analyzer-1.9.17.1.dist-info/WHEEL +4 -0
  149. 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
+ )