Files
Devour/internal/quality/scanner.go
T
Tomas Dvorak 55885a0e8f first commit
2026-02-22 10:42:17 +01:00

234 lines
5.5 KiB
Go

package quality
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
)
// Scanner orchestrates the code quality scanning process
type Scanner struct {
detectors map[string]Detector
finder FileFinder
config *Config
}
// NewScanner creates a new quality scanner
func NewScanner(config *Config) *Scanner {
return &Scanner{
detectors: make(map[string]Detector),
config: config,
}
}
// RegisterDetector registers a detector with the scanner
func (s *Scanner) RegisterDetector(detector Detector) {
s.detectors[detector.Name()] = detector
}
// SetFileFinder sets the file finder for the scanner
func (s *Scanner) SetFileFinder(finder FileFinder) {
s.finder = finder
}
// Scan performs a comprehensive quality scan
func (s *Scanner) Scan(ctx context.Context) (*ScanResult, error) {
start := time.Now()
log.Printf("Starting quality scan for path: %s", s.config.Path)
allFindings := make([]Finding, 0)
filesChecked := 0
// Determine language if not specified
language := s.config.Language
if language == "" {
language = s.detectLanguage(s.config.Path)
log.Printf("Auto-detected language: %s", language)
}
// Get source files
files, err := s.getSourceFiles(s.config.Path, language)
if err != nil {
return nil, fmt.Errorf("failed to get source files: %w", err)
}
filesChecked = len(files)
log.Printf("Found %d source files to analyze", filesChecked)
// Run all detectors
for name, detector := range s.detectors {
log.Printf("Running detector: %s", name)
// Skip language-specific detectors for different languages
if langDetector, ok := detector.(LanguageDetector); ok {
supported := langDetector.SupportedLanguages()
if !contains(supported, language) {
log.Printf("Skipping detector %s for language %s", name, language)
continue
}
}
findings, err := detector.Detect(ctx, s.config.Path, s.config)
if err != nil {
log.Printf("Detector %s failed: %v", name, err)
continue
}
// Filter findings based on exclude patterns
filtered := s.filterFindings(findings)
allFindings = append(allFindings, filtered...)
log.Printf("Detector %s found %d issues", name, len(filtered))
}
// Calculate scores
score, strictScore := s.calculateScores(allFindings)
duration := time.Since(start)
result := &ScanResult{
Findings: allFindings,
Score: score,
StrictScore: strictScore,
FilesChecked: filesChecked,
Duration: duration.String(),
Timestamp: time.Now(),
}
log.Printf("Scan completed in %s: %d findings, score: %d (strict: %d)",
duration, len(allFindings), score, strictScore)
return result, nil
}
// detectLanguage attempts to auto-detect the project language
func (s *Scanner) detectLanguage(path string) string {
// Check for marker files
markers := map[string]string{
"go.mod": "go",
"package.json": "typescript",
"tsconfig.json": "typescript",
"requirements.txt": "python",
"setup.py": "python",
"pyproject.toml": "python",
"pom.xml": "java",
"build.gradle": "java",
"Cargo.toml": "rust",
"composer.json": "php",
}
for file, lang := range markers {
if _, err := filepath.Abs(filepath.Join(path, file)); err == nil {
if _, err := filepath.Glob(filepath.Join(path, file)); err == nil {
return lang
}
}
}
// Default to Go if no markers found
return "go"
}
// getSourceFiles gets all source files for the given language and path
func (s *Scanner) getSourceFiles(path, language string) ([]string, error) {
if s.finder != nil {
return s.finder.FindFiles(path, language)
}
// Fallback to basic file extension matching
extensions := map[string][]string{
"go": {".go"},
"typescript": {".ts", ".tsx"},
"python": {".py"},
"java": {".java"},
"rust": {".rs"},
"javascript": {".js", ".jsx"},
}
langExts, ok := extensions[language]
if !ok {
langExts = []string{".go"} // default to Go
}
var files []string
err := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
// Skip hidden directories and common exclude dirs
base := filepath.Base(filePath)
if strings.HasPrefix(base, ".") || base == "node_modules" || base == "vendor" {
return filepath.SkipDir
}
return nil
}
// Check file extension
ext := filepath.Ext(filePath)
for _, langExt := range langExts {
if ext == langExt {
if !ShouldExclude(filePath, s.config.Exclude) {
files = append(files, filePath)
}
break
}
}
return nil
})
return files, err
}
// filterFindings filters findings based on exclude patterns
func (s *Scanner) filterFindings(findings []Finding) []Finding {
if len(s.config.Exclude) == 0 {
return findings
}
var filtered []Finding
for _, finding := range findings {
if !ShouldExclude(finding.File, s.config.Exclude) {
filtered = append(filtered, finding)
}
}
return filtered
}
// calculateScores calculates quality scores based on findings
func (s *Scanner) calculateScores(findings []Finding) (int, int) {
totalScore := 0
strictScore := 0
for _, finding := range findings {
weight := int(finding.Severity)
score := finding.Score * weight
totalScore += score
// Strict score includes open and wontfix findings
if finding.Status == StatusOpen || finding.Status == StatusWontfix {
strictScore += score
}
}
return totalScore, strictScore
}
// contains checks if a slice contains a string
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}