refactor: replace all remaining fmt.Errorf with coreerr.E from go-log

Replaces ~46 fmt.Errorf and errors.New calls in production code across
pkg/lint, pkg/php, cmd/core-lint, and cmd/qa with structured coreerr.E()
calls using the forge.lthn.ai/core/go-log convention.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-16 21:29:34 +00:00
parent a36f835fe0
commit 9681b062ac
13 changed files with 66 additions and 52 deletions

View file

@ -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, "", " ")

View file

@ -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
}

View file

@ -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

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}