feat: add LSP server, VSCode + DataGrip extensions, release infra, autofix
CI / Test (push) Failing after 47s
CI / Build snapshot (push) Has been skipped

- 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)
This commit is contained in:
2026-06-28 12:48:28 +02:00
parent 7fb76bae3d
commit e88d32f281
49 changed files with 3228 additions and 49 deletions
+75 -6
View File
@@ -2,11 +2,12 @@ package lint
import (
"fmt"
"strings"
pg_query "github.com/pganalyze/pg_query_go/v6"
"github.com/hein/pgtidy/pkg/diagnostics"
"github.com/hein/pgtidy/pkg/pgast"
"git.warky.dev/wdevs/pgtidy/pkg/diagnostics"
"git.warky.dev/wdevs/pgtidy/pkg/pgast"
)
// MIG001 — CREATE INDEX without CONCURRENT.
@@ -30,13 +31,17 @@ func (ruleMIG001) Check(stmts []*pg_query.RawStmt, src string) []diagnostics.Dia
continue
}
line, col := pgast.LocationToLineCol(src, pgast.FirstTokenOffset(src, int(raw.StmtLocation)))
out = append(out, diagnostics.Diagnostic{
d := diagnostics.Diagnostic{
RuleID: "MIG001",
Severity: diagnostics.SeverityWarning,
Message: fmt.Sprintf("CREATE INDEX on %q without CONCURRENT blocks writes; use CREATE INDEX CONCURRENTLY", relName(s.Relation)),
Line: line,
Col: col,
})
}
if fix := mig001Fix(src, int(raw.StmtLocation), int(raw.StmtLen)); fix != nil {
d.Fix = fix
}
out = append(out, d)
}
return out
}
@@ -127,13 +132,17 @@ func (ruleMIG003) Check(stmts []*pg_query.RawStmt, src string) []diagnostics.Dia
kind = "CHECK"
}
line, col := pgast.LocationToLineCol(src, pgast.FirstTokenOffset(src, int(raw.StmtLocation)))
out = append(out, diagnostics.Diagnostic{
d := diagnostics.Diagnostic{
RuleID: "MIG003",
Severity: diagnostics.SeverityWarning,
Message: fmt.Sprintf("ALTER TABLE %q ADD %s CONSTRAINT without NOT VALID validates all rows immediately; use NOT VALID + VALIDATE CONSTRAINT", relName(tbl.Relation), kind),
Line: line,
Col: col,
})
}
if fix := mig003Fix(src, int(raw.StmtLocation), int(raw.StmtLen)); fix != nil {
d.Fix = fix
}
out = append(out, d)
}
}
return out
@@ -169,3 +178,63 @@ func relName(rv *pg_query.RangeVar) string {
}
return rv.Relname
}
// stmtText returns the text of a statement given its start offset and length.
// When stmtLen is 0 (last statement in file) it extends to EOF.
func stmtText(src string, stmtOffset, stmtLen int) string {
end := stmtOffset + stmtLen
if stmtLen == 0 || end > len(src) {
end = len(src)
}
return src[stmtOffset:end]
}
// mig001Fix builds the TextFix for MIG001: insert CONCURRENTLY after INDEX.
func mig001Fix(src string, stmtOffset, stmtLen int) *diagnostics.TextFix {
text := stmtText(src, stmtOffset, stmtLen)
upper := strings.ToUpper(text)
pos := strings.Index(upper, "INDEX")
if pos < 0 {
return nil
}
insertAt := stmtOffset + pos + len("INDEX")
return &diagnostics.TextFix{
Offset: insertAt,
End: insertAt,
New: " CONCURRENTLY",
Title: "Add CONCURRENTLY",
}
}
// mig003Fix builds the TextFix for MIG003: insert NOT VALID before the trailing semicolon.
// pg_query's StmtLen excludes the ";", which sits at src[stmtOffset+stmtLen].
func mig003Fix(src string, stmtOffset, stmtLen int) *diagnostics.TextFix {
end := stmtOffset + stmtLen
semiAt := -1
if stmtLen > 0 && end < len(src) && src[end] == ';' {
semiAt = end
} else {
// Fallback for stmtLen=0 (last statement without terminator) or edge cases.
if stmtLen == 0 {
end = len(src)
}
idx := strings.LastIndex(src[stmtOffset:end], ";")
if idx >= 0 {
semiAt = stmtOffset + idx
}
}
if semiAt < 0 {
return nil
}
// Insert " NOT VALID" just before the ";", after any trailing whitespace.
insertAt := semiAt
for insertAt > stmtOffset && (src[insertAt-1] == ' ' || src[insertAt-1] == '\t' || src[insertAt-1] == '\n' || src[insertAt-1] == '\r') {
insertAt--
}
return &diagnostics.TextFix{
Offset: insertAt,
End: insertAt,
New: " NOT VALID",
Title: "Add NOT VALID",
}
}