mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-03 20:13:03 +00:00
234 lines
5.5 KiB
Go
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
|
|
}
|