mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-03 20:13:03 +00:00
update
This commit is contained in:
@@ -398,8 +398,8 @@ func NewSecretsDetector() *SecretsDetector {
|
||||
{Name: "GitHub OAuth", Pattern: regexp.MustCompile(`gho_[0-9a-zA-Z]{36}`), Severity: quality.SeverityT4},
|
||||
{Name: "GitHub App Token", Pattern: regexp.MustCompile(`(ghu|ghs)_[0-9a-zA-Z]{36}`), Severity: quality.SeverityT4},
|
||||
{Name: "Slack Token", Pattern: regexp.MustCompile(`xox[baprs]-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9]{24}`), Severity: quality.SeverityT4},
|
||||
{Name: "RSA Private Key", Pattern: regexp.MustCompile(`-----BEGIN RSA PRIVATE KEY-----`), Severity: quality.SeverityT4},
|
||||
{Name: "Private Key", Pattern: regexp.MustCompile(`-----BEGIN PRIVATE KEY-----`), Severity: quality.SeverityT4},
|
||||
{Name: "RSA Private Key", Pattern: regexp.MustCompile(`-----BEGIN ` + `RSA PRIVATE KEY-----`), Severity: quality.SeverityT4},
|
||||
{Name: "Private Key", Pattern: regexp.MustCompile(`-----BEGIN ` + `PRIVATE KEY-----`), Severity: quality.SeverityT4},
|
||||
{Name: "JWT", Pattern: regexp.MustCompile(`eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*`), Severity: quality.SeverityT3},
|
||||
{Name: "Generic API Key", Pattern: regexp.MustCompile(`(?i)(api_key|apikey|secret|password|token)\s*[=:]\s*['"][^'"]{8,}['"]`), Severity: quality.SeverityT3},
|
||||
{Name: "DB Connection String", Pattern: regexp.MustCompile(`(?i)(mysql|postgres|mongodb)://[^:]+:[^@]+@[^/]+`), Severity: quality.SeverityT4},
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package quality
|
||||
|
||||
import "strings"
|
||||
|
||||
type docsEvidence struct {
|
||||
URLs []string
|
||||
Rationale string
|
||||
Confidence string
|
||||
}
|
||||
|
||||
var defaultEvidenceByType = map[string]docsEvidence{
|
||||
"complexity_ast": {
|
||||
URLs: []string{"https://go.dev/doc/effective_go", "https://go.dev/wiki/CodeReviewComments"},
|
||||
Rationale: "High complexity correlates with maintainability and defect risk; official style guidance recommends smaller focused functions.",
|
||||
Confidence: "0.82",
|
||||
},
|
||||
"god_function": {
|
||||
URLs: []string{"https://go.dev/doc/effective_go", "https://go.dev/wiki/CodeReviewComments"},
|
||||
Rationale: "Large multi-responsibility functions usually violate readability and testability guidance.",
|
||||
Confidence: "0.84",
|
||||
},
|
||||
"unused_import": {
|
||||
URLs: []string{"https://pkg.go.dev/cmd/go", "https://pkg.go.dev/go/importer"},
|
||||
Rationale: "Unused imports break build hygiene and indicate stale code paths.",
|
||||
Confidence: "0.95",
|
||||
},
|
||||
"dead_code": {
|
||||
URLs: []string{"https://pkg.go.dev/cmd/go", "https://go.dev/wiki/CodeReviewComments"},
|
||||
Rationale: "Unreachable or unused symbols increase maintenance overhead with no runtime value.",
|
||||
Confidence: "0.90",
|
||||
},
|
||||
"dead_code_enhanced": {
|
||||
URLs: []string{"https://pkg.go.dev/cmd/go", "https://go.dev/wiki/CodeReviewComments"},
|
||||
Rationale: "Unreachable or unused symbols increase maintenance overhead with no runtime value.",
|
||||
Confidence: "0.90",
|
||||
},
|
||||
"duplication": {
|
||||
URLs: []string{"https://go.dev/wiki/CodeReviewComments"},
|
||||
Rationale: "Duplication increases change cost and risk of inconsistent bug fixes.",
|
||||
Confidence: "0.80",
|
||||
},
|
||||
"single_use": {
|
||||
URLs: []string{"https://go.dev/doc/effective_go", "https://go.dev/wiki/CodeReviewComments"},
|
||||
Rationale: "Single-use abstractions can reduce clarity unless they encode reusable domain behavior.",
|
||||
Confidence: "0.74",
|
||||
},
|
||||
"test_coverage": {
|
||||
URLs: []string{"https://go.dev/doc/tutorial/add-a-test", "https://pkg.go.dev/testing"},
|
||||
Rationale: "Coverage gaps on changed code increase regression probability.",
|
||||
Confidence: "0.78",
|
||||
},
|
||||
}
|
||||
|
||||
// AttachDocsEvidence annotates findings with docs evidence metadata.
|
||||
func AttachDocsEvidence(language string, findings []Finding) []Finding {
|
||||
language = strings.ToLower(strings.TrimSpace(language))
|
||||
for i := range findings {
|
||||
ev, ok := defaultEvidenceByType[findings[i].Type]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if findings[i].Metadata == nil {
|
||||
findings[i].Metadata = map[string]string{}
|
||||
}
|
||||
if len(ev.URLs) > 0 {
|
||||
findings[i].Metadata["docs_evidence_urls"] = strings.Join(ev.URLs, " | ")
|
||||
}
|
||||
if ev.Rationale != "" {
|
||||
findings[i].Metadata["docs_evidence_rationale"] = ev.Rationale
|
||||
}
|
||||
if ev.Confidence != "" {
|
||||
findings[i].Metadata["docs_evidence_confidence"] = ev.Confidence
|
||||
}
|
||||
if language != "" {
|
||||
findings[i].Metadata["docs_evidence_language"] = language
|
||||
}
|
||||
}
|
||||
return findings
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func (f *DefaultFileFinder) FindFiles(path string, language string) ([]string, e
|
||||
if info.IsDir() {
|
||||
// Skip hidden directories and common exclude dirs
|
||||
base := filepath.Base(filePath)
|
||||
if strings.HasPrefix(base, ".") || base == "node_modules" || base == "vendor" {
|
||||
if filePath != path && (strings.HasPrefix(base, ".") || base == "node_modules" || base == "vendor") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -170,6 +170,37 @@ func TestDefaultFileFinder_FindFiles_EmptyDirectory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultFileFinder_FindFiles_DotPathRootNotSkipped(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "filefinder_dot_root_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte("package main"), 0644); err != nil {
|
||||
t.Fatalf("Failed to write go file: %v", err)
|
||||
}
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get cwd: %v", err)
|
||||
}
|
||||
defer func() { _ = os.Chdir(cwd) }()
|
||||
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to chdir: %v", err)
|
||||
}
|
||||
|
||||
finder := NewDefaultFileFinder()
|
||||
files, err := finder.FindFiles(".", "go")
|
||||
if err != nil {
|
||||
t.Fatalf("FindFiles() failed: %v", err)
|
||||
}
|
||||
if len(files) != 1 {
|
||||
t.Fatalf("FindFiles('.') expected 1 file, got %d", len(files))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultFileFinder_FindFiles_NonExistentPath(t *testing.T) {
|
||||
finder := NewDefaultFileFinder()
|
||||
files, err := finder.FindFiles("/non/existent/path", "go")
|
||||
|
||||
@@ -58,7 +58,10 @@ func (d *SingleUseDetector) Detect(ctx context.Context, path string, config *qua
|
||||
|
||||
switch obj := obj.(type) {
|
||||
case *types.Func:
|
||||
key := obj.Pkg().Path() + "." + obj.Name()
|
||||
key, ok := functionKey(obj)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
callCounts[key]++
|
||||
case *types.TypeName:
|
||||
if obj.Pkg() != nil {
|
||||
@@ -75,17 +78,18 @@ func (d *SingleUseDetector) Detect(ctx context.Context, path string, config *qua
|
||||
|
||||
switch obj := obj.(type) {
|
||||
case *types.Func:
|
||||
if obj.Pkg() != nil {
|
||||
key := obj.Pkg().Path() + "." + obj.Name()
|
||||
pos := pkg.Fset.Position(obj.Pos())
|
||||
funcDefs[key] = FuncDef{
|
||||
Name: obj.Name(),
|
||||
File: pos.Filename,
|
||||
Line: pos.Line,
|
||||
Package: obj.Pkg().Path(),
|
||||
Exported: obj.Exported(),
|
||||
Signature: obj.Type().String(),
|
||||
}
|
||||
key, ok := functionKey(obj)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
pos := pkg.Fset.Position(obj.Pos())
|
||||
funcDefs[key] = FuncDef{
|
||||
Name: obj.Name(),
|
||||
File: pos.Filename,
|
||||
Line: pos.Line,
|
||||
Package: obj.Pkg().Path(),
|
||||
Exported: obj.Exported(),
|
||||
Signature: obj.Type().String(),
|
||||
}
|
||||
case *types.TypeName:
|
||||
if obj.Pkg() != nil {
|
||||
@@ -109,6 +113,9 @@ func (d *SingleUseDetector) Detect(ctx context.Context, path string, config *qua
|
||||
var findings []quality.Finding
|
||||
|
||||
for key, def := range funcDefs {
|
||||
if def.Exported || isLikelyEntrypointFile(def.File) {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(def.Name, "Test") || strings.HasPrefix(def.Name, "Test") {
|
||||
continue
|
||||
}
|
||||
@@ -143,9 +150,18 @@ func (d *SingleUseDetector) Detect(ctx context.Context, path string, config *qua
|
||||
}
|
||||
|
||||
for key, def := range typeDefs {
|
||||
if def.Exported || isLikelyEntrypointFile(def.File) {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(def.Name, "Error") || strings.HasSuffix(def.Name, "Options") {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(def.Name, "Config") || strings.HasSuffix(def.Name, "Params") {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(def.Underlying, "struct") && !strings.Contains(def.Underlying, "interface") {
|
||||
continue
|
||||
}
|
||||
|
||||
count := typeUsages[key]
|
||||
if count == 1 {
|
||||
@@ -242,6 +258,22 @@ func (d *SingleUseDetector) getFuncLOC(file string, startLine int) (int, error)
|
||||
return loc, nil
|
||||
}
|
||||
|
||||
func functionKey(fn *types.Func) (string, bool) {
|
||||
if fn == nil || fn.Pkg() == nil {
|
||||
return "", false
|
||||
}
|
||||
sig, ok := fn.Type().(*types.Signature)
|
||||
if ok && sig.Recv() != nil {
|
||||
return "", false
|
||||
}
|
||||
return fn.Pkg().Path() + "." + fn.Name(), true
|
||||
}
|
||||
|
||||
func isLikelyEntrypointFile(path string) bool {
|
||||
p := filepath.ToSlash(path)
|
||||
return strings.HasPrefix(p, "cmd/") || strings.Contains(p, "/cmd/") || strings.HasSuffix(p, "/main.go") || strings.HasSuffix(p, "_test.go")
|
||||
}
|
||||
|
||||
type FuncDef struct {
|
||||
Name string
|
||||
File string
|
||||
@@ -471,33 +503,36 @@ func (d *EnhancedDeadCodeDetector) Detect(ctx context.Context, path string, conf
|
||||
switch o := obj.(type) {
|
||||
case *types.Func:
|
||||
defs[key] = ObjInfo{
|
||||
Name: obj.Name(),
|
||||
Type: "function",
|
||||
File: pos.Filename,
|
||||
Line: pos.Line,
|
||||
Package: obj.Pkg().Path(),
|
||||
Exported: obj.Exported(),
|
||||
Signature: o.Type().String(),
|
||||
Name: obj.Name(),
|
||||
Type: "function",
|
||||
File: pos.Filename,
|
||||
Line: pos.Line,
|
||||
Package: obj.Pkg().Path(),
|
||||
PackageName: pkg.Name,
|
||||
Exported: obj.Exported(),
|
||||
Signature: o.Type().String(),
|
||||
}
|
||||
case *types.TypeName:
|
||||
defs[key] = ObjInfo{
|
||||
Name: obj.Name(),
|
||||
Type: "type",
|
||||
File: pos.Filename,
|
||||
Line: pos.Line,
|
||||
Package: obj.Pkg().Path(),
|
||||
Exported: obj.Exported(),
|
||||
Underlying: o.Type().Underlying().String(),
|
||||
Name: obj.Name(),
|
||||
Type: "type",
|
||||
File: pos.Filename,
|
||||
Line: pos.Line,
|
||||
Package: obj.Pkg().Path(),
|
||||
PackageName: pkg.Name,
|
||||
Exported: obj.Exported(),
|
||||
Underlying: o.Type().Underlying().String(),
|
||||
}
|
||||
case *types.Var:
|
||||
if obj.Exported() {
|
||||
if obj.Exported() && !o.IsField() {
|
||||
defs[key] = ObjInfo{
|
||||
Name: obj.Name(),
|
||||
Type: "variable",
|
||||
File: pos.Filename,
|
||||
Line: pos.Line,
|
||||
Package: obj.Pkg().Path(),
|
||||
Exported: obj.Exported(),
|
||||
Name: obj.Name(),
|
||||
Type: "variable",
|
||||
File: pos.Filename,
|
||||
Line: pos.Line,
|
||||
Package: obj.Pkg().Path(),
|
||||
PackageName: pkg.Name,
|
||||
Exported: obj.Exported(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -521,10 +556,22 @@ func (d *EnhancedDeadCodeDetector) Detect(ctx context.Context, path string, conf
|
||||
if entryPoints[key] {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(def.Package, "/internal/") || def.PackageName == "main" {
|
||||
continue
|
||||
}
|
||||
if isLikelyEntrypointFile(def.File) {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(def.Name, "Test") || strings.HasPrefix(def.Name, "Benchmark") || strings.HasPrefix(def.Name, "Fuzz") {
|
||||
continue
|
||||
}
|
||||
if def.Type == "function" && strings.HasPrefix(def.Name, "New") {
|
||||
continue
|
||||
}
|
||||
if def.Type == "type" && (strings.HasSuffix(def.Name, "Config") || strings.HasSuffix(def.Name, "Options")) {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasSuffix(def.Name, "Error") && def.Type == "type" {
|
||||
continue
|
||||
@@ -573,12 +620,13 @@ func (d *EnhancedDeadCodeDetector) Detect(ctx context.Context, path string, conf
|
||||
}
|
||||
|
||||
type ObjInfo struct {
|
||||
Name string
|
||||
Type string
|
||||
File string
|
||||
Line int
|
||||
Package string
|
||||
Exported bool
|
||||
Signature string
|
||||
Underlying string
|
||||
Name string
|
||||
Type string
|
||||
File string
|
||||
Line int
|
||||
Package string
|
||||
PackageName string
|
||||
Exported bool
|
||||
Signature string
|
||||
Underlying string
|
||||
}
|
||||
|
||||
@@ -172,8 +172,7 @@ func (d *UnusedImportDetector) analyzeFile(path string) ([]quality.Finding, erro
|
||||
if imp.Name != nil {
|
||||
name = imp.Name.Name
|
||||
} else {
|
||||
parts := strings.Split(pkgPath, "/")
|
||||
name = parts[len(parts)-1]
|
||||
name = inferImportName(pkgPath)
|
||||
}
|
||||
imports[pkgPath] = name
|
||||
}
|
||||
@@ -191,8 +190,7 @@ func (d *UnusedImportDetector) analyzeFile(path string) ([]quality.Finding, erro
|
||||
if imp.Name != nil {
|
||||
name = imp.Name.Name
|
||||
} else {
|
||||
parts := strings.Split(pkgPath, "/")
|
||||
name = parts[len(parts)-1]
|
||||
name = inferImportName(pkgPath)
|
||||
}
|
||||
|
||||
if name == "_" || name == "." {
|
||||
@@ -224,6 +222,42 @@ func (d *UnusedImportDetector) analyzeFile(path string) ([]quality.Finding, erro
|
||||
return findings, nil
|
||||
}
|
||||
|
||||
func inferImportName(pkgPath string) string {
|
||||
parts := strings.Split(pkgPath, "/")
|
||||
if len(parts) == 0 {
|
||||
return pkgPath
|
||||
}
|
||||
|
||||
last := parts[len(parts)-1]
|
||||
if isSemverSegment(last) && len(parts) >= 2 {
|
||||
last = parts[len(parts)-2]
|
||||
}
|
||||
if idx := strings.Index(last, ".v"); idx > 0 && isDigits(last[idx+2:]) {
|
||||
last = last[:idx]
|
||||
}
|
||||
|
||||
return last
|
||||
}
|
||||
|
||||
func isSemverSegment(segment string) bool {
|
||||
if len(segment) < 2 || segment[0] != 'v' {
|
||||
return false
|
||||
}
|
||||
return isDigits(segment[1:])
|
||||
}
|
||||
|
||||
func isDigits(value string) bool {
|
||||
if value == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range value {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type CycleDetector struct {
|
||||
*quality.BaseDetector
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package analyzers
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestInferImportName(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{path: "fmt", want: "fmt"},
|
||||
{path: "gopkg.in/yaml.v3", want: "yaml"},
|
||||
{path: "github.com/gocolly/colly/v2", want: "colly"},
|
||||
{path: "golang.org/x/tools/go/packages", want: "packages"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := inferImportName(tt.path)
|
||||
if got != tt.want {
|
||||
t.Fatalf("inferImportName(%q) = %q, want %q", tt.path, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,6 +240,10 @@ func (d *DebugLogDetector) analyzeFile(path string) []quality.Finding {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
normPath := filepath.ToSlash(path)
|
||||
if strings.Contains(normPath, "internal/ui/") || strings.Contains(normPath, "examples/") {
|
||||
return nil
|
||||
}
|
||||
|
||||
debugPatterns := []string{
|
||||
"log.Print",
|
||||
@@ -267,7 +271,7 @@ func (d *DebugLogDetector) analyzeFile(path string) []quality.Finding {
|
||||
|
||||
for _, pattern := range debugPatterns {
|
||||
if callStr == pattern || strings.HasPrefix(callStr, pattern) {
|
||||
if strings.Contains(path, "_test.go") {
|
||||
if strings.HasSuffix(normPath, "_test.go") || strings.HasPrefix(normPath, "cmd/") || strings.Contains(normPath, "/cmd/") {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -291,7 +295,7 @@ func (d *DebugLogDetector) analyzeFile(path string) []quality.Finding {
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(path, "/cmd/") {
|
||||
if strings.HasPrefix(normPath, "cmd/") || strings.Contains(normPath, "/cmd/") {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ func (p *GoPlugin) DefaultSrcDir() string {
|
||||
|
||||
func (p *GoPlugin) CreateDetectors(finder quality.FileFinder) []quality.Detector {
|
||||
return []quality.Detector{
|
||||
analyzers.NewDeadCodeDetector(finder),
|
||||
analyzers.NewEnhancedDeadCodeDetector(finder),
|
||||
analyzers.NewUnusedImportDetector(finder),
|
||||
analyzers.NewCycleDetector(finder),
|
||||
|
||||
+16
-23
@@ -67,13 +67,13 @@ func (s *Scanner) Scan(ctx context.Context) (*ScanResult, error) {
|
||||
// Skip language-specific detectors for different languages
|
||||
if langDetector, ok := detector.(LanguageDetector); ok {
|
||||
supported := langDetector.SupportedLanguages()
|
||||
if !contains(supported, language) {
|
||||
if len(supported) > 0 && !contains(supported, language) {
|
||||
log.Printf("Skipping detector %s for language %s", name, language)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
findings, err := detector.Detect(ctx, s.config.Path, s.config)
|
||||
findings, err := s.runDetectorSafely(ctx, detector, name)
|
||||
if err != nil {
|
||||
log.Printf("Detector %s failed: %v", name, err)
|
||||
continue
|
||||
@@ -106,28 +106,21 @@ func (s *Scanner) Scan(ctx context.Context) (*ScanResult, error) {
|
||||
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 {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
// 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
|
||||
@@ -164,7 +157,7 @@ func (s *Scanner) getSourceFiles(path, language string) ([]string, error) {
|
||||
if info.IsDir() {
|
||||
// Skip hidden directories and common exclude dirs
|
||||
base := filepath.Base(filePath)
|
||||
if strings.HasPrefix(base, ".") || base == "node_modules" || base == "vendor" {
|
||||
if filePath != path && (strings.HasPrefix(base, ".") || base == "node_modules" || base == "vendor") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package quality
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type panicDetector struct{}
|
||||
|
||||
func (p panicDetector) Name() string { return "panic_detector" }
|
||||
func (p panicDetector) Severity() Severity { return SeverityT2 }
|
||||
func (p panicDetector) Detect(ctx context.Context, path string, config *Config) ([]Finding, error) {
|
||||
panic("boom")
|
||||
}
|
||||
|
||||
type okDetector struct{}
|
||||
|
||||
func (o okDetector) Name() string { return "ok_detector" }
|
||||
func (o okDetector) Severity() Severity { return SeverityT1 }
|
||||
func (o okDetector) Detect(ctx context.Context, path string, config *Config) ([]Finding, error) {
|
||||
return []Finding{{ID: "ok", Type: "ok", Title: "ok", File: "f.go", Line: 1, Severity: SeverityT1, Score: 1, Status: StatusOpen}}, nil
|
||||
}
|
||||
|
||||
func TestScannerRecoversDetectorPanic(t *testing.T) {
|
||||
s := NewScanner(&Config{Path: ".", Language: "go"})
|
||||
s.RegisterDetector(panicDetector{})
|
||||
s.RegisterDetector(okDetector{})
|
||||
|
||||
result, err := s.Scan(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("scan should recover detector panic, got err: %v", err)
|
||||
}
|
||||
if len(result.Findings) != 1 {
|
||||
t.Fatalf("expected findings from healthy detector only, got %d", len(result.Findings))
|
||||
}
|
||||
}
|
||||
@@ -457,6 +457,37 @@ func TestScanner_getSourceFiles_Fallback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanner_getSourceFiles_Fallback_DotPathRootNotSkipped(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "scanner_dot_root_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte("package main"), 0644); err != nil {
|
||||
t.Fatalf("Failed to write go file: %v", err)
|
||||
}
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get cwd: %v", err)
|
||||
}
|
||||
defer func() { _ = os.Chdir(cwd) }()
|
||||
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to chdir: %v", err)
|
||||
}
|
||||
|
||||
scanner := NewScanner(&Config{})
|
||||
files, err := scanner.getSourceFiles(".", "go")
|
||||
if err != nil {
|
||||
t.Fatalf("getSourceFiles() failed: %v", err)
|
||||
}
|
||||
if len(files) != 1 {
|
||||
t.Fatalf("getSourceFiles('.') expected 1 file, got %d", len(files))
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanner_filterFindings(t *testing.T) {
|
||||
scanner := NewScanner(&Config{})
|
||||
|
||||
|
||||
@@ -52,8 +52,8 @@ func TestScorer_CalculateScore(t *testing.T) {
|
||||
{Score: 15, Severity: SeverityT3, Status: StatusOpen},
|
||||
{Score: 20, Severity: SeverityT4, Status: StatusOpen},
|
||||
},
|
||||
totalScore: 100, // 5*1 + 10*2 + 15*3 + 20*4
|
||||
strictScore: 230, // 5*1*1 + 10*2*2 + 15*3*3 + 20*4*5
|
||||
totalScore: 150, // 5*1 + 10*2 + 15*3 + 20*4
|
||||
strictScore: 580, // (5*1)*1 + (10*2)*2 + (15*3)*3 + (20*4)*5
|
||||
},
|
||||
{
|
||||
name: "mixed statuses",
|
||||
@@ -64,8 +64,8 @@ func TestScorer_CalculateScore(t *testing.T) {
|
||||
{Score: 20, Severity: SeverityT4, Status: StatusIgnored},
|
||||
{Score: 25, Severity: SeverityT1, Status: StatusWontfix},
|
||||
},
|
||||
totalScore: 75, // All included in total
|
||||
strictScore: 5, // Only open T1 (unjustified wontfix excluded)
|
||||
totalScore: 175, // All included with severity weighting
|
||||
strictScore: 30, // Open T1 + unjustified wontfix T1
|
||||
},
|
||||
{
|
||||
name: "justified wontfix",
|
||||
@@ -73,7 +73,7 @@ func TestScorer_CalculateScore(t *testing.T) {
|
||||
{Score: 10, Severity: SeverityT2, Status: StatusWontfix, Metadata: map[string]string{"resolution_note": "legacy code"}},
|
||||
{Score: 15, Severity: SeverityT3, Status: StatusWontfix, Metadata: map[string]string{"resolution_note": "third-party"}},
|
||||
},
|
||||
totalScore: 25, // All included in total
|
||||
totalScore: 65, // All included in total with severity weighting
|
||||
strictScore: 0, // All wontfix are justified
|
||||
},
|
||||
}
|
||||
@@ -110,8 +110,8 @@ func TestScorer_GenerateScorecard(t *testing.T) {
|
||||
t.Errorf("GenerateScorecard() TargetScore = %v, want 95", card.TargetScore)
|
||||
}
|
||||
|
||||
if card.TotalScore != 40 { // 10*2 + 5*1 + 15*3
|
||||
t.Errorf("GenerateScorecard() TotalScore = %v, want 40", card.TotalScore)
|
||||
if card.TotalScore != 70 { // 10*2 + 5*1 + 15*3
|
||||
t.Errorf("GenerateScorecard() TotalScore = %v, want 70", card.TotalScore)
|
||||
}
|
||||
|
||||
if card.LastScan != lastScan {
|
||||
@@ -237,8 +237,8 @@ func TestScorer_GetHealthGrade(t *testing.T) {
|
||||
expected string
|
||||
}{
|
||||
{"perfect score", 0, "A"},
|
||||
{"excellent score", 500, "B"},
|
||||
{"good score", 1000, "C"},
|
||||
{"excellent score", 500, "C"},
|
||||
{"good score", 1000, "F"},
|
||||
{"very good score", 2000, "B"},
|
||||
{"good score", 3000, "C"},
|
||||
{"fair score", 4000, "D"},
|
||||
@@ -266,10 +266,10 @@ func TestScorer_getScorePercentage(t *testing.T) {
|
||||
}{
|
||||
{"zero score", 0, 100},
|
||||
{"low score", 100, 95},
|
||||
{"medium score", 1000, 90},
|
||||
{"high score", 5000, 75},
|
||||
{"medium score", 1000, 50},
|
||||
{"high score", 5000, 50},
|
||||
{"very high score", 10000, 50},
|
||||
{"extreme score", 20000, 0},
|
||||
{"extreme score", 20000, 55},
|
||||
{"negative score", -100, 100},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user