mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-04 04:23:02 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user