diff --git a/internal/cli/helpers.go b/internal/cli/helpers.go index 38997f1..39b2196 100644 --- a/internal/cli/helpers.go +++ b/internal/cli/helpers.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "os" "strings" service "github.com/devr-tools/codeguard/pkg/codeguard" @@ -54,7 +53,7 @@ func loadScanConfigWithFallback(path string, profile string, targetPath string, if err == nil { return applyProfileOverride(cfg, profile) } - if !defaultConfigRequested || strings.TrimSpace(targetPath) == "" || !errors.Is(err, os.ErrNotExist) { + if !defaultConfigRequested || strings.TrimSpace(targetPath) == "" || !errors.Is(err, service.ErrConfigNotFound) { return service.Config{}, err } diff --git a/internal/codeguard/config/io.go b/internal/codeguard/config/io.go index 7afe0ee..ec3a96a 100644 --- a/internal/codeguard/config/io.go +++ b/internal/codeguard/config/io.go @@ -17,6 +17,9 @@ import ( const maxConfigFileBytes = 32 << 20 var ( + // ErrConfigNotFound identifies a missing top-level configuration file. It is + // distinct from missing artifacts referenced by an existing configuration. + ErrConfigNotFound = errors.New("config file not found") defaultConfigNames = []string{"codeguard.yaml", "codeguard.yml", "codeguard.json"} directoryConfigNames = []string{"codeguard.yaml", "codeguard.yml", "codeguard.json", "config.yaml", "config.yml", "config.json"} defaultConfigDirs = []string{".", ".codeguard"} @@ -30,6 +33,9 @@ func LoadFile(path string) (core.Config, error) { f, err := os.Open(resolvedPath) //nolint:gosec // operator-supplied config path; read is size-capped by LimitReader below if err != nil { + if errors.Is(err, os.ErrNotExist) { + return core.Config{}, fmt.Errorf("%w: %w", ErrConfigNotFound, err) + } return core.Config{}, err } defer func() { _ = f.Close() }() diff --git a/pkg/codeguard/sdk_config.go b/pkg/codeguard/sdk_config.go index 8bc0c33..e925f05 100644 --- a/pkg/codeguard/sdk_config.go +++ b/pkg/codeguard/sdk_config.go @@ -2,6 +2,9 @@ package codeguard import "github.com/devr-tools/codeguard/internal/codeguard/config" +// ErrConfigNotFound identifies a missing top-level configuration file. +var ErrConfigNotFound = config.ErrConfigNotFound + // ExampleConfig returns CodeGuard's complete, ready-to-edit starter // configuration. It is intended for callers creating a new configuration, // rather than as a way to normalize a partial Config. diff --git a/tests/cli/scan_test.go b/tests/cli/scan_test.go index 3d6255e..9362bbe 100644 --- a/tests/cli/scan_test.go +++ b/tests/cli/scan_test.go @@ -235,6 +235,34 @@ func TestRunScanFolderWithExplicitMissingConfigStillFails(t *testing.T) { } } +func TestRunScanFolderWithMissingDesignRulesDoesNotFallBack(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("getwd: %v", err) + } + dir := t.TempDir() + if err := os.Chdir(dir); err != nil { + t.Fatalf("chdir tempdir: %v", err) + } + t.Cleanup(func() { _ = os.Chdir(cwd) }) + + writeScanTestFile(t, filepath.Join(dir, "codeguard.yml"), ` +name: repository-policy +checks: + design_rules_file: .codeguard/missing-design-rules.yml +`) + writeScanTestFile(t, filepath.Join(dir, "sub", "main.go"), "package main\n") + + var stdout, stderr bytes.Buffer + code := cli.Run([]string{"scan", "-folder", "sub"}, strings.NewReader(""), &stdout, &stderr) + if code != 1 { + t.Fatalf("expected exit 1, got %d; stdout = %s", code, stdout.String()) + } + if !strings.Contains(stderr.String(), "checks.design_rules_file") { + t.Fatalf("expected missing design rules error, got %s", stderr.String()) + } +} + func writeScanTestFile(t *testing.T, path string, content string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {