Codelint-MCP / src /utils /auto_fix.py
OsamaAliMid's picture
Add CodeLint MCP Premium Edition application
ec37394
"""
🔧 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