chore: import zh skill code-mentor
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Code Analyzer - Static analysis tool for code review
|
||||
|
||||
Analyzes code for:
|
||||
- Bugs and potential errors
|
||||
- Style violations
|
||||
- Complexity metrics
|
||||
- Security issues
|
||||
- Best practice violations
|
||||
|
||||
Supports: Python, JavaScript, Java, C++
|
||||
|
||||
Usage:
|
||||
python analyze_code.py <file_path>
|
||||
python analyze_code.py <file_path> --format json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
class CodeIssue:
|
||||
"""Represents a code issue found during analysis."""
|
||||
|
||||
def __init__(self, category, severity, line, message, suggestion=None):
|
||||
self.category = category # bug, style, performance, security
|
||||
self.severity = severity # critical, warning, info
|
||||
self.line = line
|
||||
self.message = message
|
||||
self.suggestion = suggestion
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'category': self.category,
|
||||
'severity': self.severity,
|
||||
'line': self.line,
|
||||
'message': self.message,
|
||||
'suggestion': self.suggestion
|
||||
}
|
||||
|
||||
|
||||
class PythonAnalyzer:
|
||||
"""Analyzer for Python code."""
|
||||
|
||||
def __init__(self, code, filename):
|
||||
self.code = code
|
||||
self.filename = filename
|
||||
self.lines = code.split('\n')
|
||||
self.issues = []
|
||||
|
||||
def analyze(self) -> List[CodeIssue]:
|
||||
"""Run all analysis checks."""
|
||||
try:
|
||||
tree = ast.parse(self.code)
|
||||
self._check_syntax(tree)
|
||||
except SyntaxError as e:
|
||||
self.issues.append(CodeIssue(
|
||||
'bug', 'critical', e.lineno,
|
||||
f"Syntax error: {e.msg}"
|
||||
))
|
||||
return self.issues
|
||||
|
||||
self._check_style()
|
||||
self._check_complexity()
|
||||
self._check_best_practices()
|
||||
self._check_security()
|
||||
|
||||
return self.issues
|
||||
|
||||
def _check_syntax(self, tree):
|
||||
"""Check for common syntax and logic issues."""
|
||||
for node in ast.walk(tree):
|
||||
# Check for bare except
|
||||
if isinstance(node, ast.ExceptHandler):
|
||||
if node.type is None:
|
||||
self.issues.append(CodeIssue(
|
||||
'style', 'warning', node.lineno,
|
||||
"Bare except: clause catches all exceptions",
|
||||
"Use specific exception types (e.g., except ValueError:)"
|
||||
))
|
||||
|
||||
# Check for mutable default arguments
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
for default in node.args.defaults:
|
||||
if isinstance(default, (ast.List, ast.Dict, ast.Set)):
|
||||
self.issues.append(CodeIssue(
|
||||
'bug', 'warning', node.lineno,
|
||||
f"Mutable default argument in function '{node.name}'",
|
||||
"Use None as default and create mutable object inside function"
|
||||
))
|
||||
|
||||
def _check_style(self):
|
||||
"""Check PEP 8 style guidelines."""
|
||||
for i, line in enumerate(self.lines, 1):
|
||||
# Line too long
|
||||
if len(line) > 100:
|
||||
self.issues.append(CodeIssue(
|
||||
'style', 'info', i,
|
||||
f"Line too long ({len(line)} > 100 characters)"
|
||||
))
|
||||
|
||||
# Multiple statements on one line
|
||||
if ';' in line and not line.strip().startswith('#'):
|
||||
self.issues.append(CodeIssue(
|
||||
'style', 'info', i,
|
||||
"Multiple statements on one line (use semicolon)",
|
||||
"Place each statement on its own line"
|
||||
))
|
||||
|
||||
# Trailing whitespace
|
||||
if line.endswith(' ') or line.endswith('\t'):
|
||||
self.issues.append(CodeIssue(
|
||||
'style', 'info', i,
|
||||
"Trailing whitespace"
|
||||
))
|
||||
|
||||
def _check_complexity(self):
|
||||
"""Check for complexity issues."""
|
||||
try:
|
||||
tree = ast.parse(self.code)
|
||||
except:
|
||||
return
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
# Count nested depth
|
||||
max_depth = self._calculate_nesting_depth(node)
|
||||
if max_depth > 4:
|
||||
self.issues.append(CodeIssue(
|
||||
'performance', 'warning', node.lineno,
|
||||
f"Function '{node.name}' has deep nesting (depth {max_depth})",
|
||||
"Consider extracting nested logic into separate functions"
|
||||
))
|
||||
|
||||
# Count number of statements
|
||||
statements = sum(1 for _ in ast.walk(node))
|
||||
if statements > 50:
|
||||
self.issues.append(CodeIssue(
|
||||
'style', 'warning', node.lineno,
|
||||
f"Function '{node.name}' is too long ({statements} statements)",
|
||||
"Consider breaking into smaller functions"
|
||||
))
|
||||
|
||||
def _calculate_nesting_depth(self, node, depth=0):
|
||||
"""Calculate maximum nesting depth in a function."""
|
||||
max_depth = depth
|
||||
for child in ast.iter_child_nodes(node):
|
||||
if isinstance(child, (ast.If, ast.For, ast.While, ast.With)):
|
||||
child_depth = self._calculate_nesting_depth(child, depth + 1)
|
||||
max_depth = max(max_depth, child_depth)
|
||||
return max_depth
|
||||
|
||||
def _check_best_practices(self):
|
||||
"""Check for violations of best practices."""
|
||||
for i, line in enumerate(self.lines, 1):
|
||||
# Check for print statements in production code
|
||||
if re.search(r'\bprint\s*\(', line) and 'debug' not in line.lower():
|
||||
self.issues.append(CodeIssue(
|
||||
'style', 'info', i,
|
||||
"Print statement found - consider using logging",
|
||||
"Use logging module instead of print for production code"
|
||||
))
|
||||
|
||||
# Check for == None instead of is None
|
||||
if re.search(r'==\s*None|None\s*==', line):
|
||||
self.issues.append(CodeIssue(
|
||||
'style', 'info', i,
|
||||
"Use 'is None' instead of '== None'"
|
||||
))
|
||||
|
||||
# Check for != None instead of is not None
|
||||
if re.search(r'!=\s*None|None\s*!=', line):
|
||||
self.issues.append(CodeIssue(
|
||||
'style', 'info', i,
|
||||
"Use 'is not None' instead of '!= None'"
|
||||
))
|
||||
|
||||
def _check_security(self):
|
||||
"""Check for common security issues."""
|
||||
for i, line in enumerate(self.lines, 1):
|
||||
# SQL injection vulnerability
|
||||
if 'execute' in line and ('+' in line or '%' in line or 'format' in line):
|
||||
if 'SELECT' in line.upper() or 'INSERT' in line.upper():
|
||||
self.issues.append(CodeIssue(
|
||||
'security', 'critical', i,
|
||||
"Potential SQL injection vulnerability",
|
||||
"Use parameterized queries with placeholders"
|
||||
))
|
||||
|
||||
# eval() usage
|
||||
if re.search(r'\beval\s*\(', line):
|
||||
self.issues.append(CodeIssue(
|
||||
'security', 'critical', i,
|
||||
"Use of eval() is dangerous",
|
||||
"Avoid eval() - use ast.literal_eval() for safe evaluation"
|
||||
))
|
||||
|
||||
# Hard-coded passwords/secrets
|
||||
if re.search(r'password\s*=\s*["\']', line, re.IGNORECASE):
|
||||
self.issues.append(CodeIssue(
|
||||
'security', 'critical', i,
|
||||
"Potential hard-coded password",
|
||||
"Use environment variables or secure configuration"
|
||||
))
|
||||
|
||||
|
||||
class JavaScriptAnalyzer:
|
||||
"""Basic analyzer for JavaScript code."""
|
||||
|
||||
def __init__(self, code, filename):
|
||||
self.code = code
|
||||
self.filename = filename
|
||||
self.lines = code.split('\n')
|
||||
self.issues = []
|
||||
|
||||
def analyze(self) -> List[CodeIssue]:
|
||||
"""Run all analysis checks."""
|
||||
self._check_style()
|
||||
self._check_best_practices()
|
||||
return self.issues
|
||||
|
||||
def _check_style(self):
|
||||
"""Check style guidelines."""
|
||||
for i, line in enumerate(self.lines, 1):
|
||||
# var instead of let/const
|
||||
if re.search(r'\bvar\s+', line):
|
||||
self.issues.append(CodeIssue(
|
||||
'style', 'warning', i,
|
||||
"Use 'let' or 'const' instead of 'var'",
|
||||
"ES6+ recommends let/const for better scoping"
|
||||
))
|
||||
|
||||
# == instead of ===
|
||||
if '==' in line and '===' not in line and '!==' not in line:
|
||||
self.issues.append(CodeIssue(
|
||||
'style', 'info', i,
|
||||
"Use '===' instead of '==' for strict equality"
|
||||
))
|
||||
|
||||
def _check_best_practices(self):
|
||||
"""Check JavaScript best practices."""
|
||||
for i, line in enumerate(self.lines, 1):
|
||||
# console.log in production
|
||||
if 'console.log' in line:
|
||||
self.issues.append(CodeIssue(
|
||||
'style', 'info', i,
|
||||
"console.log found - remove before production"
|
||||
))
|
||||
|
||||
|
||||
class CodeMetrics:
|
||||
"""Calculate code metrics."""
|
||||
|
||||
def __init__(self, code):
|
||||
self.code = code
|
||||
self.lines = code.split('\n')
|
||||
|
||||
def calculate(self) -> Dict[str, Any]:
|
||||
"""Calculate various metrics."""
|
||||
total_lines = len(self.lines)
|
||||
code_lines = sum(1 for line in self.lines if line.strip() and not line.strip().startswith('#'))
|
||||
comment_lines = sum(1 for line in self.lines if line.strip().startswith('#'))
|
||||
blank_lines = total_lines - code_lines - comment_lines
|
||||
|
||||
return {
|
||||
'total_lines': total_lines,
|
||||
'code_lines': code_lines,
|
||||
'comment_lines': comment_lines,
|
||||
'blank_lines': blank_lines,
|
||||
'comment_ratio': round(comment_lines / max(code_lines, 1), 2)
|
||||
}
|
||||
|
||||
|
||||
def detect_language(filename):
|
||||
"""Detect programming language from file extension."""
|
||||
ext = Path(filename).suffix.lower()
|
||||
language_map = {
|
||||
'.py': 'python',
|
||||
'.js': 'javascript',
|
||||
'.jsx': 'javascript',
|
||||
'.ts': 'javascript',
|
||||
'.tsx': 'javascript',
|
||||
'.java': 'java',
|
||||
'.cpp': 'cpp',
|
||||
'.cc': 'cpp',
|
||||
'.cxx': 'cpp',
|
||||
'.c': 'c'
|
||||
}
|
||||
return language_map.get(ext, 'unknown')
|
||||
|
||||
|
||||
def analyze_file(filepath, output_format='text'):
|
||||
"""Analyze a code file."""
|
||||
if not os.path.exists(filepath):
|
||||
print(f"Error: File '{filepath}' not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
code = f.read()
|
||||
|
||||
language = detect_language(filepath)
|
||||
|
||||
# Choose analyzer based on language
|
||||
if language == 'python':
|
||||
analyzer = PythonAnalyzer(code, filepath)
|
||||
elif language == 'javascript':
|
||||
analyzer = JavaScriptAnalyzer(code, filepath)
|
||||
else:
|
||||
print(f"Error: Unsupported language '{language}'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Run analysis
|
||||
issues = analyzer.analyze()
|
||||
|
||||
# Calculate metrics
|
||||
metrics = CodeMetrics(code).calculate()
|
||||
|
||||
# Output results
|
||||
if output_format == 'json':
|
||||
result = {
|
||||
'file': filepath,
|
||||
'language': language,
|
||||
'metrics': metrics,
|
||||
'issues': [issue.to_dict() for issue in issues]
|
||||
}
|
||||
print(json.dumps(result, indent=2))
|
||||
else:
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Code Analysis: {filepath}")
|
||||
print(f"Language: {language}")
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
print("METRICS:")
|
||||
print(f" Total lines: {metrics['total_lines']}")
|
||||
print(f" Code lines: {metrics['code_lines']}")
|
||||
print(f" Comment lines: {metrics['comment_lines']}")
|
||||
print(f" Blank lines: {metrics['blank_lines']}")
|
||||
print(f" Comment ratio: {metrics['comment_ratio']:.2%}\n")
|
||||
|
||||
if issues:
|
||||
print(f"ISSUES FOUND: {len(issues)}\n")
|
||||
|
||||
# Group by severity
|
||||
critical = [i for i in issues if i.severity == 'critical']
|
||||
warnings = [i for i in issues if i.severity == 'warning']
|
||||
info = [i for i in issues if i.severity == 'info']
|
||||
|
||||
for severity, items in [('CRITICAL', critical), ('WARNING', warnings), ('INFO', info)]:
|
||||
if items:
|
||||
print(f"{severity}:")
|
||||
for issue in items:
|
||||
print(f" Line {issue.line}: [{issue.category}] {issue.message}")
|
||||
if issue.suggestion:
|
||||
print(f" → {issue.suggestion}")
|
||||
print()
|
||||
else:
|
||||
print("✓ No issues found!\n")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Analyze code for issues and metrics')
|
||||
parser.add_argument('file', help='Path to code file to analyze')
|
||||
parser.add_argument('--format', choices=['text', 'json'], default='text',
|
||||
help='Output format (default: text)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
analyze_file(args.file, args.format)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,291 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Complexity Analyzer - Analyze time and space complexity of algorithms
|
||||
|
||||
Features:
|
||||
- Parse code using AST
|
||||
- Detect loops (nested, sequential)
|
||||
- Identify recursion
|
||||
- Analyze data structure operations
|
||||
- Estimate Big-O complexity
|
||||
- Suggest optimizations
|
||||
|
||||
Usage:
|
||||
python complexity_analyzer.py <file_path> [--function <function_name>]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
|
||||
class ComplexityAnalyzer(ast.NodeVisitor):
|
||||
"""Analyze time and space complexity of Python code."""
|
||||
|
||||
def __init__(self, function_name=None):
|
||||
self.function_name = function_name
|
||||
self.results = {}
|
||||
self.current_function = None
|
||||
|
||||
def visit_FunctionDef(self, node):
|
||||
"""Analyze a function definition."""
|
||||
# Only analyze specific function if requested
|
||||
if self.function_name and node.name != self.function_name:
|
||||
return
|
||||
|
||||
self.current_function = node.name
|
||||
|
||||
analysis = {
|
||||
'name': node.name,
|
||||
'line': node.lineno,
|
||||
'time_complexity': 'O(1)',
|
||||
'space_complexity': 'O(1)',
|
||||
'loops': [],
|
||||
'recursion': False,
|
||||
'operations': [],
|
||||
'suggestions': []
|
||||
}
|
||||
|
||||
# Analyze the function body
|
||||
loop_depth = self._analyze_loops(node)
|
||||
has_recursion = self._check_recursion(node)
|
||||
data_structure_ops = self._analyze_data_structures(node)
|
||||
|
||||
# Determine time complexity
|
||||
if has_recursion:
|
||||
analysis['recursion'] = True
|
||||
recursion_type = self._classify_recursion(node)
|
||||
analysis['time_complexity'] = recursion_type
|
||||
analysis['suggestions'].append(
|
||||
"Recursive function - consider memoization or iterative approach"
|
||||
)
|
||||
elif loop_depth >= 3:
|
||||
analysis['time_complexity'] = f'O(n^{loop_depth})'
|
||||
analysis['suggestions'].append(
|
||||
f"Deep nesting ({loop_depth} levels) - consider optimization"
|
||||
)
|
||||
elif loop_depth == 2:
|
||||
analysis['time_complexity'] = 'O(n²)'
|
||||
analysis['suggestions'].append(
|
||||
"Nested loop detected - can this be optimized with hash map?"
|
||||
)
|
||||
elif loop_depth == 1:
|
||||
analysis['time_complexity'] = 'O(n)'
|
||||
|
||||
# Adjust for data structure operations
|
||||
for op in data_structure_ops:
|
||||
if op['type'] == 'sort':
|
||||
if 'n²' not in analysis['time_complexity']:
|
||||
analysis['time_complexity'] = 'O(n log n)'
|
||||
elif op['type'] == 'dict_lookup':
|
||||
analysis['operations'].append(op)
|
||||
elif op['type'] == 'list_search':
|
||||
if loop_depth == 0:
|
||||
analysis['time_complexity'] = 'O(n)'
|
||||
|
||||
# Analyze space complexity
|
||||
space = self._analyze_space_complexity(node)
|
||||
analysis['space_complexity'] = space
|
||||
|
||||
self.results[node.name] = analysis
|
||||
self.generic_visit(node)
|
||||
|
||||
def _analyze_loops(self, node, depth=0) -> int:
|
||||
"""Calculate maximum loop nesting depth."""
|
||||
max_depth = depth
|
||||
|
||||
for child in ast.walk(node):
|
||||
if isinstance(child, (ast.For, ast.While)):
|
||||
# Check if this is a direct child, not in a nested function
|
||||
if self._is_direct_child(node, child):
|
||||
child_depth = self._analyze_loops(child, depth + 1)
|
||||
max_depth = max(max_depth, child_depth)
|
||||
|
||||
return max_depth
|
||||
|
||||
def _is_direct_child(self, parent, child):
|
||||
"""Check if child is a direct descendant (not in nested function)."""
|
||||
for node in ast.walk(parent):
|
||||
if node == child:
|
||||
return True
|
||||
if isinstance(node, ast.FunctionDef) and node != parent:
|
||||
# Stop if we hit another function definition
|
||||
return False
|
||||
return False
|
||||
|
||||
def _check_recursion(self, node) -> bool:
|
||||
"""Check if function is recursive."""
|
||||
function_name = node.name
|
||||
|
||||
for child in ast.walk(node):
|
||||
if isinstance(child, ast.Call):
|
||||
if isinstance(child.func, ast.Name) and child.func.id == function_name:
|
||||
return True
|
||||
# Check for indirect recursion via attribute
|
||||
if isinstance(child.func, ast.Attribute):
|
||||
if child.func.attr == function_name:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _classify_recursion(self, node) -> str:
|
||||
"""Classify type of recursion for complexity estimation."""
|
||||
# Count recursive calls
|
||||
recursive_calls = 0
|
||||
function_name = node.name
|
||||
|
||||
for child in ast.walk(node):
|
||||
if isinstance(child, ast.Call):
|
||||
if isinstance(child.func, ast.Name) and child.func.id == function_name:
|
||||
recursive_calls += 1
|
||||
|
||||
if recursive_calls == 1:
|
||||
# Linear recursion (e.g., factorial)
|
||||
return 'O(n)'
|
||||
elif recursive_calls == 2:
|
||||
# Binary recursion (e.g., fibonacci)
|
||||
return 'O(2^n)'
|
||||
else:
|
||||
return 'O(recursive)'
|
||||
|
||||
def _analyze_data_structures(self, node) -> List[Dict]:
|
||||
"""Analyze data structure operations."""
|
||||
operations = []
|
||||
|
||||
for child in ast.walk(node):
|
||||
# Sorting
|
||||
if isinstance(child, ast.Call):
|
||||
if isinstance(child.func, ast.Attribute):
|
||||
if child.func.attr == 'sort':
|
||||
operations.append({'type': 'sort', 'line': child.lineno})
|
||||
elif isinstance(child.func, ast.Name):
|
||||
if child.func.id == 'sorted':
|
||||
operations.append({'type': 'sort', 'line': child.lineno})
|
||||
|
||||
# Dictionary/set operations (O(1) average)
|
||||
if isinstance(child, ast.Subscript):
|
||||
if isinstance(child.value, (ast.Dict, ast.Set)):
|
||||
operations.append({'type': 'dict_lookup', 'line': child.lineno})
|
||||
|
||||
# List search operations (O(n))
|
||||
if isinstance(child, ast.Compare):
|
||||
if any(isinstance(op, ast.In) for op in child.ops):
|
||||
operations.append({'type': 'list_search', 'line': child.lineno})
|
||||
|
||||
return operations
|
||||
|
||||
def _analyze_space_complexity(self, node) -> str:
|
||||
"""Estimate space complexity."""
|
||||
# Check for list comprehensions, array creation
|
||||
has_array_creation = False
|
||||
has_recursion = self._check_recursion(node)
|
||||
|
||||
for child in ast.walk(node):
|
||||
# List comprehension or list creation
|
||||
if isinstance(child, (ast.ListComp, ast.List)):
|
||||
has_array_creation = True
|
||||
|
||||
# Dictionary comprehension
|
||||
if isinstance(child, (ast.DictComp, ast.Dict)):
|
||||
has_array_creation = True
|
||||
|
||||
if has_recursion:
|
||||
# Recursion uses call stack
|
||||
return 'O(n) - call stack'
|
||||
elif has_array_creation:
|
||||
return 'O(n) - auxiliary space'
|
||||
else:
|
||||
return 'O(1)'
|
||||
|
||||
|
||||
def format_output(results, output_format='text'):
|
||||
"""Format analysis results."""
|
||||
if output_format == 'json':
|
||||
print(json.dumps(results, indent=2))
|
||||
else:
|
||||
print("\n" + "=" * 60)
|
||||
print("COMPLEXITY ANALYSIS")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
for func_name, analysis in results.items():
|
||||
print(f"Function: {func_name} (line {analysis['line']})")
|
||||
print(f" Time Complexity: {analysis['time_complexity']}")
|
||||
print(f" Space Complexity: {analysis['space_complexity']}")
|
||||
|
||||
if analysis['recursion']:
|
||||
print(f" Recursion: Yes")
|
||||
|
||||
if analysis['operations']:
|
||||
print(f" Operations:")
|
||||
for op in analysis['operations']:
|
||||
print(f" - {op['type']} at line {op['line']}")
|
||||
|
||||
if analysis['suggestions']:
|
||||
print(f" Suggestions:")
|
||||
for suggestion in analysis['suggestions']:
|
||||
print(f" → {suggestion}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def analyze_file(filepath, function_name=None, output_format='text'):
|
||||
"""Analyze a Python file."""
|
||||
if not os.path.exists(filepath):
|
||||
print(f"Error: File '{filepath}' not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
code = f.read()
|
||||
|
||||
try:
|
||||
tree = ast.parse(code)
|
||||
except SyntaxError as e:
|
||||
print(f"Syntax error in file: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
analyzer = ComplexityAnalyzer(function_name)
|
||||
analyzer.visit(tree)
|
||||
|
||||
if not analyzer.results:
|
||||
if function_name:
|
||||
print(f"Error: Function '{function_name}' not found", file=sys.stderr)
|
||||
else:
|
||||
print("No functions found in file", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
format_output(analyzer.results, output_format)
|
||||
|
||||
|
||||
def analyze_code_snippet(code, output_format='text'):
|
||||
"""Analyze a code snippet."""
|
||||
try:
|
||||
tree = ast.parse(code)
|
||||
except SyntaxError as e:
|
||||
print(f"Syntax error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
analyzer = ComplexityAnalyzer()
|
||||
analyzer.visit(tree)
|
||||
|
||||
format_output(analyzer.results, output_format)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Analyze time and space complexity of code'
|
||||
)
|
||||
parser.add_argument('file', help='Python file to analyze')
|
||||
parser.add_argument('--function', help='Specific function to analyze')
|
||||
parser.add_argument('--format', choices=['text', 'json'], default='text',
|
||||
help='Output format (default: text)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
analyze_file(args.file, args.function, args.format)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,334 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test Runner - Execute and format test results
|
||||
|
||||
Supports:
|
||||
- pytest (Python)
|
||||
- unittest (Python)
|
||||
- jest (JavaScript)
|
||||
- JUnit (Java)
|
||||
|
||||
Usage:
|
||||
python run_tests.py <test_file>
|
||||
python run_tests.py <test_directory>
|
||||
python run_tests.py --framework pytest
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class TestResult:
|
||||
"""Represents test execution results."""
|
||||
|
||||
def __init__(self):
|
||||
self.passed = 0
|
||||
self.failed = 0
|
||||
self.errors = 0
|
||||
self.skipped = 0
|
||||
self.total = 0
|
||||
self.duration = 0.0
|
||||
self.failures = []
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'passed': self.passed,
|
||||
'failed': self.failed,
|
||||
'errors': self.errors,
|
||||
'skipped': self.skipped,
|
||||
'total': self.total,
|
||||
'duration': self.duration,
|
||||
'failures': self.failures
|
||||
}
|
||||
|
||||
|
||||
class TestRunner:
|
||||
"""Base class for test runners."""
|
||||
|
||||
def __init__(self, target):
|
||||
self.target = target
|
||||
|
||||
def run(self) -> TestResult:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PytestRunner(TestRunner):
|
||||
"""Run pytest tests."""
|
||||
|
||||
def run(self) -> TestResult:
|
||||
result = TestResult()
|
||||
|
||||
try:
|
||||
# Run pytest with verbose output and JSON report
|
||||
cmd = [
|
||||
'python', '-m', 'pytest',
|
||||
self.target,
|
||||
'-v',
|
||||
'--tb=short'
|
||||
]
|
||||
|
||||
process = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
# Parse output
|
||||
output = process.stdout + process.stderr
|
||||
lines = output.split('\n')
|
||||
|
||||
for line in lines:
|
||||
if ' PASSED' in line:
|
||||
result.passed += 1
|
||||
elif ' FAILED' in line:
|
||||
result.failed += 1
|
||||
# Extract test name and failure info
|
||||
test_name = line.split('::')[1].split(' ')[0] if '::' in line else 'unknown'
|
||||
result.failures.append({
|
||||
'test': test_name,
|
||||
'message': 'See output for details'
|
||||
})
|
||||
elif ' ERROR' in line:
|
||||
result.errors += 1
|
||||
elif ' SKIPPED' in line:
|
||||
result.skipped += 1
|
||||
|
||||
# Extract duration
|
||||
if 'passed in' in line or 'failed in' in line:
|
||||
try:
|
||||
duration_str = line.split(' in ')[1].split('s')[0]
|
||||
result.duration = float(duration_str)
|
||||
except:
|
||||
pass
|
||||
|
||||
result.total = result.passed + result.failed + result.errors + result.skipped
|
||||
|
||||
return result
|
||||
|
||||
except FileNotFoundError:
|
||||
print("Error: pytest not found. Install with: pip install pytest", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except subprocess.TimeoutExpired:
|
||||
print("Error: Tests timed out after 60 seconds", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error running tests: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class UnittestRunner(TestRunner):
|
||||
"""Run unittest tests."""
|
||||
|
||||
def run(self) -> TestResult:
|
||||
result = TestResult()
|
||||
|
||||
try:
|
||||
cmd = [
|
||||
'python', '-m', 'unittest',
|
||||
'discover',
|
||||
'-s', self.target,
|
||||
'-v'
|
||||
]
|
||||
|
||||
process = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
output = process.stdout + process.stderr
|
||||
lines = output.split('\n')
|
||||
|
||||
for line in lines:
|
||||
if ' ... ok' in line:
|
||||
result.passed += 1
|
||||
elif 'FAIL:' in line:
|
||||
result.failed += 1
|
||||
test_name = line.replace('FAIL:', '').strip()
|
||||
result.failures.append({
|
||||
'test': test_name,
|
||||
'message': 'See output for details'
|
||||
})
|
||||
elif 'ERROR:' in line:
|
||||
result.errors += 1
|
||||
|
||||
# Parse summary line
|
||||
for line in reversed(lines):
|
||||
if 'Ran ' in line and ' test' in line:
|
||||
try:
|
||||
result.total = int(line.split('Ran ')[1].split(' test')[0])
|
||||
except:
|
||||
pass
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
except FileNotFoundError:
|
||||
print("Error: unittest module not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error running tests: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class JestRunner(TestRunner):
|
||||
"""Run Jest tests."""
|
||||
|
||||
def run(self) -> TestResult:
|
||||
result = TestResult()
|
||||
|
||||
try:
|
||||
cmd = ['npx', 'jest', self.target, '--verbose']
|
||||
|
||||
process = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
output = process.stdout + process.stderr
|
||||
lines = output.split('\n')
|
||||
|
||||
for line in lines:
|
||||
if '✓' in line or 'PASS' in line:
|
||||
result.passed += 1
|
||||
elif '✕' in line or 'FAIL' in line:
|
||||
result.failed += 1
|
||||
|
||||
# Parse summary
|
||||
for line in lines:
|
||||
if 'Tests:' in line:
|
||||
parts = line.split(',')
|
||||
for part in parts:
|
||||
if 'passed' in part:
|
||||
try:
|
||||
result.passed = int(part.split()[0])
|
||||
except:
|
||||
pass
|
||||
elif 'failed' in part:
|
||||
try:
|
||||
result.failed = int(part.split()[0])
|
||||
except:
|
||||
pass
|
||||
|
||||
result.total = result.passed + result.failed
|
||||
|
||||
return result
|
||||
|
||||
except FileNotFoundError:
|
||||
print("Error: Jest not found. Install with: npm install --save-dev jest", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error running tests: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def detect_framework(target):
|
||||
"""Detect testing framework to use."""
|
||||
# Check if it's a Python file
|
||||
if target.endswith('.py') or os.path.isdir(target):
|
||||
# Check for pytest markers
|
||||
if os.path.isfile(target):
|
||||
with open(target, 'r') as f:
|
||||
content = f.read()
|
||||
if 'import pytest' in content or '@pytest' in content:
|
||||
return 'pytest'
|
||||
elif 'import unittest' in content or 'class Test' in content:
|
||||
return 'unittest'
|
||||
else:
|
||||
# Check for pytest.ini or setup.cfg
|
||||
if os.path.exists('pytest.ini') or os.path.exists('setup.cfg'):
|
||||
return 'pytest'
|
||||
return 'unittest'
|
||||
|
||||
# Check if it's JavaScript
|
||||
elif target.endswith('.js') or target.endswith('.test.js'):
|
||||
return 'jest'
|
||||
|
||||
return 'pytest' # Default
|
||||
|
||||
|
||||
def run_tests(target, framework=None, output_format='text'):
|
||||
"""Run tests and format output."""
|
||||
if not os.path.exists(target):
|
||||
print(f"Error: Target '{target}' not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Detect framework if not specified
|
||||
if framework is None:
|
||||
framework = detect_framework(target)
|
||||
|
||||
# Create appropriate runner
|
||||
if framework == 'pytest':
|
||||
runner = PytestRunner(target)
|
||||
elif framework == 'unittest':
|
||||
runner = UnittestRunner(target)
|
||||
elif framework == 'jest':
|
||||
runner = JestRunner(target)
|
||||
else:
|
||||
print(f"Error: Unsupported framework '{framework}'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\nRunning tests with {framework}...")
|
||||
print(f"Target: {target}\n")
|
||||
|
||||
# Run tests
|
||||
result = runner.run()
|
||||
|
||||
# Output results
|
||||
if output_format == 'json':
|
||||
print(json.dumps(result.to_dict(), indent=2))
|
||||
else:
|
||||
print("=" * 60)
|
||||
print("TEST RESULTS")
|
||||
print("=" * 60)
|
||||
print(f"Total: {result.total}")
|
||||
print(f"Passed: {result.passed} ✓")
|
||||
print(f"Failed: {result.failed} ✗")
|
||||
if result.errors > 0:
|
||||
print(f"Errors: {result.errors}")
|
||||
if result.skipped > 0:
|
||||
print(f"Skipped: {result.skipped}")
|
||||
if result.duration > 0:
|
||||
print(f"Duration: {result.duration:.2f}s")
|
||||
print()
|
||||
|
||||
if result.failures:
|
||||
print("FAILURES:")
|
||||
for failure in result.failures:
|
||||
print(f" - {failure['test']}")
|
||||
if failure.get('message'):
|
||||
print(f" {failure['message']}")
|
||||
print()
|
||||
|
||||
# Summary
|
||||
if result.failed == 0 and result.errors == 0:
|
||||
print("✓ All tests passed!")
|
||||
else:
|
||||
print(f"✗ {result.failed + result.errors} test(s) failed")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Run and format test results')
|
||||
parser.add_argument('target', help='Test file or directory to run')
|
||||
parser.add_argument('--framework', choices=['pytest', 'unittest', 'jest'],
|
||||
help='Testing framework to use (auto-detected if not specified)')
|
||||
parser.add_argument('--format', choices=['text', 'json'], default='text',
|
||||
help='Output format (default: text)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
run_tests(args.target, args.framework, args.format)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user