diff --git a/src/detectors/base.py b/src/detectors/base.py new file mode 100644 index 0000000..2fb9048 --- /dev/null +++ b/src/detectors/base.py @@ -0,0 +1,114 @@ +"""Base detector class for project detection.""" + +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Dict, List, Optional + + +class BaseDetector(ABC): + """Abstract base class for all detectors.""" + + @abstractmethod + def detect(self, project_path: Path) -> List[str]: + """Detect items from the project path. + + Args: + project_path: Path to the project directory. + + Returns: + List of detected item names (e.g., language names, framework names). + """ + pass + + @abstractmethod + def get_priority(self) -> int: + """Get the priority of this detector. + + Higher priority detectors are run first. + + Returns: + Integer priority value. + """ + pass + + def _find_files(self, project_path: Path, patterns: List[str]) -> List[Path]: + """Find files matching given patterns. + + Args: + project_path: Root directory to search. + patterns: List of filename patterns to match. + + Returns: + List of matching file paths. + """ + if not project_path.exists() or not project_path.is_dir(): + return [] + + found_files = [] + try: + for pattern in patterns: + matches = list(project_path.rglob(pattern)) + found_files.extend(matches) + except PermissionError: + pass + except OSError: + pass + + return found_files + + def _read_file_content(self, file_path: Path, max_size: int = 10240) -> Optional[str]: + """Read file content safely. + + Args: + file_path: Path to the file. + max_size: Maximum file size to read in bytes. + + Returns: + File content as string, or None if reading fails. + """ + try: + if file_path.stat().st_size > max_size: + return None + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + return f.read() + except (PermissionError, OSError, UnicodeDecodeError): + return None + + def _check_file_exists(self, project_path: Path, filenames: List[str]) -> List[str]: + """Check if any of the given filenames exist. + + Args: + project_path: Project root directory. + filenames: List of filenames to check. + + Returns: + List of existing filenames. + """ + existing = [] + for filename in filenames: + if (project_path / filename).exists(): + existing.append(filename) + return existing + + def _check_content_markers(self, project_path: Path, markers: Dict[str, str]) -> List[str]: + """Check for content markers in source files. + + Args: + project_path: Project root directory. + markers: Dict mapping marker names to search patterns. + + Returns: + List of found marker names. + """ + found = [] + try: + for file_path in project_path.rglob("*"): + if file_path.is_file() and file_path.suffix in ['.py', '.js', '.ts', '.go', '.java', '.rb', '.php', '.rs']: + content = self._read_file_content(file_path) + if content: + for name, pattern in markers.items(): + if name not in found and pattern.lower() in content.lower(): + found.append(name) + except (PermissionError, OSError): + pass + return found