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

128 lines
3.8 KiB
Go

package lint
import (
"fmt"
pg_query "github.com/pganalyze/pg_query_go/v6"
"git.warky.dev/wdevs/pgtidy/pkg/diagnostics"
"git.warky.dev/wdevs/pgtidy/pkg/pgast"
)
// COR001 — SELECT *.
// Selecting all columns by wildcard is fragile: column additions/removals
// break callers silently. Enumerate the columns you need explicitly.
type ruleCOR001 struct{}
func (ruleCOR001) ID() string { return "COR001" }
func (ruleCOR001) Severity() diagnostics.Severity { return diagnostics.SeverityHint }
func (ruleCOR001) Check(stmts []*pg_query.RawStmt, src string) []diagnostics.Diagnostic {
var out []diagnostics.Diagnostic
for _, raw := range stmts {
walkSelectStar(raw.Stmt, src, &out)
}
return out
}
func walkSelectStar(node *pg_query.Node, src string, out *[]diagnostics.Diagnostic) {
if node == nil {
return
}
sel, ok := node.GetNode().(*pg_query.Node_SelectStmt)
if !ok {
return
}
checkSelectStmt(sel.SelectStmt, src, out)
}
func checkSelectStmt(s *pg_query.SelectStmt, src string, out *[]diagnostics.Diagnostic) {
if s == nil {
return
}
for _, t := range s.TargetList {
rt, ok := t.GetNode().(*pg_query.Node_ResTarget)
if !ok {
continue
}
cr, ok := rt.ResTarget.Val.GetNode().(*pg_query.Node_ColumnRef)
if !ok {
continue
}
for _, f := range cr.ColumnRef.Fields {
if _, isStar := f.GetNode().(*pg_query.Node_AStar); isStar {
line, col := pgast.LocationToLineCol(src, int(cr.ColumnRef.Location))
*out = append(*out, diagnostics.Diagnostic{
RuleID: "COR001",
Severity: diagnostics.SeverityHint,
Message: "SELECT * is fragile; enumerate the columns explicitly",
Line: line,
Col: col,
})
}
}
}
// Recurse into set-operation branches (UNION, INTERSECT, EXCEPT).
checkSelectStmt(s.Larg, src, out)
checkSelectStmt(s.Rarg, src, out)
}
// COR002 — UPDATE without WHERE.
// An UPDATE with no WHERE clause modifies every row in the table.
type ruleCOR002 struct{}
func (ruleCOR002) ID() string { return "COR002" }
func (ruleCOR002) Severity() diagnostics.Severity { return diagnostics.SeverityWarning }
func (ruleCOR002) Check(stmts []*pg_query.RawStmt, src string) []diagnostics.Diagnostic {
var out []diagnostics.Diagnostic
for _, raw := range stmts {
u, ok := raw.Stmt.GetNode().(*pg_query.Node_UpdateStmt)
if !ok {
continue
}
if u.UpdateStmt.WhereClause != nil {
continue
}
line, col := pgast.LocationToLineCol(src, pgast.FirstTokenOffset(src, int(raw.StmtLocation)))
out = append(out, diagnostics.Diagnostic{
RuleID: "COR002",
Severity: diagnostics.SeverityWarning,
Message: fmt.Sprintf("UPDATE %q has no WHERE clause; this modifies every row", relName(u.UpdateStmt.Relation)),
Line: line,
Col: col,
})
}
return out
}
// COR003 — DELETE without WHERE.
// A DELETE with no WHERE clause removes every row from the table.
// Use TRUNCATE if you intend a full wipe; it is faster and explicit.
type ruleCOR003 struct{}
func (ruleCOR003) ID() string { return "COR003" }
func (ruleCOR003) Severity() diagnostics.Severity { return diagnostics.SeverityWarning }
func (ruleCOR003) Check(stmts []*pg_query.RawStmt, src string) []diagnostics.Diagnostic {
var out []diagnostics.Diagnostic
for _, raw := range stmts {
d, ok := raw.Stmt.GetNode().(*pg_query.Node_DeleteStmt)
if !ok {
continue
}
if d.DeleteStmt.WhereClause != nil {
continue
}
line, col := pgast.LocationToLineCol(src, pgast.FirstTokenOffset(src, int(raw.StmtLocation)))
out = append(out, diagnostics.Diagnostic{
RuleID: "COR003",
Severity: diagnostics.SeverityWarning,
Message: fmt.Sprintf("DELETE FROM %q has no WHERE clause; this removes every row (use TRUNCATE if intentional)", relName(d.DeleteStmt.Relation)),
Line: line,
Col: col,
})
}
return out
}