Spaces:
Sleeping
Sleeping
| """ | |
| 🔧 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""" | |
| 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)) | |
| 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 | |
| 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") | |
| } | |
| 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 | |
| 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 | |
| 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)] | |
| 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 | |