Files
PgTidy/cmd/pgtidy/lint.go
T
warkanum e88d32f281
CI / Test (push) Failing after 47s
CI / Build snapshot (push) Has been skipped
feat: add LSP server, VSCode + DataGrip extensions, release infra, autofix
- pkg/lsp: JSON-RPC 2.0 LSP server (formatting, diagnostics, codeAction quick-fixes)
- cmd/pgtidy: lsp and config subcommands
- pkg/diagnostics: TextFix struct for byte-range autofixes
- pkg/lint: MIG001/MIG003 autofixes, ApplyFixes helper, --fix flag on lint command
- editors/vscode: TypeScript extension with LanguageClient, showVersion/showConfig/formatDocument commands, logo
- editors/datagrip: Gradle JetBrains plugin via LSP4IJ, pluginIcon
- .goreleaser.yaml, .github/workflows: CI + release pipeline
- Makefile: snapshot, release, vscode-compile, vscode-package targets
- go.mod + all imports: module path updated to git.warky.dev/wdevs/pgtidy
- assets: logo files (256px, 128px, 1024px, ico)
2026-06-28 12:48:28 +02:00

173 lines
4.0 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)
`)
}