diff --git a/cmd/core-lint/main.go b/cmd/core-lint/main.go index d4f6868..0e9eeef 100644 --- a/cmd/core-lint/main.go +++ b/cmd/core-lint/main.go @@ -7,6 +7,7 @@ import ( "strings" "forge.lthn.ai/core/cli/pkg/cli" + coreerr "forge.lthn.ai/core/go-log" lint "forge.lthn.ai/core/lint" lintpkg "forge.lthn.ai/core/lint/pkg/lint" ) @@ -29,7 +30,7 @@ func addLintCommands(root *cli.Command) { checkCmd := cli.NewCommand("check", "Scan files for pattern matches", "", func(cmd *cli.Command, args []string) error { cat, err := lint.LoadEmbeddedCatalog() if err != nil { - return fmt.Errorf("loading catalog: %w", err) + return coreerr.E("cmd.check", "loading catalog", err) } rules := cat.Rules @@ -55,7 +56,7 @@ func addLintCommands(root *cli.Command) { scanner, err := lintpkg.NewScanner(rules) if err != nil { - return fmt.Errorf("creating scanner: %w", err) + return coreerr.E("cmd.check", "creating scanner", err) } paths := args @@ -67,7 +68,7 @@ func addLintCommands(root *cli.Command) { for _, p := range paths { info, err := os.Stat(p) if err != nil { - return fmt.Errorf("stat %s: %w", p, err) + return coreerr.E("cmd.check", "stat "+p, err) } var findings []lintpkg.Finding @@ -120,7 +121,7 @@ func addLintCommands(root *cli.Command) { listCmd := cli.NewCommand("list", "List all rules in the catalog", "", func(cmd *cli.Command, args []string) error { cat, err := lint.LoadEmbeddedCatalog() if err != nil { - return fmt.Errorf("loading catalog: %w", err) + return coreerr.E("cmd.catalog.list", "loading catalog", err) } rules := cat.Rules @@ -145,17 +146,17 @@ func addLintCommands(root *cli.Command) { // catalog show showCmd := cli.NewCommand("show", "Show details of a specific rule", "", func(cmd *cli.Command, args []string) error { if len(args) == 0 { - return fmt.Errorf("rule ID required") + return coreerr.E("cmd.catalog.show", "rule ID required", nil) } cat, err := lint.LoadEmbeddedCatalog() if err != nil { - return fmt.Errorf("loading catalog: %w", err) + return coreerr.E("cmd.catalog.show", "loading catalog", err) } r := cat.ByID(args[0]) if r == nil { - return fmt.Errorf("rule %q not found", args[0]) + return coreerr.E("cmd.catalog.show", "rule "+args[0]+" not found", nil) } data, err := json.MarshalIndent(r, "", " ") diff --git a/cmd/qa/cmd_review.go b/cmd/qa/cmd_review.go index 2fff525..b1088f5 100644 --- a/cmd/qa/cmd_review.go +++ b/cmd/qa/cmd_review.go @@ -199,7 +199,7 @@ func fetchPRs(ctx context.Context, repo, search string) ([]PullRequest, error) { output, err := cmd.Output() if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { - return nil, fmt.Errorf("%s", strings.TrimSpace(string(exitErr.Stderr))) + return nil, log.E("qa.fetchPRs", strings.TrimSpace(string(exitErr.Stderr)), nil) } return nil, err } diff --git a/cmd/qa/cmd_watch.go b/cmd/qa/cmd_watch.go index c51ab2a..7a5577f 100644 --- a/cmd/qa/cmd_watch.go +++ b/cmd/qa/cmd_watch.go @@ -261,7 +261,7 @@ func parseGitHubRepo(url string) (string, error) { } } - return "", fmt.Errorf("could not parse GitHub repo from URL: %s", url) + return "", log.E("qa.parseGitHubRepo", "could not parse GitHub repo from URL: "+url, nil) } // fetchWorkflowRunsForCommit fetches workflow runs for a specific commit diff --git a/pkg/lint/matcher.go b/pkg/lint/matcher.go index 13351ab..502263f 100644 --- a/pkg/lint/matcher.go +++ b/pkg/lint/matcher.go @@ -2,9 +2,10 @@ package lint import ( "bytes" - "fmt" "regexp" "strings" + + coreerr "forge.lthn.ai/core/go-log" ) // Finding represents a single match of a rule against a source file. @@ -43,14 +44,14 @@ func NewMatcher(rules []Rule) (*Matcher, error) { pat, err := regexp.Compile(r.Pattern) if err != nil { - return nil, fmt.Errorf("compiling pattern for rule %s: %w", r.ID, err) + return nil, coreerr.E("NewMatcher", "compiling pattern for rule "+r.ID, err) } var excl *regexp.Regexp if r.ExcludePattern != "" { excl, err = regexp.Compile(r.ExcludePattern) if err != nil { - return nil, fmt.Errorf("compiling exclude pattern for rule %s: %w", r.ID, err) + return nil, coreerr.E("NewMatcher", "compiling exclude pattern for rule "+r.ID, err) } } diff --git a/pkg/lint/rule.go b/pkg/lint/rule.go index a683ecb..bcb28d6 100644 --- a/pkg/lint/rule.go +++ b/pkg/lint/rule.go @@ -5,6 +5,7 @@ import ( "regexp" "slices" + coreerr "forge.lthn.ai/core/go-log" "gopkg.in/yaml.v3" ) @@ -32,35 +33,35 @@ type Rule struct { // Validate checks that the rule has all required fields and that regex patterns compile. func (r *Rule) Validate() error { if r.ID == "" { - return fmt.Errorf("rule validation: id must not be empty") + return coreerr.E("Rule.Validate", "id must not be empty", nil) } if r.Title == "" { - return fmt.Errorf("rule %s: title must not be empty", r.ID) + return coreerr.E("Rule.Validate", "rule "+r.ID+": title must not be empty", nil) } if r.Severity == "" { - return fmt.Errorf("rule %s: severity must not be empty", r.ID) + return coreerr.E("Rule.Validate", "rule "+r.ID+": severity must not be empty", nil) } if !slices.Contains(validSeverities, r.Severity) { - return fmt.Errorf("rule %s: severity %q is not valid (want one of %v)", r.ID, r.Severity, validSeverities) + return coreerr.E("Rule.Validate", fmt.Sprintf("rule %s: severity %q is not valid (want one of %v)", r.ID, r.Severity, validSeverities), nil) } if len(r.Languages) == 0 { - return fmt.Errorf("rule %s: languages must not be empty", r.ID) + return coreerr.E("Rule.Validate", "rule "+r.ID+": languages must not be empty", nil) } if r.Pattern == "" { - return fmt.Errorf("rule %s: pattern must not be empty", r.ID) + return coreerr.E("Rule.Validate", "rule "+r.ID+": pattern must not be empty", nil) } if r.Detection == "" { - return fmt.Errorf("rule %s: detection must not be empty", r.ID) + return coreerr.E("Rule.Validate", "rule "+r.ID+": detection must not be empty", nil) } // Only validate regex compilation when detection type is regex. if r.Detection == "regex" { if _, err := regexp.Compile(r.Pattern); err != nil { - return fmt.Errorf("rule %s: pattern does not compile: %w", r.ID, err) + return coreerr.E("Rule.Validate", "rule "+r.ID+": pattern does not compile", err) } if r.ExcludePattern != "" { if _, err := regexp.Compile(r.ExcludePattern); err != nil { - return fmt.Errorf("rule %s: exclude_pattern does not compile: %w", r.ID, err) + return coreerr.E("Rule.Validate", "rule "+r.ID+": exclude_pattern does not compile", err) } } } @@ -72,7 +73,7 @@ func (r *Rule) Validate() error { func ParseRules(data []byte) ([]Rule, error) { var rules []Rule if err := yaml.Unmarshal(data, &rules); err != nil { - return nil, fmt.Errorf("parsing rules: %w", err) + return nil, coreerr.E("ParseRules", "parsing rules", err) } return rules, nil } diff --git a/pkg/lint/tools.go b/pkg/lint/tools.go index febb6a1..66a6eb9 100644 --- a/pkg/lint/tools.go +++ b/pkg/lint/tools.go @@ -11,6 +11,8 @@ import ( "strconv" "strings" "time" + + coreerr "forge.lthn.ai/core/go-log" ) // ToolFinding represents a single issue found by an external tool (e.g. go vet). @@ -145,7 +147,7 @@ func (t *Toolkit) FindTODOs(dir string) ([]TODO, error) { return nil, nil } if err != nil && exitCode != 1 { - return nil, fmt.Errorf("git grep failed (exit %d): %w\n%s", exitCode, err, stderr) + return nil, coreerr.E("Toolkit.FindTODOs", fmt.Sprintf("git grep failed (exit %d):\n%s", exitCode, stderr), err) } var todos []TODO @@ -181,7 +183,7 @@ func (t *Toolkit) FindTODOs(dir string) ([]TODO, error) { func (t *Toolkit) AuditDeps() ([]Vulnerability, error) { stdout, stderr, exitCode, err := t.Run("govulncheck", "./...") if err != nil && exitCode != 0 && !strings.Contains(stdout, "Vulnerability") { - return nil, fmt.Errorf("govulncheck failed (exit %d): %w\n%s", exitCode, err, stderr) + return nil, coreerr.E("Toolkit.AuditDeps", fmt.Sprintf("govulncheck failed (exit %d):\n%s", exitCode, stderr), err) } var vulns []Vulnerability @@ -230,7 +232,7 @@ func (t *Toolkit) AuditDeps() ([]Vulnerability, error) { func (t *Toolkit) DiffStat() (DiffSummary, error) { stdout, stderr, exitCode, err := t.Run("git", "diff", "--stat") if err != nil && exitCode != 0 { - return DiffSummary{}, fmt.Errorf("git diff failed (exit %d): %w\n%s", exitCode, err, stderr) + return DiffSummary{}, coreerr.E("Toolkit.DiffStat", fmt.Sprintf("git diff failed (exit %d):\n%s", exitCode, stderr), err) } var s DiffSummary @@ -263,7 +265,7 @@ func (t *Toolkit) DiffStat() (DiffSummary, error) { func (t *Toolkit) UncommittedFiles() ([]string, error) { stdout, stderr, exitCode, err := t.Run("git", "status", "--porcelain") if err != nil && exitCode != 0 { - return nil, fmt.Errorf("git status failed: %w\n%s", err, stderr) + return nil, coreerr.E("Toolkit.UncommittedFiles", "git status failed:\n"+stderr, err) } var files []string for line := range strings.SplitSeq(strings.TrimSpace(stdout), "\n") { @@ -281,7 +283,7 @@ func (t *Toolkit) Lint(pkg string) ([]ToolFinding, error) { return nil, nil } if err != nil && exitCode != 2 { - return nil, fmt.Errorf("go vet failed: %w", err) + return nil, coreerr.E("Toolkit.Lint", "go vet failed", err) } var findings []ToolFinding @@ -311,7 +313,7 @@ func (t *Toolkit) ScanSecrets(dir string) ([]SecretLeak, error) { return nil, nil } if err != nil && exitCode != 1 { - return nil, fmt.Errorf("gitleaks failed: %w", err) + return nil, coreerr.E("Toolkit.ScanSecrets", "gitleaks failed", err) } var leaks []SecretLeak @@ -338,7 +340,7 @@ func (t *Toolkit) ScanSecrets(dir string) ([]SecretLeak, error) { func (t *Toolkit) ModTidy() error { _, stderr, exitCode, err := t.Run("go", "mod", "tidy") if err != nil && exitCode != 0 { - return fmt.Errorf("go mod tidy failed: %s", stderr) + return coreerr.E("Toolkit.ModTidy", "go mod tidy failed: "+strings.TrimSpace(stderr), nil) } return nil } @@ -350,7 +352,7 @@ func (t *Toolkit) Build(targets ...string) ([]BuildResult, error) { _, stderr, _, err := t.Run("go", "build", "-o", "/dev/null", target) r := BuildResult{Target: target} if err != nil { - r.Error = fmt.Errorf("%s", strings.TrimSpace(stderr)) + r.Error = coreerr.E("Toolkit.Build", strings.TrimSpace(stderr), nil) } results = append(results, r) } @@ -361,7 +363,7 @@ func (t *Toolkit) Build(targets ...string) ([]BuildResult, error) { func (t *Toolkit) TestCount(pkg string) (int, error) { stdout, stderr, exitCode, err := t.Run("go", "test", "-list", ".*", pkg) if err != nil && exitCode != 0 { - return 0, fmt.Errorf("go test -list failed: %w\n%s", err, stderr) + return 0, coreerr.E("Toolkit.TestCount", fmt.Sprintf("go test -list failed:\n%s", stderr), err) } count := 0 for line := range strings.SplitSeq(strings.TrimSpace(stdout), "\n") { @@ -379,7 +381,7 @@ func (t *Toolkit) Coverage(pkg string) ([]CoverageReport, error) { } stdout, stderr, exitCode, err := t.Run("go", "test", "-cover", pkg) if err != nil && exitCode != 0 && !strings.Contains(stdout, "coverage:") { - return nil, fmt.Errorf("go test -cover failed (exit %d): %w\n%s", exitCode, err, stderr) + return nil, coreerr.E("Toolkit.Coverage", fmt.Sprintf("go test -cover failed (exit %d):\n%s", exitCode, stderr), err) } var reports []CoverageReport @@ -406,7 +408,7 @@ func (t *Toolkit) RaceDetect(pkg string) ([]RaceCondition, error) { } _, stderr, _, err := t.Run("go", "test", "-race", pkg) if err != nil && !strings.Contains(stderr, "WARNING: DATA RACE") { - return nil, fmt.Errorf("go test -race failed: %w", err) + return nil, coreerr.E("Toolkit.RaceDetect", "go test -race failed", err) } var races []RaceCondition @@ -434,7 +436,7 @@ func (t *Toolkit) RaceDetect(pkg string) ([]RaceCondition, error) { func (t *Toolkit) GocycloComplexity(threshold int) ([]ComplexFunc, error) { stdout, stderr, exitCode, err := t.Run("gocyclo", "-over", strconv.Itoa(threshold), ".") if err != nil && exitCode == -1 { - return nil, fmt.Errorf("gocyclo not available: %w\n%s", err, stderr) + return nil, coreerr.E("Toolkit.GocycloComplexity", "gocyclo not available:\n"+stderr, err) } var funcs []ComplexFunc @@ -467,7 +469,7 @@ func (t *Toolkit) GocycloComplexity(threshold int) ([]ComplexFunc, error) { func (t *Toolkit) DepGraph(pkg string) (*Graph, error) { stdout, stderr, exitCode, err := t.Run("go", "mod", "graph") if err != nil && exitCode != 0 { - return nil, fmt.Errorf("go mod graph failed (exit %d): %w\n%s", exitCode, err, stderr) + return nil, coreerr.E("Toolkit.DepGraph", fmt.Sprintf("go mod graph failed (exit %d):\n%s", exitCode, stderr), err) } graph := &Graph{Edges: make(map[string][]string)} @@ -494,7 +496,7 @@ func (t *Toolkit) DepGraph(pkg string) (*Graph, error) { func (t *Toolkit) GitLog(n int) ([]Commit, error) { stdout, stderr, exitCode, err := t.Run("git", "log", fmt.Sprintf("-n%d", n), "--format=%H|%an|%aI|%s") if err != nil && exitCode != 0 { - return nil, fmt.Errorf("git log failed (exit %d): %w\n%s", exitCode, err, stderr) + return nil, coreerr.E("Toolkit.GitLog", fmt.Sprintf("git log failed (exit %d):\n%s", exitCode, stderr), err) } var commits []Commit @@ -543,7 +545,7 @@ func (t *Toolkit) CheckPerms(dir string) ([]PermIssue, error) { return nil }) if err != nil { - return nil, fmt.Errorf("walk failed: %w", err) + return nil, coreerr.E("Toolkit.CheckPerms", "walk failed", err) } return issues, nil } diff --git a/pkg/lint/vulncheck.go b/pkg/lint/vulncheck.go index 7978078..907f3d9 100644 --- a/pkg/lint/vulncheck.go +++ b/pkg/lint/vulncheck.go @@ -2,8 +2,9 @@ package lint import ( "encoding/json" - "fmt" "strings" + + coreerr "forge.lthn.ai/core/go-log" ) // VulnFinding represents a single vulnerability found by govulncheck. @@ -89,7 +90,7 @@ func (t *Toolkit) VulnCheck(modulePath string) (*VulnResult, error) { stdout, stderr, exitCode, err := t.Run("govulncheck", "-json", modulePath) if err != nil && exitCode == -1 { - return nil, fmt.Errorf("govulncheck not installed or not available: %w", err) + return nil, coreerr.E("Toolkit.VulnCheck", "govulncheck not installed or not available", err) } return ParseVulnCheckJSON(stdout, stderr) diff --git a/pkg/php/analyse.go b/pkg/php/analyse.go index 0f80332..d710b13 100644 --- a/pkg/php/analyse.go +++ b/pkg/php/analyse.go @@ -7,6 +7,8 @@ import ( "os" "os/exec" "path/filepath" + + coreerr "forge.lthn.ai/core/go-log" ) // AnalyseOptions configures PHP static analysis. @@ -78,7 +80,7 @@ func Analyse(ctx context.Context, opts AnalyseOptions) error { if opts.Dir == "" { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("get working directory: %w", err) + return coreerr.E("php.Analyse", "get working directory", err) } opts.Dir = cwd } @@ -90,7 +92,7 @@ func Analyse(ctx context.Context, opts AnalyseOptions) error { // Check if analyser is available analyser, found := DetectAnalyser(opts.Dir) if !found { - return fmt.Errorf("no static analyser found (install PHPStan: composer require phpstan/phpstan --dev)") + return coreerr.E("php.Analyse", "no static analyser found (install PHPStan: composer require phpstan/phpstan --dev)", nil) } var cmdName string @@ -192,7 +194,7 @@ func RunPsalm(ctx context.Context, opts PsalmOptions) error { if opts.Dir == "" { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("get working directory: %w", err) + return coreerr.E("php.RunPsalm", "get working directory", err) } opts.Dir = cwd } diff --git a/pkg/php/audit.go b/pkg/php/audit.go index 8136747..235c21a 100644 --- a/pkg/php/audit.go +++ b/pkg/php/audit.go @@ -3,11 +3,12 @@ package php import ( "context" "encoding/json" - "fmt" "io" "os" "os/exec" "path/filepath" + + coreerr "forge.lthn.ai/core/go-log" ) // AuditOptions configures dependency security auditing. @@ -40,7 +41,7 @@ func RunAudit(ctx context.Context, opts AuditOptions) ([]AuditResult, error) { if opts.Dir == "" { cwd, err := os.Getwd() if err != nil { - return nil, fmt.Errorf("get working directory: %w", err) + return nil, coreerr.E("php.RunAudit", "get working directory", err) } opts.Dir = cwd } diff --git a/pkg/php/format.go b/pkg/php/format.go index d11a47f..b7104f8 100644 --- a/pkg/php/format.go +++ b/pkg/php/format.go @@ -3,11 +3,12 @@ package php import ( "context" - "fmt" "io" "os" "os/exec" "path/filepath" + + coreerr "forge.lthn.ai/core/go-log" ) // fileExists reports whether the named file or directory exists. @@ -68,7 +69,7 @@ func Format(ctx context.Context, opts FormatOptions) error { if opts.Dir == "" { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("get working directory: %w", err) + return coreerr.E("php.Format", "get working directory", err) } opts.Dir = cwd } @@ -80,7 +81,7 @@ func Format(ctx context.Context, opts FormatOptions) error { // Check if formatter is available formatter, found := DetectFormatter(opts.Dir) if !found { - return fmt.Errorf("no formatter found (install Laravel Pint: composer require laravel/pint --dev)") + return coreerr.E("php.Format", "no formatter found (install Laravel Pint: composer require laravel/pint --dev)", nil) } var cmdName string diff --git a/pkg/php/mutation.go b/pkg/php/mutation.go index 21c237f..3464a7c 100644 --- a/pkg/php/mutation.go +++ b/pkg/php/mutation.go @@ -7,6 +7,8 @@ import ( "os" "os/exec" "path/filepath" + + coreerr "forge.lthn.ai/core/go-log" ) // InfectionOptions configures Infection mutation testing. @@ -44,7 +46,7 @@ func RunInfection(ctx context.Context, opts InfectionOptions) error { if opts.Dir == "" { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("get working directory: %w", err) + return coreerr.E("php.RunInfection", "get working directory", err) } opts.Dir = cwd } diff --git a/pkg/php/refactor.go b/pkg/php/refactor.go index 14f9de5..02df2bf 100644 --- a/pkg/php/refactor.go +++ b/pkg/php/refactor.go @@ -2,11 +2,12 @@ package php import ( "context" - "fmt" "io" "os" "os/exec" "path/filepath" + + coreerr "forge.lthn.ai/core/go-log" ) // RectorOptions configures Rector code refactoring. @@ -40,7 +41,7 @@ func RunRector(ctx context.Context, opts RectorOptions) error { if opts.Dir == "" { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("get working directory: %w", err) + return coreerr.E("php.RunRector", "get working directory", err) } opts.Dir = cwd } diff --git a/pkg/php/test.go b/pkg/php/test.go index d54b184..8e5643c 100644 --- a/pkg/php/test.go +++ b/pkg/php/test.go @@ -2,11 +2,12 @@ package php import ( "context" - "fmt" "io" "os" "os/exec" "path/filepath" + + coreerr "forge.lthn.ai/core/go-log" ) // TestOptions configures PHP test execution. @@ -63,7 +64,7 @@ func RunTests(ctx context.Context, opts TestOptions) error { if opts.Dir == "" { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("get working directory: %w", err) + return coreerr.E("php.RunTests", "get working directory", err) } opts.Dir = cwd }