code-analyser 0.1.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.
- code_analyser-0.1.0.dist-info/METADATA +283 -0
- code_analyser-0.1.0.dist-info/RECORD +34 -0
- code_analyser-0.1.0.dist-info/WHEEL +4 -0
- code_analyser-0.1.0.dist-info/licenses/LICENSE +21 -0
- codelens/__init__.py +7 -0
- codelens/__main__.py +19 -0
- codelens/analyzers/__init__.py +30 -0
- codelens/analyzers/base.py +139 -0
- codelens/analyzers/manager.py +207 -0
- codelens/analyzers/python_analyzer.py +344 -0
- codelens/analyzers/similarity_analyzer.py +512 -0
- codelens/api/__init__.py +1 -0
- codelens/api/routes/__init__.py +1 -0
- codelens/api/routes/analysis.py +441 -0
- codelens/api/routes/reports.py +438 -0
- codelens/api/routes/rubrics.py +349 -0
- codelens/api/schemas.py +305 -0
- codelens/cli.py +297 -0
- codelens/core/__init__.py +1 -0
- codelens/core/config.py +91 -0
- codelens/db/__init__.py +1 -0
- codelens/db/database.py +57 -0
- codelens/main.py +111 -0
- codelens/models/__init__.py +14 -0
- codelens/models/assignments.py +105 -0
- codelens/models/reports.py +172 -0
- codelens/models/rubrics.py +76 -0
- codelens/services/__init__.py +37 -0
- codelens/services/batch_processor.py +508 -0
- codelens/services/code_executor.py +310 -0
- codelens/services/sandbox.py +375 -0
- codelens/services/similarity_service.py +449 -0
- codelens/utils/__init__.py +29 -0
- codelens/utils/helpers.py +217 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analyzer manager for orchestrating different code analyzers
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import structlog
|
|
8
|
+
|
|
9
|
+
from codelens.core.config import settings
|
|
10
|
+
|
|
11
|
+
from .base import AnalysisIssue, AnalysisResult, BaseAnalyzer, CodeMetrics, Severity
|
|
12
|
+
from .python_analyzer import PythonAnalyzer
|
|
13
|
+
|
|
14
|
+
logger = structlog.get_logger()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AnalyzerManager:
|
|
18
|
+
"""Manages and orchestrates different code analyzers"""
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self.analyzers: dict[str, BaseAnalyzer] = {}
|
|
22
|
+
self._initialize_analyzers()
|
|
23
|
+
|
|
24
|
+
def _initialize_analyzers(self) -> None:
|
|
25
|
+
"""Initialize available analyzers based on configuration"""
|
|
26
|
+
|
|
27
|
+
# Python analyzer
|
|
28
|
+
python_config = {
|
|
29
|
+
"ruff_enabled": settings.analyzer.ruff_enabled,
|
|
30
|
+
"mypy_enabled": settings.analyzer.mypy_enabled,
|
|
31
|
+
"ruff_config": settings.analyzer.ruff_config,
|
|
32
|
+
"mypy_config": settings.analyzer.mypy_config,
|
|
33
|
+
"max_line_length": settings.analyzer.max_line_length,
|
|
34
|
+
"check_type_hints": settings.analyzer.check_type_hints,
|
|
35
|
+
"check_docstrings": settings.analyzer.check_docstrings,
|
|
36
|
+
}
|
|
37
|
+
self.analyzers["python"] = PythonAnalyzer(python_config)
|
|
38
|
+
|
|
39
|
+
logger.info("Initialized analyzers", analyzers=list(self.analyzers.keys()))
|
|
40
|
+
|
|
41
|
+
async def analyze_code(
|
|
42
|
+
self,
|
|
43
|
+
code: str,
|
|
44
|
+
language: str,
|
|
45
|
+
file_path: str = "temp",
|
|
46
|
+
analyzer_config: dict[str, Any] | None = None
|
|
47
|
+
) -> AnalysisResult:
|
|
48
|
+
"""
|
|
49
|
+
Analyze code using the appropriate analyzer for the language
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
code: Source code to analyze
|
|
53
|
+
language: Programming language (python, javascript, etc.)
|
|
54
|
+
file_path: Optional file path for context
|
|
55
|
+
analyzer_config: Optional analyzer-specific configuration
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
AnalysisResult with combined results from all applicable analyzers
|
|
59
|
+
"""
|
|
60
|
+
language = language.lower()
|
|
61
|
+
|
|
62
|
+
if language not in self.analyzers:
|
|
63
|
+
logger.warning("Unsupported language", language=language)
|
|
64
|
+
return AnalysisResult(
|
|
65
|
+
success=False,
|
|
66
|
+
issues=[AnalysisIssue(
|
|
67
|
+
line=1,
|
|
68
|
+
severity=Severity.ERROR,
|
|
69
|
+
code="UNSUPPORTED_LANGUAGE",
|
|
70
|
+
message=f"Language '{language}' is not supported",
|
|
71
|
+
category="system"
|
|
72
|
+
)],
|
|
73
|
+
metrics=CodeMetrics(),
|
|
74
|
+
analyzer_version="AnalyzerManager"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Get the appropriate analyzer
|
|
78
|
+
analyzer = self.analyzers[language]
|
|
79
|
+
|
|
80
|
+
# Override analyzer configuration if provided
|
|
81
|
+
if analyzer_config:
|
|
82
|
+
# Create a new analyzer instance with custom config
|
|
83
|
+
if language == "python":
|
|
84
|
+
analyzer = PythonAnalyzer(analyzer_config)
|
|
85
|
+
# Add other language analyzers here when implemented
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
logger.info("Starting code analysis", language=language, file_path=file_path)
|
|
89
|
+
result = await analyzer.analyze(code, file_path)
|
|
90
|
+
logger.info("Analysis completed",
|
|
91
|
+
language=language,
|
|
92
|
+
success=result.success,
|
|
93
|
+
issue_count=len(result.issues),
|
|
94
|
+
execution_time=result.execution_time)
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.error("Analysis failed", language=language, error=str(e))
|
|
99
|
+
return AnalysisResult(
|
|
100
|
+
success=False,
|
|
101
|
+
issues=[AnalysisIssue(
|
|
102
|
+
line=1,
|
|
103
|
+
severity=Severity.ERROR,
|
|
104
|
+
code="ANALYSIS_ERROR",
|
|
105
|
+
message=f"Analysis failed: {str(e)}",
|
|
106
|
+
category="system"
|
|
107
|
+
)],
|
|
108
|
+
metrics=CodeMetrics(),
|
|
109
|
+
analyzer_version=analyzer.get_version()
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def analyze_batch(
|
|
113
|
+
self,
|
|
114
|
+
files: list[dict[str, str | dict[str, Any]]],
|
|
115
|
+
language: str,
|
|
116
|
+
analyzer_config: dict[str, Any] | None = None
|
|
117
|
+
) -> list[AnalysisResult]:
|
|
118
|
+
"""
|
|
119
|
+
Analyze multiple files in batch
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
files: List of file dictionaries with 'code', 'path' keys
|
|
123
|
+
language: Programming language
|
|
124
|
+
analyzer_config: Optional analyzer configuration
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
List of AnalysisResult objects
|
|
128
|
+
"""
|
|
129
|
+
results = []
|
|
130
|
+
|
|
131
|
+
for file_info in files:
|
|
132
|
+
code = file_info.get("code", "")
|
|
133
|
+
file_path = file_info.get("path", "unknown")
|
|
134
|
+
|
|
135
|
+
# Ensure we have strings, not dict/Any
|
|
136
|
+
if not isinstance(code, str):
|
|
137
|
+
code = str(code)
|
|
138
|
+
if not isinstance(file_path, str):
|
|
139
|
+
file_path = str(file_path)
|
|
140
|
+
|
|
141
|
+
result = await self.analyze_code(
|
|
142
|
+
code=code,
|
|
143
|
+
language=language,
|
|
144
|
+
file_path=file_path,
|
|
145
|
+
analyzer_config=analyzer_config
|
|
146
|
+
)
|
|
147
|
+
results.append(result)
|
|
148
|
+
|
|
149
|
+
return results
|
|
150
|
+
|
|
151
|
+
def get_supported_languages(self) -> list[str]:
|
|
152
|
+
"""Get list of supported programming languages"""
|
|
153
|
+
return list(self.analyzers.keys())
|
|
154
|
+
|
|
155
|
+
def get_analyzer_info(self, language: str) -> dict[str, Any] | None:
|
|
156
|
+
"""Get information about a specific analyzer"""
|
|
157
|
+
if language not in self.analyzers:
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
analyzer = self.analyzers[language]
|
|
161
|
+
return {
|
|
162
|
+
"language": language,
|
|
163
|
+
"version": analyzer.get_version(),
|
|
164
|
+
"name": analyzer.name,
|
|
165
|
+
"config": analyzer.config
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
def get_all_analyzer_info(self) -> dict[str, dict[str, Any]]:
|
|
169
|
+
"""Get information about all available analyzers"""
|
|
170
|
+
info = {}
|
|
171
|
+
for language in self.analyzers:
|
|
172
|
+
analyzer_info = self.get_analyzer_info(language)
|
|
173
|
+
if analyzer_info:
|
|
174
|
+
info[language] = analyzer_info
|
|
175
|
+
return info
|
|
176
|
+
|
|
177
|
+
def update_analyzer_config(self, language: str, config: dict[str, Any]) -> bool:
|
|
178
|
+
"""
|
|
179
|
+
Update configuration for a specific analyzer
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
language: Programming language
|
|
183
|
+
config: New configuration dictionary
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
True if successful, False otherwise
|
|
187
|
+
"""
|
|
188
|
+
if language not in self.analyzers:
|
|
189
|
+
logger.warning("Cannot update config for unsupported language", language=language)
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
# Create new analyzer instance with updated config
|
|
194
|
+
if language == "python":
|
|
195
|
+
self.analyzers[language] = PythonAnalyzer(config)
|
|
196
|
+
# Add other languages here
|
|
197
|
+
|
|
198
|
+
logger.info("Updated analyzer config", language=language)
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
except Exception as e:
|
|
202
|
+
logger.error("Failed to update analyzer config", language=language, error=str(e))
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# Global analyzer manager instance
|
|
207
|
+
analyzer_manager = AnalyzerManager()
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python code analyzer using ruff and mypy
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import structlog
|
|
14
|
+
|
|
15
|
+
from .base import AnalysisIssue, AnalysisResult, BaseAnalyzer, Severity
|
|
16
|
+
|
|
17
|
+
logger = structlog.get_logger()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PythonAnalyzer(BaseAnalyzer):
|
|
21
|
+
"""Python code analyzer using ruff (linting/formatting) and mypy (type checking)"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, config: dict[str, Any] | None = None):
|
|
24
|
+
super().__init__(config)
|
|
25
|
+
|
|
26
|
+
# Default configuration
|
|
27
|
+
self.ruff_enabled = self.config.get("ruff_enabled", True)
|
|
28
|
+
self.mypy_enabled = self.config.get("mypy_enabled", True)
|
|
29
|
+
self.ruff_config = self.config.get("ruff_config") # Path to ruff config file
|
|
30
|
+
self.mypy_config = self.config.get("mypy_config") # Path to mypy config file
|
|
31
|
+
|
|
32
|
+
# Analysis options
|
|
33
|
+
self.max_line_length = self.config.get("max_line_length", 88)
|
|
34
|
+
self.check_type_hints = self.config.get("check_type_hints", True)
|
|
35
|
+
self.check_docstrings = self.config.get("check_docstrings", True)
|
|
36
|
+
|
|
37
|
+
async def analyze(self, code: str, file_path: str = "temp.py") -> AnalysisResult:
|
|
38
|
+
"""Analyze Python code using ruff and mypy"""
|
|
39
|
+
start_time = time.time()
|
|
40
|
+
|
|
41
|
+
issues: list[AnalysisIssue] = []
|
|
42
|
+
metrics = self.calculate_basic_metrics(code)
|
|
43
|
+
|
|
44
|
+
# Create temporary file for analysis
|
|
45
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_file:
|
|
46
|
+
temp_file.write(code)
|
|
47
|
+
temp_file_path = temp_file.name
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
# Run ruff analysis
|
|
51
|
+
if self.ruff_enabled:
|
|
52
|
+
ruff_issues = await self._run_ruff(temp_file_path, code)
|
|
53
|
+
issues.extend(ruff_issues)
|
|
54
|
+
|
|
55
|
+
# Run mypy analysis
|
|
56
|
+
if self.mypy_enabled and self.check_type_hints:
|
|
57
|
+
mypy_issues = await self._run_mypy(temp_file_path, code)
|
|
58
|
+
issues.extend(mypy_issues)
|
|
59
|
+
|
|
60
|
+
# Calculate advanced metrics
|
|
61
|
+
advanced_metrics = await self._calculate_advanced_metrics(code)
|
|
62
|
+
metrics.cyclomatic_complexity = advanced_metrics.get("cyclomatic_complexity", 0)
|
|
63
|
+
metrics.cognitive_complexity = advanced_metrics.get("cognitive_complexity", 0)
|
|
64
|
+
metrics.maintainability_index = advanced_metrics.get("maintainability_index", 0.0)
|
|
65
|
+
|
|
66
|
+
execution_time = time.time() - start_time
|
|
67
|
+
|
|
68
|
+
return AnalysisResult(
|
|
69
|
+
success=True,
|
|
70
|
+
issues=issues,
|
|
71
|
+
metrics=metrics,
|
|
72
|
+
execution_time=execution_time,
|
|
73
|
+
analyzer_version=self.get_version()
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error("Python analysis failed", error=str(e), file_path=file_path)
|
|
78
|
+
return AnalysisResult(
|
|
79
|
+
success=False,
|
|
80
|
+
issues=[AnalysisIssue(
|
|
81
|
+
line=1,
|
|
82
|
+
severity=Severity.ERROR,
|
|
83
|
+
code="ANALYZER_ERROR",
|
|
84
|
+
message=f"Analysis failed: {str(e)}",
|
|
85
|
+
category="system"
|
|
86
|
+
)],
|
|
87
|
+
metrics=metrics,
|
|
88
|
+
execution_time=time.time() - start_time,
|
|
89
|
+
analyzer_version=self.get_version()
|
|
90
|
+
)
|
|
91
|
+
finally:
|
|
92
|
+
# Clean up temporary file
|
|
93
|
+
try:
|
|
94
|
+
os.unlink(temp_file_path)
|
|
95
|
+
except OSError:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
async def _run_ruff(self, file_path: str, code: str) -> list[AnalysisIssue]:
|
|
99
|
+
"""Run ruff linter and formatter checks"""
|
|
100
|
+
issues: list[AnalysisIssue] = []
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
# Build ruff command
|
|
104
|
+
cmd = ["ruff", "check", "--output-format=json"]
|
|
105
|
+
|
|
106
|
+
if self.ruff_config:
|
|
107
|
+
cmd.extend(["--config", self.ruff_config])
|
|
108
|
+
else:
|
|
109
|
+
# Use inline configuration
|
|
110
|
+
cmd.extend([
|
|
111
|
+
"--line-length", str(self.max_line_length),
|
|
112
|
+
"--select", "E,W,F,B,C4,UP,I", # Error categories
|
|
113
|
+
])
|
|
114
|
+
|
|
115
|
+
cmd.append(file_path)
|
|
116
|
+
|
|
117
|
+
# Run ruff
|
|
118
|
+
process = await asyncio.create_subprocess_exec(
|
|
119
|
+
*cmd,
|
|
120
|
+
stdout=asyncio.subprocess.PIPE,
|
|
121
|
+
stderr=asyncio.subprocess.PIPE
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
stdout, stderr = await process.communicate()
|
|
125
|
+
|
|
126
|
+
if process.returncode == 0:
|
|
127
|
+
# No issues found
|
|
128
|
+
pass
|
|
129
|
+
elif process.returncode == 1:
|
|
130
|
+
# Issues found - parse JSON output
|
|
131
|
+
try:
|
|
132
|
+
ruff_results = json.loads(stdout.decode())
|
|
133
|
+
for result in ruff_results:
|
|
134
|
+
issues.append(AnalysisIssue(
|
|
135
|
+
line=result.get("location", {}).get("row", 1),
|
|
136
|
+
column=result.get("location", {}).get("column", 0),
|
|
137
|
+
severity=self._map_ruff_severity(result.get("code", "")),
|
|
138
|
+
code=result.get("code", ""),
|
|
139
|
+
message=result.get("message", ""),
|
|
140
|
+
category="style" if result.get("code", "").startswith(("E", "W")) else "logic",
|
|
141
|
+
suggestion=result.get("fix", {}).get("message") if result.get("fix") else None
|
|
142
|
+
))
|
|
143
|
+
except json.JSONDecodeError:
|
|
144
|
+
logger.warning("Failed to parse ruff JSON output", stdout=stdout.decode())
|
|
145
|
+
|
|
146
|
+
else:
|
|
147
|
+
# Error running ruff
|
|
148
|
+
logger.error("Ruff execution failed",
|
|
149
|
+
returncode=process.returncode,
|
|
150
|
+
stderr=stderr.decode())
|
|
151
|
+
|
|
152
|
+
except FileNotFoundError:
|
|
153
|
+
logger.warning("Ruff not found - skipping ruff analysis")
|
|
154
|
+
issues.append(AnalysisIssue(
|
|
155
|
+
line=1,
|
|
156
|
+
severity=Severity.WARNING,
|
|
157
|
+
code="TOOL_MISSING",
|
|
158
|
+
message="Ruff not installed - style checking skipped",
|
|
159
|
+
category="system"
|
|
160
|
+
))
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.error("Ruff analysis error", error=str(e))
|
|
163
|
+
|
|
164
|
+
return issues
|
|
165
|
+
|
|
166
|
+
async def _run_mypy(self, file_path: str, code: str) -> list[AnalysisIssue]:
|
|
167
|
+
"""Run mypy type checker"""
|
|
168
|
+
issues: list[AnalysisIssue] = []
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
# Build mypy command
|
|
172
|
+
cmd = ["mypy", "--show-error-codes", "--no-error-summary", "--output", "json"]
|
|
173
|
+
|
|
174
|
+
if self.mypy_config:
|
|
175
|
+
cmd.extend(["--config-file", self.mypy_config])
|
|
176
|
+
else:
|
|
177
|
+
# Basic mypy settings
|
|
178
|
+
cmd.extend([
|
|
179
|
+
"--check-untyped-defs",
|
|
180
|
+
"--disallow-untyped-defs",
|
|
181
|
+
"--warn-return-any",
|
|
182
|
+
])
|
|
183
|
+
|
|
184
|
+
cmd.append(file_path)
|
|
185
|
+
|
|
186
|
+
# Run mypy
|
|
187
|
+
process = await asyncio.create_subprocess_exec(
|
|
188
|
+
*cmd,
|
|
189
|
+
stdout=asyncio.subprocess.PIPE,
|
|
190
|
+
stderr=asyncio.subprocess.PIPE
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
stdout, stderr = await process.communicate()
|
|
194
|
+
|
|
195
|
+
if process.returncode == 0:
|
|
196
|
+
# No type issues
|
|
197
|
+
pass
|
|
198
|
+
else:
|
|
199
|
+
# Parse mypy JSON output
|
|
200
|
+
try:
|
|
201
|
+
for line in stdout.decode().strip().split('\n'):
|
|
202
|
+
if line.strip():
|
|
203
|
+
mypy_result = json.loads(line)
|
|
204
|
+
issues.append(AnalysisIssue(
|
|
205
|
+
line=mypy_result.get("line", 1),
|
|
206
|
+
column=mypy_result.get("column", 0),
|
|
207
|
+
severity=self._map_mypy_severity(mypy_result.get("severity", "error")),
|
|
208
|
+
code=mypy_result.get("code", ""),
|
|
209
|
+
message=mypy_result.get("message", ""),
|
|
210
|
+
category="types"
|
|
211
|
+
))
|
|
212
|
+
except (json.JSONDecodeError, KeyError):
|
|
213
|
+
# Fallback to parsing text output
|
|
214
|
+
self._parse_mypy_text_output(stdout.decode(), issues)
|
|
215
|
+
|
|
216
|
+
except FileNotFoundError:
|
|
217
|
+
logger.warning("Mypy not found - skipping type checking")
|
|
218
|
+
issues.append(AnalysisIssue(
|
|
219
|
+
line=1,
|
|
220
|
+
severity=Severity.INFO,
|
|
221
|
+
code="TOOL_MISSING",
|
|
222
|
+
message="Mypy not installed - type checking skipped",
|
|
223
|
+
category="system"
|
|
224
|
+
))
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error("Mypy analysis error", error=str(e))
|
|
227
|
+
|
|
228
|
+
return issues
|
|
229
|
+
|
|
230
|
+
def _map_ruff_severity(self, code: str) -> Severity:
|
|
231
|
+
"""Map ruff error codes to severity levels"""
|
|
232
|
+
if code.startswith("F"): # pyflakes - logical errors
|
|
233
|
+
return Severity.ERROR
|
|
234
|
+
elif code.startswith("E"): # pycodestyle errors
|
|
235
|
+
return Severity.WARNING
|
|
236
|
+
elif code.startswith("W"): # pycodestyle warnings
|
|
237
|
+
return Severity.INFO
|
|
238
|
+
elif code.startswith("B"): # flake8-bugbear - likely bugs
|
|
239
|
+
return Severity.ERROR
|
|
240
|
+
else:
|
|
241
|
+
return Severity.WARNING
|
|
242
|
+
|
|
243
|
+
def _map_mypy_severity(self, severity: str) -> Severity:
|
|
244
|
+
"""Map mypy severity to our severity levels"""
|
|
245
|
+
return {
|
|
246
|
+
"error": Severity.ERROR,
|
|
247
|
+
"warning": Severity.WARNING,
|
|
248
|
+
"note": Severity.INFO
|
|
249
|
+
}.get(severity.lower(), Severity.WARNING)
|
|
250
|
+
|
|
251
|
+
def _parse_mypy_text_output(self, output: str, issues: list[AnalysisIssue]) -> None:
|
|
252
|
+
"""Parse mypy text output when JSON parsing fails"""
|
|
253
|
+
for line in output.strip().split('\n'):
|
|
254
|
+
if ':' in line and ('error:' in line or 'warning:' in line):
|
|
255
|
+
try:
|
|
256
|
+
parts = line.split(':', 3)
|
|
257
|
+
if len(parts) >= 4:
|
|
258
|
+
line_num = int(parts[1]) if parts[1].isdigit() else 1
|
|
259
|
+
severity_text = "error" if "error:" in line else "warning"
|
|
260
|
+
message = parts[3].strip()
|
|
261
|
+
|
|
262
|
+
issues.append(AnalysisIssue(
|
|
263
|
+
line=line_num,
|
|
264
|
+
severity=self._map_mypy_severity(severity_text),
|
|
265
|
+
message=message,
|
|
266
|
+
category="types"
|
|
267
|
+
))
|
|
268
|
+
except (ValueError, IndexError):
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
async def _calculate_advanced_metrics(self, code: str) -> dict[str, Any]:
|
|
272
|
+
"""Calculate advanced code metrics using radon or custom analysis"""
|
|
273
|
+
metrics = {}
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
# Try to use radon for complexity metrics
|
|
277
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_file:
|
|
278
|
+
temp_file.write(code)
|
|
279
|
+
temp_file_path = temp_file.name
|
|
280
|
+
|
|
281
|
+
# Cyclomatic complexity with radon
|
|
282
|
+
process = await asyncio.create_subprocess_exec(
|
|
283
|
+
"radon", "cc", "--json", temp_file_path,
|
|
284
|
+
stdout=asyncio.subprocess.PIPE,
|
|
285
|
+
stderr=asyncio.subprocess.PIPE
|
|
286
|
+
)
|
|
287
|
+
stdout, _ = await process.communicate()
|
|
288
|
+
|
|
289
|
+
if process.returncode == 0:
|
|
290
|
+
radon_results = json.loads(stdout.decode())
|
|
291
|
+
max_complexity = 0
|
|
292
|
+
for file_results in radon_results.values():
|
|
293
|
+
for item in file_results:
|
|
294
|
+
max_complexity = max(max_complexity, item.get('complexity', 0))
|
|
295
|
+
metrics['cyclomatic_complexity'] = max_complexity
|
|
296
|
+
|
|
297
|
+
os.unlink(temp_file_path)
|
|
298
|
+
|
|
299
|
+
except (FileNotFoundError, Exception):
|
|
300
|
+
# Fallback to basic AST analysis
|
|
301
|
+
metrics.update(self._calculate_ast_complexity(code))
|
|
302
|
+
|
|
303
|
+
return metrics
|
|
304
|
+
|
|
305
|
+
def _calculate_ast_complexity(self, code: str) -> dict[str, Any]:
|
|
306
|
+
"""Calculate complexity metrics using AST analysis"""
|
|
307
|
+
try:
|
|
308
|
+
tree = ast.parse(code)
|
|
309
|
+
complexity = self._calculate_cyclomatic_complexity(tree)
|
|
310
|
+
return {
|
|
311
|
+
'cyclomatic_complexity': complexity,
|
|
312
|
+
'cognitive_complexity': complexity, # Simplified
|
|
313
|
+
'maintainability_index': max(0, 171 - 5.2 * complexity - 0.23 * len(code.split('\n')))
|
|
314
|
+
}
|
|
315
|
+
except SyntaxError:
|
|
316
|
+
return {
|
|
317
|
+
'cyclomatic_complexity': 0,
|
|
318
|
+
'cognitive_complexity': 0,
|
|
319
|
+
'maintainability_index': 0
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
def _calculate_cyclomatic_complexity(self, node: ast.AST) -> int:
|
|
323
|
+
"""Calculate cyclomatic complexity from AST"""
|
|
324
|
+
complexity = 1 # Base complexity
|
|
325
|
+
|
|
326
|
+
for child in ast.walk(node):
|
|
327
|
+
# Decision points that increase complexity
|
|
328
|
+
if isinstance(child, ast.If | ast.While | ast.For | ast.With | ast.Try | ast.ExceptHandler):
|
|
329
|
+
complexity += 1
|
|
330
|
+
elif isinstance(child, ast.BoolOp): # and/or operators
|
|
331
|
+
complexity += len(child.values) - 1
|
|
332
|
+
|
|
333
|
+
return complexity
|
|
334
|
+
|
|
335
|
+
def get_version(self) -> str:
|
|
336
|
+
"""Get analyzer version info"""
|
|
337
|
+
versions = []
|
|
338
|
+
|
|
339
|
+
if self.ruff_enabled:
|
|
340
|
+
versions.append("ruff:enabled")
|
|
341
|
+
if self.mypy_enabled:
|
|
342
|
+
versions.append("mypy:enabled")
|
|
343
|
+
|
|
344
|
+
return f"PythonAnalyzer({','.join(versions)})"
|