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

1#!/usr/bin/env python3 

2""" 

3Code Analysis Tools for Osiris CLI 

4Analyze code quality, suggest improvements, and explain code 

5""" 

6 

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 

13 

14console = Console() 

15 

16 

17class CodeAnalyzer: 

18 """Analyze Python code for quality and complexity""" 

19 

20 def __init__(self): 

21 self.issues = [] 

22 

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"} 

27 

28 try: 

29 with open(file_path, 'r') as f: 

30 code = f.read() 

31 

32 return self.analyze_code(code, str(file_path)) 

33 except Exception as e: 

34 return {"error": str(e)} 

35 

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 } 

46 

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 } 

58 

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 }) 

72 

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 }) 

84 

85 elif isinstance(node, ast.ClassDef): 

86 stats["classes"] += 1 

87 

88 elif isinstance(node, (ast.Import, ast.ImportFrom)): 

89 stats["imports"] += 1 

90 

91 # Calculate overall complexity 

92 stats["complexity"] = sum( 

93 issue["complexity"] for issue in stats["issues"] 

94 if issue["type"] == "high_complexity" 

95 ) 

96 

97 return stats 

98 

99 def _calculate_complexity(self, node: ast.FunctionDef) -> int: 

100 """Calculate cyclomatic complexity of a function""" 

101 complexity = 1 # Base complexity 

102 

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 

109 

110 return complexity 

111 

112 def suggest_refactoring(self, file_path: Path) -> List[str]: 

113 """Suggest refactoring opportunities""" 

114 analysis = self.analyze_file(file_path) 

115 

116 if "error" in analysis: 

117 return [f"Error: {analysis['error']}"] 

118 

119 suggestions = [] 

120 

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 ) 

128 

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 ) 

136 

137 # File-level suggestions 

138 if analysis["lines"] > 500: 

139 suggestions.append( 

140 f"File has {analysis['lines']} lines - consider splitting into modules" 

141 ) 

142 

143 if analysis["functions"] > 20: 

144 suggestions.append( 

145 f"File has {analysis['functions']} functions - consider organizing into classes" 

146 ) 

147 

148 return suggestions if suggestions else ["No refactoring suggestions - code looks good!"] 

149 

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" 

156 

157 explanation = [] 

158 

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 ) 

165 

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 ) 

171 

172 return "\n".join(explanation) if explanation else "No high-level structures found" 

173 

174 def display_analysis(self, file_path: Path): 

175 """Display analysis results in a nice format""" 

176 analysis = self.analyze_file(file_path) 

177 

178 if "error" in analysis: 

179 console.print(f"[red]Error: {analysis['error']}[/red]") 

180 return 

181 

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""" 

189 

190 console.print(Panel(summary, title="Code Analysis", border_style="cyan")) 

191 

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']}") 

197 

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']}") 

203 

204 if not analysis["issues"] and not analysis["suggestions"]: 

205 console.print("\n[green]✓ No issues found - code looks good![/green]") 

206 

207 

208# Global analyzer instance 

209code_analyzer = CodeAnalyzer()