""" 🔧 Auto-Fix Engine Executable fixes for common issues with preview and batch capabilities """ from typing import Dict, List, Any, Optional, Tuple import re import logging logger = logging.getLogger(__name__) class AutoFixer: """Auto-fix engine with preview and batch fix capabilities""" @staticmethod def can_auto_fix(issue: Dict[str, Any]) -> bool: """Check if issue can be automatically fixed""" rule_id = issue.get("rule_id", "").lower() message = issue.get("message", "").lower() fixable_rules = [ "missing-semicolon", "trailing-whitespace", "unused-import", "console.log", "debugger", "var-to-const", "var-to-let", "double-equals", "missing-docstring", "import-order", "quote-style", "indentation", "line-length" ] fixable_patterns = [ "missing semicolon", "trailing whitespace", "unused", "console.log", "debugger statement", "use const instead", "use let instead", "use === instead of ==", "missing docstring", "import order" ] return (rule_id in fixable_rules or any(pattern in message for pattern in fixable_patterns)) @staticmethod def generate_fix(code: str, issue: Dict[str, Any]) -> Optional[Tuple[str, str]]: """ Generate fix for an issue Returns: Tuple of (fixed_code, description) or None if can't fix """ rule_id = issue.get("rule_id", "").lower() message = issue.get("message", "").lower() line_num = issue.get("line", 1) lines = code.splitlines() if line_num < 1 or line_num > len(lines): return None line_idx = line_num - 1 original_line = lines[line_idx] fixed_line = original_line description = "" # Missing semicolon if "semicolon" in message or rule_id == "missing-semicolon": if not original_line.rstrip().endswith((';', '{', '}', ':')): fixed_line = original_line.rstrip() + ';' description = "Added missing semicolon" # console.log removal elif "console.log" in message or "console.log" in original_line: fixed_line = re.sub(r'console\.log\([^)]*\);?\s*', '', original_line) if not fixed_line.strip(): lines.pop(line_idx) description = "Removed console.log statement" return '\n'.join(lines), description description = "Removed console.log" # debugger statement elif "debugger" in message or "debugger" in original_line: fixed_line = re.sub(r'debugger;?\s*', '', original_line) if not fixed_line.strip(): lines.pop(line_idx) description = "Removed debugger statement" return '\n'.join(lines), description description = "Removed debugger" # Trailing whitespace elif "trailing" in message and "whitespace" in message: fixed_line = original_line.rstrip() + '\n' if original_line.endswith('\n') else original_line.rstrip() description = "Removed trailing whitespace" # var to const/let elif "var" in message and ("const" in message or "let" in message): if "const" in message: fixed_line = re.sub(r'\bvar\b', 'const', original_line, count=1) description = "Changed var to const" else: fixed_line = re.sub(r'\bvar\b', 'let', original_line, count=1) description = "Changed var to let" # == to === elif "===" in message or ("double" in message and "equals" in message): fixed_line = re.sub(r'([^=!])={2}([^=])', r'\1===\2', original_line) description = "Changed == to ===" # Assignment in condition elif "assignment" in message and "condition" in message: # Change = to == fixed_line = re.sub(r'if\s*\([^=]*?\s=\s', lambda m: m.group(0).replace('=', '=='), original_line) description = "Changed assignment to comparison" # Missing docstring (Python) elif "docstring" in message and "missing" in message: indent = len(original_line) - len(original_line.lstrip()) docstring = ' ' * (indent + 4) + '"""TODO: Add docstring"""' lines.insert(line_idx + 1, docstring) description = "Added placeholder docstring" return '\n'.join(lines), description # Unused variable (prefix with _) elif "unused" in message and "variable" in message: # Extract variable name var_match = re.search(r"variable '(\w+)'", message) if var_match: var_name = var_match.group(1) fixed_line = original_line.replace(var_name, f'_{var_name}', 1) description = f"Prefixed unused variable '{var_name}' with underscore" else: return None # Apply fix if fixed_line != original_line: lines[line_idx] = fixed_line return '\n'.join(lines), description return None @classmethod def preview_fix(cls, code: str, issue: Dict[str, Any]) -> Optional[Dict[str, Any]]: """ Preview what a fix would look like Returns: Dict with original_line, fixed_line, and description """ result = cls.generate_fix(code, issue) if not result: return None fixed_code, description = result line_num = issue.get("line", 1) original_lines = code.splitlines() fixed_lines = fixed_code.splitlines() # Get context (3 lines before and after) start = max(0, line_num - 4) end = min(len(original_lines), line_num + 3) return { "can_fix": True, "description": description, "original_snippet": '\n'.join(original_lines[start:end]), "fixed_snippet": '\n'.join(fixed_lines[start:end]), "line_number": line_num, "rule_id": issue.get("rule_id"), "message": issue.get("message") } @classmethod def apply_fix(cls, code: str, issue: Dict[str, Any]) -> Optional[str]: """Apply fix and return fixed code""" result = cls.generate_fix(code, issue) if result: return result[0] return None @classmethod def batch_fix(cls, code: str, issues: List[Dict[str, Any]]) -> Tuple[str, List[Dict[str, Any]]]: """ Apply multiple fixes at once Returns: Tuple of (fixed_code, list of applied fixes) """ fixed_code = code applied_fixes = [] # Sort issues by line number (descending) to avoid line number shifts sorted_issues = sorted(issues, key=lambda x: x.get("line", 0), reverse=True) for issue in sorted_issues: if cls.can_auto_fix(issue): result = cls.generate_fix(fixed_code, issue) if result: fixed_code, description = result applied_fixes.append({ "line": issue.get("line"), "rule_id": issue.get("rule_id"), "message": issue.get("message"), "fix_description": description }) return fixed_code, applied_fixes @classmethod def get_fixable_issues(cls, issues: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """Filter to only auto-fixable issues""" return [issue for issue in issues if cls.can_auto_fix(issue)] @classmethod def get_fix_summary(cls, code: str, issues: List[Dict[str, Any]]) -> Dict[str, Any]: """Generate summary of what can be fixed""" fixable = cls.get_fixable_issues(issues) # Preview all fixes previews = [] for issue in fixable: preview = cls.preview_fix(code, issue) if preview: previews.append(preview) return { "total_issues": len(issues), "fixable_count": len(fixable), "fixable_percentage": round(len(fixable) / len(issues) * 100, 1) if issues else 0, "fixable_issues": fixable, "previews": previews[:10] # Limit to 10 previews } def format_fix_report(fix_summary: Dict[str, Any]) -> str: """Format fix summary into readable report""" total = fix_summary.get("total_issues", 0) fixable = fix_summary.get("fixable_count", 0) percentage = fix_summary.get("fixable_percentage", 0) report = f""" # 🔧 Auto-Fix Report ## 📊 Summary - **Total Issues**: {total} - **Auto-Fixable**: {fixable} ({percentage}%) - **Requires Manual Fix**: {total - fixable} """ previews = fix_summary.get("previews", []) if previews: report += "## 🔍 Fix Previews\n\n" for i, preview in enumerate(previews, 1): line = preview.get("line_number") desc = preview.get("description") rule = preview.get("rule_id") report += f"### {i}. Line {line}: {desc}\n" report += f"**Rule**: `{rule}`\n\n" report += "**Before:**\n```\n" report += preview.get("original_snippet", "") report += "\n```\n\n" report += "**After:**\n```\n" report += preview.get("fixed_snippet", "") report += "\n```\n\n" if fixable > 0: report += "✨ **Ready to apply fixes!**\n" return report