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