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,125 @@
|
||||
package lint_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/hein/pgtidy/pkg/diagnostics"
|
||||
"github.com/hein/pgtidy/pkg/lint"
|
||||
)
|
||||
|
||||
func fixtureDir() string {
|
||||
return filepath.Join("..", "..", "testdata", "lint")
|
||||
}
|
||||
|
||||
func checkFile(t *testing.T, name string) []diagnostics.Diagnostic {
|
||||
t.Helper()
|
||||
src, err := os.ReadFile(filepath.Join(fixtureDir(), name))
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", name, err)
|
||||
}
|
||||
eng := lint.New()
|
||||
diags, err := eng.Check(string(src), name)
|
||||
if err != nil {
|
||||
t.Fatalf("Check(%s): %v", name, err)
|
||||
}
|
||||
return diags
|
||||
}
|
||||
|
||||
func ruleIDs(diags []diagnostics.Diagnostic) map[string]int {
|
||||
m := make(map[string]int)
|
||||
for _, d := range diags {
|
||||
m[d.RuleID]++
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func TestMigrationViolations(t *testing.T) {
|
||||
diags := checkFile(t, "migration_violations.sql")
|
||||
ids := ruleIDs(diags)
|
||||
|
||||
cases := []struct {
|
||||
id string
|
||||
count int
|
||||
}{
|
||||
{"MIG001", 1}, // one non-concurrent index
|
||||
{"MIG002", 1}, // one NOT NULL + no DEFAULT
|
||||
{"MIG003", 2}, // one FK + one CHECK without NOT VALID
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := ids[c.id]; got != c.count {
|
||||
t.Errorf("rule %s: want %d findings, got %d", c.id, c.count, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrationClean(t *testing.T) {
|
||||
diags := checkFile(t, "migration_clean.sql")
|
||||
ids := ruleIDs(diags)
|
||||
for _, id := range []string{"MIG001", "MIG002", "MIG003"} {
|
||||
if n := ids[id]; n != 0 {
|
||||
t.Errorf("rule %s: want 0 findings on clean fixture, got %d", id, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorrectnessViolations(t *testing.T) {
|
||||
diags := checkFile(t, "correctness_violations.sql")
|
||||
ids := ruleIDs(diags)
|
||||
|
||||
cases := []struct {
|
||||
id string
|
||||
count int
|
||||
}{
|
||||
{"COR001", 1},
|
||||
{"COR002", 1},
|
||||
{"COR003", 1},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := ids[c.id]; got != c.count {
|
||||
t.Errorf("rule %s: want %d findings, got %d", c.id, c.count, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamingViolations(t *testing.T) {
|
||||
diags := checkFile(t, "naming_violations.sql")
|
||||
ids := ruleIDs(diags)
|
||||
|
||||
cases := []struct {
|
||||
id string
|
||||
count int
|
||||
}{
|
||||
{"NAM001", 1}, // UserAccounts
|
||||
{"NAM002", 2}, // userId, emailAddress
|
||||
{"NAM003", 1}, // GetUserById
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := ids[c.id]; got != c.count {
|
||||
t.Errorf("rule %s: want %d findings, got %d", c.id, c.count, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseError(t *testing.T) {
|
||||
eng := lint.New()
|
||||
diags, err := eng.Check("SELECT FROM WHERE", "test.sql")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(diags) == 0 || diags[0].RuleID != "PARSE" {
|
||||
t.Errorf("expected a PARSE diagnostic, got %v", diags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomEngine(t *testing.T) {
|
||||
eng := lint.Custom()
|
||||
eng.Register(lint.New().Rules()[0]) // register first rule only
|
||||
// Just confirm it doesn't panic and returns results.
|
||||
diags, err := eng.Check("SELECT 1", "stdin")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
_ = diags
|
||||
}
|
||||
Reference in New Issue
Block a user