diff --git a/vibeguard/patterns/manager.py b/vibeguard/patterns/manager.py new file mode 100644 index 0000000..5ac174e --- /dev/null +++ b/vibeguard/patterns/manager.py @@ -0,0 +1,408 @@ +"""Pattern manager for VibeGuard.""" + +import json +from pathlib import Path +from typing import Any + +from vibeguard.patterns.definitions import Pattern, Severity + + +class PatternManager: + """Manages anti-pattern definitions for VibeGuard.""" + + def __init__(self) -> None: + self.patterns: dict[str, Pattern] = {} + + def load_all_patterns(self) -> None: + """Load all pattern definitions.""" + self._load_python_patterns() + self._load_javascript_patterns() + self._load_typescript_patterns() + self._load_go_patterns() + self._load_rust_patterns() + self._load_general_patterns() + + def _load_python_patterns(self) -> None: + """Load Python-specific patterns.""" + python_patterns = [ + Pattern( + id="PY001", + name="Magic String", + description="Long string literal without explanation", + severity=Severity.WARNING, + languages=["python"], + message="Magic string detected", + suggestion="Extract to a named constant", + category="maintainability", + ), + Pattern( + id="PY002", + name="Hardcoded Value", + description="Hardcoded numeric value that should be a constant", + severity=Severity.WARNING, + languages=["python"], + message="Hardcoded numeric value detected", + suggestion="Extract to a named constant", + category="maintainability", + ), + Pattern( + id="PY003", + name="Missing Return Type", + description="Function without return type annotation", + severity=Severity.INFO, + languages=["python"], + message="Function is missing return type annotation", + suggestion="Add return type annotation for type safety", + category="type-safety", + ), + Pattern( + id="PY004", + name="Missing Docstring", + description="Function without docstring", + severity=Severity.INFO, + languages=["python"], + message="Function is missing docstring", + suggestion="Add a docstring explaining the function's purpose", + category="documentation", + ), + Pattern( + id="PY005", + name="Bare Except", + description="Catching all exceptions without specific type", + severity=Severity.ERROR, + languages=["python"], + message="Bare except clause detected", + suggestion="Catch specific exceptions or use 'except Exception'", + category="error-handling", + ), + Pattern( + id="PY006", + name="Type Check Pattern", + description="Using type() instead of isinstance()", + severity=Severity.WARNING, + languages=["python"], + message="Using type() for type checking", + suggestion="Use isinstance() for proper inheritance support", + category="type-safety", + ), + ] + + for pattern in python_patterns: + self.patterns[pattern.id] = pattern + + def _load_javascript_patterns(self) -> None: + """Load JavaScript-specific patterns.""" + js_patterns = [ + Pattern( + id="JS001", + name="Console Log", + description="Console.log statement left in code", + severity=Severity.WARNING, + languages=["javascript"], + message="Console.log statement detected", + suggestion="Use a logging library or remove debug statements", + category="debugging", + ), + Pattern( + id="JS002", + name="Var Keyword", + description="Using var instead of const/let", + severity=Severity.WARNING, + languages=["javascript"], + message="'var' keyword detected", + suggestion="Use 'const' or 'let' for proper block scoping", + category="best-practices", + ), + Pattern( + id="JS003", + name="Magic String", + description="Long string literal without explanation", + severity=Severity.WARNING, + languages=["javascript"], + message="Magic string detected", + suggestion="Extract to a named constant", + category="maintainability", + ), + Pattern( + id="JS004", + name="Hardcoded Value", + description="Hardcoded numeric value", + severity=Severity.WARNING, + languages=["javascript"], + message="Hardcoded numeric value detected", + suggestion="Extract to a named constant", + category="maintainability", + ), + Pattern( + id="JS005", + name="Inconsistent Error Handling", + description="Generic error variable name", + severity=Severity.INFO, + languages=["javascript"], + message="Generic error variable name", + suggestion="Use more descriptive error variable names", + category="readability", + ), + Pattern( + id="JS006", + name="Unnecessary Promise Wrapper", + description="Unnecessary Promise constructor wrapper", + severity=Severity.WARNING, + languages=["javascript"], + message="Unnecessary Promise wrapper detected", + suggestion="Return the value directly instead of wrapping", + category="performance", + ), + ] + + for pattern in js_patterns: + self.patterns[pattern.id] = pattern + + def _load_typescript_patterns(self) -> None: + """Load TypeScript-specific patterns.""" + ts_patterns = [ + Pattern( + id="TS001", + name="Any Type Usage", + description="Using 'any' type defeats TypeScript's purpose", + severity=Severity.WARNING, + languages=["typescript"], + message="'any' type detected", + suggestion="Use specific types or 'unknown' with type narrowing", + category="type-safety", + ), + Pattern( + id="TS002", + name="Interface Naming", + description="Interface name should use PascalCase", + severity=Severity.INFO, + languages=["typescript"], + message="Interface name should start with capital letter", + suggestion="Rename interface to use PascalCase", + category="naming-conventions", + ), + Pattern( + id="TS003", + name="Enum Usage", + description="Enum vs const object consideration", + severity=Severity.INFO, + languages=["typescript"], + message="Enum detected", + suggestion="Consider using const objects with 'as const' for better tree-shaking", + category="best-practices", + ), + ] + + for pattern in ts_patterns: + self.patterns[pattern.id] = pattern + + def _load_go_patterns(self) -> None: + """Load Go-specific patterns.""" + go_patterns = [ + Pattern( + id="GO001", + name="Ignored Error", + description="Error being ignored with blank identifier", + severity=Severity.ERROR, + languages=["go"], + message="Error being ignored", + suggestion="Handle the error properly instead of ignoring it", + category="error-handling", + ), + Pattern( + id="GO002", + name="Context Background", + description="Using context.Background() in production code", + severity=Severity.WARNING, + languages=["go"], + message="context.Background() should not be passed to functions", + suggestion="Use context.TODO() or request-scoped context", + category="best-practices", + ), + Pattern( + id="GO003", + name="Potential Goroutine Leak", + description="Goroutine without proper lifetime management", + severity=Severity.WARNING, + languages=["go"], + message="Goroutine started - ensure it won't leak", + suggestion="Use wait groups or context to control goroutine lifetime", + category="concurrency", + ), + Pattern( + id="GO004", + name="Magic String", + description="Long string literal without explanation", + severity=Severity.WARNING, + languages=["go"], + message="Magic string detected", + suggestion="Extract to a named constant", + category="maintainability", + ), + Pattern( + id="GO005", + name="Error Wrapping", + description="Error not wrapped with %w verb", + severity=Severity.WARNING, + languages=["go"], + message="Error formatting without %w", + suggestion="Use %w verb to wrap errors for proper error chains", + category="error-handling", + ), + Pattern( + id="GO006", + name="Naked Return", + description="Naked return in function with multiple return values", + severity=Severity.WARNING, + languages=["go"], + message="Naked return in function with multiple return values", + suggestion="Use named return values or explicit returns", + category="readability", + ), + ] + + for pattern in go_patterns: + self.patterns[pattern.id] = pattern + + def _load_rust_patterns(self) -> None: + """Load Rust-specific patterns.""" + rust_patterns = [ + Pattern( + id="RS001", + name="Unnecessary Clone", + description="Calling clone() on a Copy type", + severity=Severity.WARNING, + languages=["rust"], + message="Unnecessary .clone() - type implements Copy trait", + suggestion="Remove .clone() as the type will be automatically copied", + category="performance", + ), + Pattern( + id="RS002", + name="Allow Unused", + description="Allowing unused code instead of removing it", + severity=Severity.INFO, + languages=["rust"], + message="#[allow(unused)] detected", + suggestion="Remove unused code instead of suppressing warnings", + category="best-practices", + ), + Pattern( + id="RS003", + name="Expect Panic", + description="Using expect() which can panic", + severity=Severity.WARNING, + languages=["rust"], + message=".expect() can panic", + suggestion="Use ? operator or proper error handling instead", + category="error-handling", + ), + Pattern( + id="RS004", + name="Magic String", + description="Long string literal without explanation", + severity=Severity.WARNING, + languages=["rust"], + message="Magic string detected", + suggestion="Extract to a named constant", + category="maintainability", + ), + Pattern( + id="RS005", + name="Unnecessary Boxing", + description="Unnecessary Box::new() usage", + severity=Severity.INFO, + languages=["rust"], + message="Unnecessary Box::new()", + suggestion="Only use Box for recursive types, trait objects, or large stacks", + category="performance", + ), + Pattern( + id="RS006", + name="Into Iterator Consumption", + description="into_iter() consumes the collection", + severity=Severity.INFO, + languages=["rust"], + message="into_iter() consumes the collection", + suggestion="Use .iter() for borrowing, .iter_mut() for mutable borrowing", + category="best-practices", + ), + ] + + for pattern in rust_patterns: + self.patterns[pattern.id] = pattern + + def _load_general_patterns(self) -> None: + """Load general patterns applicable to all languages.""" + general_patterns = [ + Pattern( + id="GEN001", + name="Empty Catch Block", + description="Empty catch/except block that ignores errors", + severity=Severity.ERROR, + languages=["python", "javascript", "typescript", "go"], + message="Empty catch block - errors are being silently ignored", + suggestion="Handle the error or log it", + category="error-handling", + ), + Pattern( + id="GEN002", + name="Debug Code", + description="Debug code left in production", + severity=Severity.WARNING, + languages=["python", "javascript", "typescript", "go", "rust"], + message="Debug code detected", + suggestion="Remove or disable debug code in production", + category="debugging", + ), + Pattern( + id="GEN003", + name="Inconsistent Naming", + description="Inconsistent naming convention in code", + severity=Severity.INFO, + languages=["python", "javascript", "typescript", "go", "rust"], + message="Inconsistent naming convention detected", + suggestion="Use consistent naming (camelCase, snake_case, PascalCase)", + category="naming-conventions", + ), + Pattern( + id="GEN004", + name="Deep Nesting", + description="Code with excessive nesting depth", + severity=Severity.WARNING, + languages=["python", "javascript", "typescript", "go", "rust"], + message="Excessive nesting depth detected", + suggestion="Consider refactoring with early returns or extracted functions", + category="readability", + ), + ] + + for pattern in general_patterns: + self.patterns[pattern.id] = pattern + + def get_pattern(self, pattern_id: str) -> Pattern | None: + """Get a pattern by its ID.""" + return self.patterns.get(pattern_id) + + def get_patterns_by_language(self, language: str) -> list[Pattern]: + """Get all patterns for a specific language.""" + return [ + p for p in self.patterns.values() if language in p.languages or "all" in p.languages + ] + + def get_patterns_by_severity(self, severity: Severity) -> list[Pattern]: + """Get all patterns with a specific severity.""" + return [p for p in self.patterns.values() if p.severity == severity] + + def to_dict(self) -> dict[str, Any]: + """Convert all patterns to a dictionary.""" + return {pid: { + "id": pid, + "name": p.name, + "description": p.description, + "severity": p.severity.value, + "languages": p.languages, + "message": p.message, + "suggestion": p.suggestion, + "category": p.category, + } for pid, p in self.patterns.items()}