Files
PgTidy/cmd/pgtidy/lint.go
T
Hein 04711cf7b2
CI / Test (push) Successful in 28s
CI / Build (push) Successful in 25s
fix(cmd): handle errors and improve output formatting
* update error handling in various commands to use blank identifier
* enhance output formatting for better readability
* add golangci-lint to Makefile for linting checks
2026-07-01 12:53:25 +02:00

173 lines
4.1 KiB
Go

package main
import (
"fmt"
"io"
"os"
"strings"
"git.warky.dev/wdevs/pgtidy/pkg/diagnostics"
"git.warky.dev/wdevs/pgtidy/pkg/lint"
)
// cmdLint implements `pgtidy lint`. Reads one or more SQL files (or stdin) and
// reports lint findings to stdout. Exits non-zero when any findings are found.
//
// Output format (one line per finding):
//
// file:line:col: [RULEID] severity: message
func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
var (
only []string // --only=ID,ID rule filter
fix bool
files []string
)
for _, a := range args {
switch {
case a == "-h" || a == "--help":
lintUsage(stdout)
return 0
case a == "--fix":
fix = true
case strings.HasPrefix(a, "--only="):
ids := strings.Split(strings.TrimPrefix(a, "--only="), ",")
for _, id := range ids {
if id = strings.TrimSpace(id); id != "" {
only = append(only, strings.ToUpper(id))
}
}
case len(a) > 1 && a[0] == '-':
_, _ = fmt.Fprintf(stderr, "pgtidy lint: unknown flag %q\n", a)
return 2
default:
files = append(files, a)
}
}
eng := lint.New()
check := func(sql, file string) ([]diagnostics.Diagnostic, error) {
diags, err := eng.Check(sql, file)
if err != nil {
return nil, err
}
if len(only) > 0 {
filtered := diags[:0]
for _, d := range diags {
for _, id := range only {
if d.RuleID == id {
filtered = append(filtered, d)
break
}
}
}
diags = filtered
}
return diags, nil
}
printDiags := func(diags []diagnostics.Diagnostic) {
for _, d := range diags {
loc := d.File
if d.Line > 0 {
loc = fmt.Sprintf("%s:%d:%d", d.File, d.Line, d.Col)
}
if loc == "" {
loc = "stdin"
}
_, _ = fmt.Fprintf(stdout, "%s: [%s] %s: %s\n", loc, d.RuleID, d.Severity, d.Message)
}
}
found := false
if len(files) == 0 {
src, err := io.ReadAll(stdin)
if err != nil {
_, _ = fmt.Fprintf(stderr, "pgtidy: reading stdin: %v\n", err)
return 2
}
diags, err := check(string(src), "")
if err != nil {
_, _ = fmt.Fprintf(stderr, "pgtidy: %v\n", err)
return 2
}
if fix {
fixed := lint.ApplyFixes(string(src), diags)
if fixed != string(src) {
_, _ = io.WriteString(stdout, fixed)
return 0
}
}
printDiags(diags)
if len(diags) > 0 {
found = true
}
} else {
for _, path := range files {
src, err := os.ReadFile(path)
if err != nil {
_, _ = fmt.Fprintf(stderr, "pgtidy: %v\n", err)
return 2
}
diags, err := check(string(src), path)
if err != nil {
_, _ = fmt.Fprintf(stderr, "pgtidy: %v\n", err)
return 2
}
if fix {
fixed := lint.ApplyFixes(string(src), diags)
if fixed != string(src) {
if err := os.WriteFile(path, []byte(fixed), 0o644); err != nil {
_, _ = fmt.Fprintf(stderr, "pgtidy: writing %s: %v\n", path, err)
return 2
}
// Re-check to report any remaining unfixed diagnostics.
diags, err = check(fixed, path)
if err != nil {
_, _ = fmt.Fprintf(stderr, "pgtidy: %v\n", err)
return 2
}
}
}
printDiags(diags)
if len(diags) > 0 {
found = true
}
}
}
if found {
return 1
}
return 0
}
func lintUsage(w io.Writer) {
_, _ = fmt.Fprint(w, `Usage: pgtidy lint [flags] [file ...]
Read SQL from files (or stdin) and report lint findings.
Flags:
--fix Apply autofixes for fixable rules (MIG001, MIG003) and rewrite files
--only=ID,... comma-separated rule IDs to enable (default: all rules)
-h, --help show this help
Rules:
MIG001 CREATE INDEX without CONCURRENTLY blocks writes
MIG002 ALTER TABLE ADD COLUMN NOT NULL with no DEFAULT may rewrite table
MIG003 ALTER TABLE ADD CONSTRAINT without NOT VALID validates all rows
COR001 SELECT * is fragile; enumerate columns explicitly
COR002 UPDATE without WHERE modifies every row
COR003 DELETE without WHERE removes every row
NAM001 Table name is not snake_case
NAM002 Column name is not snake_case
NAM003 Function/procedure name is not snake_case
Exit codes:
0 no findings
1 one or more findings
2 error (file not found, parse error, bad flag)
`)
}