Files
config-auditor-cli/config_auditor/fixes.py
7000pctAUTO 5f60710249
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
Add rules, fixes, and llm modules
2026-01-30 18:03:17 +00:00

164 lines
5.1 KiB
Python

from abc import ABC, abstractmethod
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional
import json
import shutil
@dataclass
class Fix:
file: Path
description: str
original_content: str
fixed_content: str
reversible: bool = True
class FixerBase(ABC):
@property
@abstractmethod
def fix_id(self) -> str:
pass
@abstractmethod
def can_fix(self, data: Dict[str, Any], file_path: Path) -> bool:
pass
@abstractmethod
def apply_fix(self, data: Dict[str, Any], file_path: Path) -> Dict[str, Any]:
pass
def revert_fix(self, original: str) -> str:
return original
class DeprecatedPackageFixer(FixerBase):
DEPRECATED_MAPPING = {
"request": "requests",
"urllib2": "urllib.request",
"httplib": "http.client",
}
@property
def fix_id(self) -> str:
return "replace-deprecated-package"
def can_fix(self, data: Dict[str, Any], file_path: Path) -> bool:
deps = {**data.get("dependencies", {}), **data.get("devDependencies", {})}
return any(pkg in self.DEPRECATED_MAPPING for pkg in deps)
def apply_fix(self, data: Dict[str, Any], file_path: Path) -> Dict[str, Any]:
for key in ["dependencies", "devDependencies"]:
if key in data:
for old_pkg, new_pkg in self.DEPRECATED_MAPPING.items():
if old_pkg in data[key]:
data[key][new_pkg] = data[key].pop(old_pkg)
return data
class AddTestScriptFixer(FixerBase):
@property
def fix_id(self) -> str:
return "add-test-script"
def can_fix(self, data: Dict[str, Any], file_path: Path) -> bool:
scripts = data.get("scripts", {})
return "test" not in scripts
def apply_fix(self, data: Dict[str, Any], file_path: Path) -> Dict[str, Any]:
if "scripts" not in data:
data["scripts"] = {}
if "test" not in data["scripts"]:
data["scripts"]["test"] = "echo 'Error: no test specified' && exit 1"
if "build" not in data["scripts"]:
data["scripts"]["build"] = "tsc" if file_path.name.endswith(".json") else "echo 'No build script'"
return data
class EnableStrictModeFixer(FixerBase):
@property
def fix_id(self) -> str:
return "enable-strict-mode"
def can_fix(self, data: Dict[str, Any], file_path: Path) -> bool:
compiler_opts = data.get("compilerOptions", {})
return compiler_opts.get("strict") is not True
def apply_fix(self, data: Dict[str, Any], file_path: Path) -> Dict[str, Any]:
if "compilerOptions" not in data:
data["compilerOptions"] = {}
data["compilerOptions"]["strict"] = True
return data
class Fixer:
FIXERS = [
DeprecatedPackageFixer(),
AddTestScriptFixer(),
EnableStrictModeFixer(),
]
def __init__(self, dry_run: bool = False, force: bool = False, confirm_func: Optional[Callable[[str], bool]] = None):
self.dry_run = dry_run
self.force = force
self.confirm_func = confirm_func
self.backup_dir = Path(".config_auditor_backup")
self.fixes_applied: List[Fix] = []
def _confirm(self, message: str) -> bool:
if self.confirm_func:
return self.confirm_func(message)
if self.force:
return True
return False
def fix_config(self, file_path: Path, format_type: str, content: str) -> int:
from config_auditor.parsers import ParserFactory
parser = ParserFactory()
data = parser.parse(format_type, content)
if data is None:
return 0
original_content = content
fix_count = 0
for fixer in self.FIXERS:
if fixer.can_fix(data, file_path):
if not self.force and not self.dry_run:
if self._confirm(f"Apply fix for {fixer.fix_id}?"):
pass
else:
continue
new_data = fixer.apply_fix(data, file_path)
fixed_content = parser.dump(format_type, new_data)
if fixed_content:
if not self.dry_run:
if self.backup_dir.exists() is False:
self.backup_dir.mkdir(exist_ok=True)
backup_path = self.backup_dir / f"{file_path.name}.bak"
shutil.copy(file_path, backup_path)
file_path.write_text(fixed_content)
self.fixes_applied.append(Fix(
file=file_path,
description=fixer.fix_id,
original_content=original_content,
fixed_content=fixed_content
))
fix_count += 1
return fix_count
def revert_last_fix(self):
if self.fixes_applied:
last_fix = self.fixes_applied.pop()
last_fix.file.write_text(last_fix.original_content)
def get_fixes(self) -> List[Fix]:
return self.fixes_applied