feat(lint): add linter rules for migration and naming conventions
* Implement migration safety rules: - MIG001: Warn on CREATE INDEX without CONCURRENT. - MIG002: Warn on ALTER TABLE ADD COLUMN NOT NULL without DEFAULT. - MIG003: Warn on ALTER TABLE ADD CONSTRAINT without NOT VALID. * Implement naming conventions rules: - NAM001: Warn on non-snake_case table names. - NAM002: Warn on non-snake_case column names in CREATE TABLE. - NAM003: Warn on non-snake_case function names. * Add test fixtures for all new rules.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
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)
|
||||
`)
|
||||
}
|
||||
+13
-5
@@ -1,5 +1,4 @@
|
||||
// Command pgtidy is the PgTidy CLI: a PostgreSQL formatter (and, later, linter)
|
||||
// and LSP server. Today it provides the `fmt` subcommand.
|
||||
// Command pgtidy is the PgTidy CLI: a PostgreSQL formatter and linter.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -23,6 +22,8 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
switch args[0] {
|
||||
case "fmt", "format":
|
||||
return cmdFmt(args[1:], stdin, stdout, stderr)
|
||||
case "lint":
|
||||
return cmdLint(args[1:], stdin, stdout, stderr)
|
||||
case "version", "--version", "-v":
|
||||
fmt.Fprintf(stdout, "pgtidy %s\n", version)
|
||||
return 0
|
||||
@@ -40,13 +41,20 @@ func usage(w io.Writer) {
|
||||
fmt.Fprint(w, `pgtidy — PostgreSQL formatter and linter
|
||||
|
||||
Usage:
|
||||
pgtidy fmt [flags] [files...] Format SQL/PL-pgSQL (stdin if no files)
|
||||
pgtidy version Print version
|
||||
pgtidy help Show this help
|
||||
pgtidy fmt [flags] [files...] Format SQL/PL-pgSQL (stdin if no files)
|
||||
pgtidy lint [flags] [files...] Lint SQL (stdin if no files)
|
||||
pgtidy version Print version
|
||||
pgtidy help Show this help
|
||||
|
||||
fmt flags:
|
||||
-w, --write Rewrite files in place
|
||||
-l, --list List files whose formatting differs (no writes)
|
||||
-d, --diff Print unified diff of changes
|
||||
--check Exit non-zero if any input is not already formatted (CI)
|
||||
|
||||
lint flags:
|
||||
--only=ID,... Enable only the specified rule IDs (comma-separated)
|
||||
|
||||
Run 'pgtidy lint --help' for the full rule list.
|
||||
`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user