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.
@@ -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)})"