Files
PgTidy/pkg/lint/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

120 lines
3.1 KiB
Go

// Package lint provides the rule engine and built-in rule packs for PgTidy.
//
// Usage:
//
// eng := lint.New()
// diags, err := eng.Check(sql, filename)
package lint
import (
"sort"
pg_query "github.com/pganalyze/pg_query_go/v6"
"git.warky.dev/wdevs/pgtidy/pkg/diagnostics"
"git.warky.dev/wdevs/pgtidy/pkg/pgast"
)
// Rule is implemented by each lint rule.
type Rule interface {
// ID returns the stable rule identifier (e.g. "MIG001").
ID() string
// Severity returns the default severity for findings from this rule.
Severity() diagnostics.Severity
// Check inspects the parsed statement list and returns any findings.
// src is the original SQL string, used for location lookups.
Check(stmts []*pg_query.RawStmt, src string) []diagnostics.Diagnostic
}
// Engine runs a set of rules against SQL input.
type Engine struct {
rules []Rule
}
// New returns an Engine loaded with all built-in rules.
func New() *Engine {
e := &Engine{}
e.registerAll()
return e
}
// Custom returns an empty Engine; use Register to add rules.
func Custom() *Engine {
return &Engine{}
}
func (e *Engine) registerAll() {
e.Register(ruleMIG001{})
e.Register(ruleMIG002{})
e.Register(ruleMIG003{})
e.Register(ruleCOR001{})
e.Register(ruleCOR002{})
e.Register(ruleCOR003{})
e.Register(ruleNAM001{})
e.Register(ruleNAM002{})
e.Register(ruleNAM003{})
}
// Register adds a rule to the engine.
func (e *Engine) Register(r Rule) {
e.rules = append(e.rules, r)
}
// Rules returns a copy of the registered rule slice (for testing/inspection).
func (e *Engine) Rules() []Rule {
out := make([]Rule, len(e.rules))
copy(out, e.rules)
return out
}
// Check parses sql and runs all registered rules. Diagnostics are sorted by
// (line, col, ruleID). A parse error is returned as a single diagnostic with
// ruleID "PARSE".
func (e *Engine) Check(sql, file string) ([]diagnostics.Diagnostic, error) {
result, err := pgast.Parse(sql)
if err != nil {
line, col := parseErrLocation(err, sql)
return []diagnostics.Diagnostic{{
RuleID: "PARSE",
Severity: diagnostics.SeverityError,
Message: err.Error(),
File: file,
Line: line,
Col: col,
}}, nil
}
var all []diagnostics.Diagnostic
for _, r := range e.rules {
found := r.Check(result.Stmts, sql)
for i := range found {
found[i].File = file
}
all = append(all, found...)
}
sort.Slice(all, func(i, j int) bool {
if all[i].Line != all[j].Line {
return all[i].Line < all[j].Line
}
if all[i].Col != all[j].Col {
return all[i].Col < all[j].Col
}
return all[i].RuleID < all[j].RuleID
})
return all, nil
}
// parseErrLocation extracts a (line, col) from a pg_query parse error.
// The error message format is "syntax error at or near ... (location N)" where
// N is a 1-based character offset.
func parseErrLocation(err error, sql string) (line, col int) {
// pg_query errors expose Location via a type assertion.
type locErr interface{ GetCursorPos() int }
if le, ok := err.(locErr); ok {
pos := le.GetCursorPos() - 1 // convert to 0-based
return pgast.LocationToLineCol(sql, pos)
}
return 1, 1
}