from abc import ABC, abstractmethod from dataclasses import dataclass, field from pathlib import Path from typing import Any, Dict, List, Optional from packaging import version import re @dataclass class Issue: severity: str category: str message: str file: Path line: Optional[int] = None suggestion: Optional[str] = None rule_id: Optional[str] = None @dataclass class RuleResult: passed: bool message: str severity: str suggestion: Optional[str] = None class Rule(ABC): @property @abstractmethod def rule_id(self) -> str: pass @property @abstractmethod def severity(self) -> str: pass @abstractmethod def evaluate(self, data: Dict[str, Any], file_path: Path) -> RuleResult: pass class DeprecatedPackageRule(Rule): DEPRECATED_PACKAGES = { "request": "requests", "urllib2": "urllib.request", "httplib": "http.client", "StringIO": "io.StringIO", } @property def rule_id(self) -> str: return "deprecated-package" @property def severity(self) -> str: return "critical" def evaluate(self, data: Dict[str, Any], file_path: Path) -> RuleResult: deps = data.get("dependencies", {}) dev_deps = data.get("devDependencies", {}) all_deps = {**deps, **dev_deps} for pkg in all_deps: if pkg in self.DEPRECATED_PACKAGES: return RuleResult( passed=False, message=f"Deprecated package '{pkg}' found. Use '{self.DEPRECATED_PACKAGES[pkg]}' instead.", severity=self.severity, suggestion=f"Replace '{pkg}' with '{self.DEPRECATED_PACKAGES[pkg]}' in dependencies" ) return RuleResult(passed=True, message="No deprecated packages found", severity="info") class OutdatedVersionRule(Rule): @property def rule_id(self) -> str: return "outdated-version" @property def severity(self) -> str: return "warning" def evaluate(self, data: Dict[str, Any], file_path: Path) -> RuleResult: deps = data.get("dependencies", {}) dev_deps = data.get("devDependencies", {}) all_deps = {**deps, **dev_deps} outdated = [] for pkg, ver in all_deps.items(): if ver and ver.startswith("^"): try: v = version.parse(ver[1:]) if v.major == 0: outdated.append(f"{pkg}@{ver} (major version 0, may have breaking changes)") except Exception: pass if outdated: return RuleResult( passed=False, message=f"Potentially outdated dependencies: {', '.join(outdated)}", severity=self.severity, suggestion="Review these dependencies for stability concerns" ) return RuleResult(passed=True, message="Dependencies appear up to date", severity="info") class MissingTypeCheckingRule(Rule): @property def rule_id(self) -> str: return "missing-type-checking" @property def severity(self) -> str: return "warning" def evaluate(self, data: Dict[str, Any], file_path: Path) -> RuleResult: if data.get("compilerOptions", {}).get("strict") is not True: return RuleResult( passed=False, message="TypeScript strict mode is not enabled", severity=self.severity, suggestion='Set "compilerOptions": { "strict": true } in tsconfig.json' ) return RuleResult(passed=True, message="TypeScript strict mode is enabled", severity="info") class SecurityVulnerabilityRule(Rule): VULNERABLE_PATTERNS = [ (r"\"version\":\s*\"0\.\d+\.\d+\"", "Versions starting with 0.x may have security issues"), (r"\"private\":\s*false", "Package should not be public if marked private"), ] @property def rule_id(self) -> str: return "security-vulnerability" @property def severity(self) -> str: return "critical" def evaluate(self, data: Dict[str, Any], file_path: Path) -> RuleResult: import json content = json.dumps(data) for pattern, message in self.VULNERABLE_PATTERNS: if re.search(pattern, content): return RuleResult( passed=False, message=message, severity=self.severity, suggestion="Review and update the configuration" ) return RuleResult(passed=True, message="No obvious security vulnerabilities detected", severity="info") class MissingScriptsRule(Rule): @property def rule_id(self) -> str: return "missing-scripts" @property def severity(self) -> str: return "warning" def evaluate(self, data: Dict[str, Any], file_path: Path) -> RuleResult: scripts = data.get("scripts", {}) required_scripts = ["test", "build"] missing = [s for s in required_scripts if s not in scripts] if missing: return RuleResult( passed=False, message=f"Missing recommended scripts: {', '.join(missing)}", severity=self.severity, suggestion=f"Add scripts for: {', '.join(missing)}" ) return RuleResult(passed=True, message="Required scripts are present", severity="info") class PythonProjectMetaRule(Rule): @property def rule_id(self) -> str: return "python-project-meta" @property def severity(self) -> str: return "info" def evaluate(self, data: Dict[str, Any], file_path: Path) -> RuleResult: issues = [] if "tool" in data: if "poetry" in data.get("tool", {}): poetry = data["tool"]["poetry"] if not poetry.get("name"): issues.append("Missing poetry project name") if not poetry.get("version"): issues.append("Missing poetry project version") elif "pytest" in data.get("tool", {}): if not data["tool"]["pytest"].get("testpaths"): issues.append("Missing pytest testpaths") if issues: return RuleResult( passed=False, message=f"Python project issues: {', '.join(issues)}", severity=self.severity, suggestion="Add missing project metadata" ) return RuleResult(passed=True, message="Python project metadata is complete", severity="info") class RuleRegistry: RULES = { "json": [ DeprecatedPackageRule(), OutdatedVersionRule(), MissingTypeCheckingRule(), SecurityVulnerabilityRule(), MissingScriptsRule(), ], "yaml": [ SecurityVulnerabilityRule(), ], "toml": [ PythonProjectMetaRule(), ], } def __init__(self): self._rules = {k: list(v) for k, v in self.RULES.items()} def evaluate(self, format_type: str, data: Dict[str, Any], file_path: Path) -> List[Issue]: issues = [] rules = self._rules.get(format_type, []) for rule in rules: result = rule.evaluate(data, file_path) if not result.passed: issues.append(Issue( severity=result.severity, category=rule.rule_id, message=result.message, file=file_path, suggestion=result.suggestion, rule_id=rule.rule_id )) return issues def add_rule(self, format_type: str, rule: Rule): if format_type not in self._rules: self._rules[format_type] = [] self._rules[format_type].append(rule)