diff --git a/src/analyzers/base.py b/src/analyzers/base.py new file mode 100644 index 0000000..81d298d --- /dev/null +++ b/src/analyzers/base.py @@ -0,0 +1,129 @@ +"""Base analyzer class and common functionality.""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from enum import Enum +from pathlib import Path +from typing import Optional +import tree_sitter + + +class SeverityLevel(str, Enum): + """Severity levels for findings.""" + + CRITICAL = "critical" + HIGH = "high" + MEDIUM = "medium" + LOW = "low" + + +class FindingCategory(str, Enum): + """Categories of findings.""" + + SECURITY = "security" + ANTIPATTERN = "antipattern" + SECRET = "secret" + PERFORMANCE = "performance" + + +@dataclass +class Finding: + """Represents a code issue finding.""" + + rule_id: str + rule_name: str + severity: SeverityLevel + category: FindingCategory + message: str + suggestion: str + file_path: Path + line_number: int + column: int + end_line: Optional[int] = None + end_column: Optional[int] = None + node: Optional[tree_sitter.Node] = None + + def to_dict(self) -> dict: + return { + "rule_id": self.rule_id, + "rule_name": self.rule_name, + "severity": self.severity.value, + "category": self.category.value, + "message": self.message, + "suggestion": self.suggestion, + "file_path": str(self.file_path), + "line_number": self.line_number, + "column": self.column, + "end_line": self.end_line, + "end_column": self.end_column, + } + + +@dataclass +class AnalysisResult: + """Result of analyzing a file.""" + + file_path: Path + findings: list[Finding] = field(default_factory=list) + error: Optional[str] = None + lines_of_code: int = 0 + + def has_issues(self) -> bool: + return len(self.findings) > 0 + + def critical_count(self) -> int: + return sum(1 for f in self.findings if f.severity == SeverityLevel.CRITICAL) + + def high_count(self) -> int: + return sum(1 for f in self.findings if f.severity == SeverityLevel.HIGH) + + def medium_count(self) -> int: + return sum(1 for f in self.findings if f.severity == SeverityLevel.MEDIUM) + + def low_count(self) -> int: + return sum(1 for f in self.findings if f.severity == SeverityLevel.LOW) + + def summary(self) -> dict: + return { + "critical": self.critical_count(), + "high": self.high_count(), + "medium": self.medium_count(), + "low": self.low_count(), + "total": len(self.findings), + "lines_of_code": self.lines_of_code, + } + + +class Analyzer(ABC): + """Abstract base class for analyzers.""" + + @abstractmethod + def analyze( + self, source_code: str, file_path: Path, tree: tree_sitter.Tree + ) -> list[Finding]: + """Analyze source code and return findings.""" + pass + + @abstractmethod + def rule_id(self) -> str: + """Return unique identifier for the rule.""" + pass + + @abstractmethod + def rule_name(self) -> str: + """Return human-readable rule name.""" + pass + + @abstractmethod + def severity(self) -> SeverityLevel: + """Return severity level of the rule.""" + pass + + @abstractmethod + def category(self) -> FindingCategory: + """Return category of the rule.""" + pass + + def supports_language(self, language: str) -> bool: + """Check if analyzer supports a language.""" + return True