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 len(supported) > 0 && !contains(supported, language) { log.Printf("Skipping detector %s for language %s", name, language) continue } } findings, err := s.runDetectorSafely(ctx, detector, name) if err != nil { log.Printf("Detector %s failed: %v", name, err) allFindings = append(allFindings, Finding{ ID: fmt.Sprintf("detector_error::%s", name), Type: "detector_error", Title: fmt.Sprintf("Detector failed: %s", name), Description: fmt.Sprintf("Detector %s failed during scan: %v", name, err), File: s.config.Path, Line: 1, Severity: SeverityT2, Score: 0, Status: StatusOpen, Metadata: map[string]string{ "detector": name, "error": err.Error(), }, CreatedAt: time.Now(), UpdatedAt: time.Now(), }) 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 } func (s *Scanner) runDetectorSafely(ctx context.Context, detector Detector, name string) (_ []Finding, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("detector panic in %s: %v", name, r) } }() return detector.Detect(ctx, s.config.Path, s.config) } // detectLanguage attempts to auto-detect the project language func (s *Scanner) detectLanguage(path string) string { // Keep auto-detection intentionally conservative until full multi-language // scanner behavior is validated in tests. if _, err := os.Stat(filepath.Join(path, "go.mod")); err == nil { return "go" } // 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 filePath != path && (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 }