Coverage for src / osiris_cli / code_analyzer.py: 0%
97 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-27 17:41 +0200
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-27 17:41 +0200
1#!/usr/bin/env python3
2"""
3Code Analysis Tools for Osiris CLI
4Analyze code quality, suggest improvements, and explain code
5"""
7import ast
8from pathlib import Path
9from typing import List, Dict, Any, Optional
10from rich.console import Console
11from rich.syntax import Syntax
12from rich.panel import Panel
14console = Console()
17class CodeAnalyzer:
18 """Analyze Python code for quality and complexity"""
20 def __init__(self):
21 self.issues = []
23 def analyze_file(self, file_path: Path) -> Dict[str, Any]:
24 """Analyze a Python file"""
25 if not file_path.exists():
26 return {"error": "File not found"}
28 try:
29 with open(file_path, 'r') as f:
30 code = f.read()
32 return self.analyze_code(code, str(file_path))
33 except Exception as e:
34 return {"error": str(e)}
36 def analyze_code(self, code: str, filename: str = "<string>") -> Dict[str, Any]:
37 """Analyze Python code"""
38 try:
39 tree = ast.parse(code, filename=filename)
40 except SyntaxError as e:
41 return {
42 "error": "Syntax error",
43 "line": e.lineno,
44 "message": e.msg
45 }
47 # Analyze AST
48 stats = {
49 "filename": filename,
50 "lines": len(code.split('\n')),
51 "functions": 0,
52 "classes": 0,
53 "imports": 0,
54 "complexity": 0,
55 "issues": [],
56 "suggestions": []
57 }
59 for node in ast.walk(tree):
60 if isinstance(node, ast.FunctionDef):
61 stats["functions"] += 1
62 # Check function complexity
63 complexity = self._calculate_complexity(node)
64 if complexity > 10:
65 stats["issues"].append({
66 "type": "high_complexity",
67 "line": node.lineno,
68 "function": node.name,
69 "complexity": complexity,
70 "message": f"Function '{node.name}' has high complexity ({complexity})"
71 })
73 # Check function length
74 if hasattr(node, 'end_lineno'):
75 length = node.end_lineno - node.lineno
76 if length > 50:
77 stats["suggestions"].append({
78 "type": "long_function",
79 "line": node.lineno,
80 "function": node.name,
81 "length": length,
82 "message": f"Consider breaking down '{node.name}' ({length} lines)"
83 })
85 elif isinstance(node, ast.ClassDef):
86 stats["classes"] += 1
88 elif isinstance(node, (ast.Import, ast.ImportFrom)):
89 stats["imports"] += 1
91 # Calculate overall complexity
92 stats["complexity"] = sum(
93 issue["complexity"] for issue in stats["issues"]
94 if issue["type"] == "high_complexity"
95 )
97 return stats
99 def _calculate_complexity(self, node: ast.FunctionDef) -> int:
100 """Calculate cyclomatic complexity of a function"""
101 complexity = 1 # Base complexity
103 for child in ast.walk(node):
104 # Count decision points
105 if isinstance(child, (ast.If, ast.While, ast.For, ast.ExceptHandler)):
106 complexity += 1
107 elif isinstance(child, ast.BoolOp):
108 complexity += len(child.values) - 1
110 return complexity
112 def suggest_refactoring(self, file_path: Path) -> List[str]:
113 """Suggest refactoring opportunities"""
114 analysis = self.analyze_file(file_path)
116 if "error" in analysis:
117 return [f"Error: {analysis['error']}"]
119 suggestions = []
121 # High complexity functions
122 for issue in analysis["issues"]:
123 if issue["type"] == "high_complexity":
124 suggestions.append(
125 f"Line {issue['line']}: Refactor '{issue['function']}' "
126 f"(complexity: {issue['complexity']}) - break into smaller functions"
127 )
129 # Long functions
130 for suggestion in analysis["suggestions"]:
131 if suggestion["type"] == "long_function":
132 suggestions.append(
133 f"Line {suggestion['line']}: '{suggestion['function']}' "
134 f"is {suggestion['length']} lines - consider extracting helper functions"
135 )
137 # File-level suggestions
138 if analysis["lines"] > 500:
139 suggestions.append(
140 f"File has {analysis['lines']} lines - consider splitting into modules"
141 )
143 if analysis["functions"] > 20:
144 suggestions.append(
145 f"File has {analysis['functions']} functions - consider organizing into classes"
146 )
148 return suggestions if suggestions else ["No refactoring suggestions - code looks good!"]
150 def explain_code(self, code: str) -> str:
151 """Generate explanation of code"""
152 try:
153 tree = ast.parse(code)
154 except SyntaxError:
155 return "Cannot explain - syntax error in code"
157 explanation = []
159 for node in ast.walk(tree):
160 if isinstance(node, ast.FunctionDef):
161 args = [arg.arg for arg in node.args.args]
162 explanation.append(
163 f"Function '{node.name}' takes {len(args)} parameter(s): {', '.join(args)}"
164 )
166 elif isinstance(node, ast.ClassDef):
167 methods = [n.name for n in node.body if isinstance(n, ast.FunctionDef)]
168 explanation.append(
169 f"Class '{node.name}' has {len(methods)} method(s): {', '.join(methods[:5])}"
170 )
172 return "\n".join(explanation) if explanation else "No high-level structures found"
174 def display_analysis(self, file_path: Path):
175 """Display analysis results in a nice format"""
176 analysis = self.analyze_file(file_path)
178 if "error" in analysis:
179 console.print(f"[red]Error: {analysis['error']}[/red]")
180 return
182 # Create summary panel
183 summary = f"""[bold]File:[/bold] {analysis['filename']}
184[bold]Lines:[/bold] {analysis['lines']}
185[bold]Functions:[/bold] {analysis['functions']}
186[bold]Classes:[/bold] {analysis['classes']}
187[bold]Imports:[/bold] {analysis['imports']}
188"""
190 console.print(Panel(summary, title="Code Analysis", border_style="cyan"))
192 # Show issues
193 if analysis["issues"]:
194 console.print("\n[bold red]Issues Found:[/bold red]")
195 for issue in analysis["issues"]:
196 console.print(f" [red]•[/red] {issue['message']}")
198 # Show suggestions
199 if analysis["suggestions"]:
200 console.print("\n[bold yellow]Suggestions:[/bold yellow]")
201 for suggestion in analysis["suggestions"]:
202 console.print(f" [yellow]•[/yellow] {suggestion['message']}")
204 if not analysis["issues"] and not analysis["suggestions"]:
205 console.print("\n[green]✓ No issues found - code looks good![/green]")
208# Global analyzer instance
209code_analyzer = CodeAnalyzer()