tree-sitter-analyzer 0.9.2__py3-none-any.whl → 0.9.4__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 (37) hide show
  1. tree_sitter_analyzer/__init__.py +1 -1
  2. tree_sitter_analyzer/cli/commands/base_command.py +2 -3
  3. tree_sitter_analyzer/cli/commands/default_command.py +18 -18
  4. tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -141
  5. tree_sitter_analyzer/cli/commands/query_command.py +92 -88
  6. tree_sitter_analyzer/cli/commands/table_command.py +235 -235
  7. tree_sitter_analyzer/cli/info_commands.py +121 -121
  8. tree_sitter_analyzer/cli_main.py +307 -303
  9. tree_sitter_analyzer/core/analysis_engine.py +584 -576
  10. tree_sitter_analyzer/core/cache_service.py +6 -5
  11. tree_sitter_analyzer/core/query.py +502 -502
  12. tree_sitter_analyzer/encoding_utils.py +6 -2
  13. tree_sitter_analyzer/exceptions.py +400 -406
  14. tree_sitter_analyzer/formatters/java_formatter.py +291 -291
  15. tree_sitter_analyzer/formatters/python_formatter.py +259 -259
  16. tree_sitter_analyzer/interfaces/cli.py +1 -1
  17. tree_sitter_analyzer/interfaces/cli_adapter.py +3 -3
  18. tree_sitter_analyzer/interfaces/mcp_server.py +426 -425
  19. tree_sitter_analyzer/language_detector.py +398 -398
  20. tree_sitter_analyzer/language_loader.py +224 -224
  21. tree_sitter_analyzer/languages/java_plugin.py +1202 -1202
  22. tree_sitter_analyzer/mcp/resources/project_stats_resource.py +559 -555
  23. tree_sitter_analyzer/mcp/server.py +30 -9
  24. tree_sitter_analyzer/mcp/tools/read_partial_tool.py +21 -4
  25. tree_sitter_analyzer/mcp/tools/table_format_tool.py +22 -4
  26. tree_sitter_analyzer/mcp/utils/error_handler.py +569 -567
  27. tree_sitter_analyzer/models.py +470 -470
  28. tree_sitter_analyzer/project_detector.py +330 -317
  29. tree_sitter_analyzer/security/__init__.py +22 -22
  30. tree_sitter_analyzer/security/boundary_manager.py +243 -237
  31. tree_sitter_analyzer/security/regex_checker.py +297 -292
  32. tree_sitter_analyzer/table_formatter.py +703 -652
  33. tree_sitter_analyzer/utils.py +53 -22
  34. {tree_sitter_analyzer-0.9.2.dist-info → tree_sitter_analyzer-0.9.4.dist-info}/METADATA +13 -13
  35. {tree_sitter_analyzer-0.9.2.dist-info → tree_sitter_analyzer-0.9.4.dist-info}/RECORD +37 -37
  36. {tree_sitter_analyzer-0.9.2.dist-info → tree_sitter_analyzer-0.9.4.dist-info}/WHEEL +0 -0
  37. {tree_sitter_analyzer-0.9.2.dist-info → tree_sitter_analyzer-0.9.4.dist-info}/entry_points.txt +0 -0
@@ -1,555 +1,559 @@
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
- )
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 as e:
211
+ logger.debug(
212
+ f"Skipping unreadable file during overview scan: {file_path} ({e})"
213
+ )
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 as e:
277
+ logger.debug(f"Failed to count lines for {file_path}: {e}")
278
+ file_lines = 0
279
+
280
+ language = self._get_language_from_file(file_path)
281
+ if language != "unknown":
282
+ if language not in language_data:
283
+ language_data[language] = {"file_count": 0, "line_count": 0}
284
+ language_data[language]["file_count"] += 1
285
+ language_data[language]["line_count"] += file_lines
286
+
287
+ # Convert to list format and calculate percentages
288
+ languages_list = []
289
+ for lang, data in language_data.items():
290
+ percentage = (
291
+ round((data["line_count"] / total_lines) * 100, 2)
292
+ if total_lines > 0
293
+ else 0.0
294
+ )
295
+ languages_list.append(
296
+ {
297
+ "name": lang,
298
+ "file_count": data["file_count"],
299
+ "line_count": data["line_count"],
300
+ "percentage": percentage,
301
+ }
302
+ )
303
+
304
+ languages_stats = {
305
+ "languages": languages_list,
306
+ "total_languages": len(languages_list),
307
+ "last_updated": datetime.now().isoformat(),
308
+ }
309
+
310
+ logger.debug(f"Generated stats for {len(languages_list)} languages")
311
+ return languages_stats
312
+
313
+ async def _generate_complexity_stats(self) -> dict[str, Any]:
314
+ """
315
+ Generate complexity statistics
316
+
317
+ Returns:
318
+ Dictionary containing complexity statistics
319
+ """
320
+ logger.debug("Generating complexity statistics")
321
+
322
+ # Analyze files for complexity
323
+ if self._project_path is None:
324
+ raise ValueError("Project path is not set")
325
+ project_dir = Path(self._project_path)
326
+ complexity_data = []
327
+ total_complexity = 0
328
+ max_complexity = 0
329
+ file_count = 0
330
+
331
+ # Analyze each supported code file
332
+ for file_path in project_dir.rglob("*"):
333
+ if file_path.is_file() and self._is_supported_code_file(file_path):
334
+ try:
335
+ language = self._get_language_from_file(file_path)
336
+
337
+ # Use appropriate analyzer based on language
338
+ if language == "java":
339
+ # Use advanced analyzer for Java
340
+ # 使用 await 调用异步方法
341
+ file_analysis = await self._advanced_analyzer.analyze_file(
342
+ str(file_path)
343
+ )
344
+ if file_analysis and hasattr(file_analysis, "methods"):
345
+ complexity = sum(
346
+ method.complexity_score or 0
347
+ for method in file_analysis.methods
348
+ )
349
+ else:
350
+ complexity = 0
351
+ else:
352
+ # Use universal analyzer for other languages
353
+ request = AnalysisRequest(
354
+ file_path=str(file_path), language=language
355
+ )
356
+ file_analysis_result = await self.analysis_engine.analyze(
357
+ request
358
+ )
359
+
360
+ complexity = 0
361
+ if file_analysis_result and file_analysis_result.success:
362
+ analysis_dict = file_analysis_result.to_dict()
363
+ # Assuming complexity is part of the metrics in the new structure
364
+ if (
365
+ "metrics" in analysis_dict
366
+ and "complexity" in analysis_dict["metrics"]
367
+ ):
368
+ complexity = analysis_dict["metrics"]["complexity"].get(
369
+ "total", 0
370
+ )
371
+
372
+ if complexity > 0:
373
+ complexity_data.append(
374
+ {
375
+ "file": str(file_path.relative_to(project_dir)),
376
+ "language": language,
377
+ "complexity": complexity,
378
+ }
379
+ )
380
+
381
+ total_complexity += complexity
382
+ max_complexity = max(max_complexity, complexity)
383
+ file_count += 1
384
+
385
+ except Exception as e:
386
+ logger.warning(f"Failed to analyze complexity for {file_path}: {e}")
387
+ continue
388
+
389
+ # Calculate average complexity
390
+ avg_complexity = total_complexity / file_count if file_count > 0 else 0
391
+
392
+ complexity_stats = {
393
+ "average_complexity": round(avg_complexity, 2),
394
+ "max_complexity": max_complexity,
395
+ "total_files_analyzed": file_count,
396
+ "files_by_complexity": sorted(
397
+ complexity_data,
398
+ key=lambda x: int(cast(int, x.get("complexity", 0))),
399
+ reverse=True,
400
+ ),
401
+ "last_updated": datetime.now().isoformat(),
402
+ }
403
+
404
+ logger.debug(f"Generated complexity stats for {file_count} files")
405
+ return complexity_stats
406
+
407
+ async def _generate_files_stats(self) -> dict[str, Any]:
408
+ """
409
+ Generate file-level statistics
410
+
411
+ Returns:
412
+ Dictionary containing file statistics
413
+ """
414
+ logger.debug("Generating file statistics")
415
+
416
+ # Get detailed file information
417
+ files_data = []
418
+ if self._project_path is None:
419
+ raise ValueError("Project path is not set")
420
+ project_dir = Path(self._project_path)
421
+
422
+ # Analyze each supported code file
423
+ for file_path in project_dir.rglob("*"):
424
+ if file_path.is_file() and self._is_supported_code_file(file_path):
425
+ try:
426
+ # Get file stats
427
+ file_stats = file_path.stat()
428
+
429
+ # Determine language using language detector
430
+ language = self._get_language_from_file(file_path)
431
+
432
+ # Count lines
433
+ try:
434
+ with open(file_path, encoding="utf-8") as f:
435
+ line_count = sum(1 for _ in f)
436
+ except Exception:
437
+ line_count = 0
438
+
439
+ files_data.append(
440
+ {
441
+ "path": str(file_path.relative_to(project_dir)),
442
+ "language": language,
443
+ "line_count": line_count,
444
+ "size_bytes": file_stats.st_size,
445
+ "modified": datetime.fromtimestamp(
446
+ file_stats.st_mtime
447
+ ).isoformat(),
448
+ }
449
+ )
450
+
451
+ except Exception as e:
452
+ logger.warning(f"Failed to get stats for {file_path}: {e}")
453
+ continue
454
+
455
+ files_stats = {
456
+ "files": sorted(
457
+ files_data,
458
+ key=lambda x: int(cast(int, x.get("line_count", 0))),
459
+ reverse=True,
460
+ ),
461
+ "total_count": len(files_data),
462
+ "last_updated": datetime.now().isoformat(),
463
+ }
464
+
465
+ logger.debug(f"Generated stats for {len(files_data)} files")
466
+ return files_stats
467
+
468
+ async def read_resource(self, uri: str) -> str:
469
+ """
470
+ Read resource content from URI
471
+
472
+ Args:
473
+ uri: The resource URI to read
474
+
475
+ Returns:
476
+ Resource content as JSON string
477
+
478
+ Raises:
479
+ ValueError: If URI format is invalid or stats type is unsupported
480
+ FileNotFoundError: If project path doesn't exist
481
+ """
482
+ logger.debug(f"Reading resource: {uri}")
483
+
484
+ # Validate URI format
485
+ if not self.matches_uri(uri):
486
+ raise ValueError(f"URI does not match project stats pattern: {uri}")
487
+
488
+ # Extract statistics type
489
+ stats_type = self._extract_stats_type(uri)
490
+
491
+ # Validate statistics type
492
+ if stats_type not in self._supported_stats_types:
493
+ raise ValueError(
494
+ f"Unsupported statistics type: {stats_type}. "
495
+ f"Supported types: {', '.join(self._supported_stats_types)}"
496
+ )
497
+
498
+ # Validate project path
499
+ self._validate_project_path()
500
+
501
+ # Generate statistics based on type
502
+ try:
503
+ if stats_type == "overview":
504
+ stats_data = await self._generate_overview_stats()
505
+ elif stats_type == "languages":
506
+ stats_data = await self._generate_languages_stats()
507
+ elif stats_type == "complexity":
508
+ stats_data = await self._generate_complexity_stats()
509
+ elif stats_type == "files":
510
+ stats_data = await self._generate_files_stats()
511
+ else:
512
+ raise ValueError(f"Unknown statistics type: {stats_type}")
513
+
514
+ # Convert to JSON
515
+ json_content = json.dumps(stats_data, indent=2, ensure_ascii=False)
516
+ logger.debug(f"Successfully generated {stats_type} statistics")
517
+ return json_content
518
+
519
+ except Exception as e:
520
+ logger.error(f"Failed to generate {stats_type} statistics: {e}")
521
+ raise
522
+
523
+ def get_supported_schemes(self) -> list[str]:
524
+ """
525
+ Get list of supported URI schemes
526
+
527
+ Returns:
528
+ List of supported schemes
529
+ """
530
+ return ["code"]
531
+
532
+ def get_supported_resource_types(self) -> list[str]:
533
+ """
534
+ Get list of supported resource types
535
+
536
+ Returns:
537
+ List of supported resource types
538
+ """
539
+ return ["stats"]
540
+
541
+ def get_supported_stats_types(self) -> list[str]:
542
+ """
543
+ Get list of supported statistics types
544
+
545
+ Returns:
546
+ List of supported statistics types
547
+ """
548
+ return list(self._supported_stats_types)
549
+
550
+ def __str__(self) -> str:
551
+ """String representation of the resource"""
552
+ return "ProjectStatsResource(pattern=code://stats/{stats_type})"
553
+
554
+ def __repr__(self) -> str:
555
+ """Detailed string representation of the resource"""
556
+ return (
557
+ f"ProjectStatsResource(uri_pattern={self._uri_pattern.pattern}, "
558
+ f"project_path={self._project_path})"
559
+ )