package main import ( "fmt" "io" "os" "strings" "github.com/hein/pgtidy/pkg/diagnostics" "github.com/hein/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 files []string ) for _, a := range args { switch { case a == "-h" || a == "--help": lintUsage(stdout) return 0 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 } 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 } 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: --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) `) }