feat: initial plan
This commit is contained in:
+173
@@ -0,0 +1,173 @@
|
||||
// Package cst defines PgTidy's concrete syntax tree.
|
||||
//
|
||||
// The CST is lossless: every lexer token (significant tokens and the trivia —
|
||||
// whitespace/comments — attached to them) is reachable, so File.Source()
|
||||
// reproduces the original input byte-for-byte. The parser builds structured
|
||||
// nodes only where it is confident; everything else is captured verbatim in a
|
||||
// Raw node. This makes "graceful degradation" a property of the data model
|
||||
// rather than something the printer must remember to do.
|
||||
package cst
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/hein/pgtidy/pkg/lexer"
|
||||
)
|
||||
|
||||
// Trivia is a run of whitespace/comment tokens preceding a significant token.
|
||||
type Trivia []lexer.Token
|
||||
|
||||
// Tok is a significant (non-trivia) token together with the trivia that
|
||||
// immediately precedes it in the source.
|
||||
type Tok struct {
|
||||
Lead Trivia
|
||||
Tok lexer.Token
|
||||
}
|
||||
|
||||
// Text returns the significant token's text.
|
||||
func (t Tok) Text() string { return t.Tok.Text }
|
||||
|
||||
// Is reports whether the token is an unquoted word equal (case-insensitively)
|
||||
// to kw.
|
||||
func (t Tok) Is(kw string) bool {
|
||||
return t.Tok.Kind == lexer.Ident && strings.EqualFold(t.Tok.Text, kw)
|
||||
}
|
||||
|
||||
// Comments returns just the comment tokens from the leading trivia.
|
||||
func (t Tok) Comments() []lexer.Token {
|
||||
var out []lexer.Token
|
||||
for _, tr := range t.Lead {
|
||||
if tr.Kind == lexer.LineComment || tr.Kind == lexer.BlockComment {
|
||||
out = append(out, tr)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Attach converts a lossless lexer stream into significant tokens, each
|
||||
// carrying its leading trivia, plus any trailing trivia before EOF.
|
||||
func Attach(toks []lexer.Token) (sig []Tok, trailing Trivia) {
|
||||
var lead Trivia
|
||||
for _, t := range toks {
|
||||
if t.Kind == lexer.EOF {
|
||||
trailing = lead
|
||||
break
|
||||
}
|
||||
if t.IsTrivia() {
|
||||
lead = append(lead, t)
|
||||
continue
|
||||
}
|
||||
sig = append(sig, Tok{Lead: lead, Tok: t})
|
||||
lead = nil
|
||||
}
|
||||
return sig, trailing
|
||||
}
|
||||
|
||||
// Node is any element of a File.
|
||||
type Node interface {
|
||||
appendTokens(*[]Tok)
|
||||
}
|
||||
|
||||
// Tokens returns all significant tokens of a node in source order.
|
||||
func Tokens(n Node) []Tok {
|
||||
var t []Tok
|
||||
n.appendTokens(&t)
|
||||
return t
|
||||
}
|
||||
|
||||
// File is the whole parsed input.
|
||||
type File struct {
|
||||
Items []Node
|
||||
Trailing Trivia // trivia after the last significant token
|
||||
}
|
||||
|
||||
// Source reconstructs the original input from the tree. With a correct parse
|
||||
// this is byte-for-byte identical to the lexer input.
|
||||
func (f *File) Source() string {
|
||||
var toks []Tok
|
||||
for _, it := range f.Items {
|
||||
it.appendTokens(&toks)
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, t := range toks {
|
||||
for _, tr := range t.Lead {
|
||||
b.WriteString(tr.Text)
|
||||
}
|
||||
b.WriteString(t.Tok.Text)
|
||||
}
|
||||
for _, tr := range f.Trailing {
|
||||
b.WriteString(tr.Text)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Raw is an unstructured statement: a verbatim run of significant tokens. This
|
||||
// is the graceful-degradation fallback for anything the parser does not (yet)
|
||||
// structure.
|
||||
type Raw struct {
|
||||
Toks []Tok
|
||||
}
|
||||
|
||||
func (r *Raw) appendTokens(out *[]Tok) { *out = append(*out, r.Toks...) }
|
||||
|
||||
// Param is one entry in a function/procedure parameter list, with the comma
|
||||
// that separates it from the next entry (nil on the last parameter).
|
||||
type Param struct {
|
||||
Toks []Tok
|
||||
Sep *Tok // trailing comma, nil if last
|
||||
}
|
||||
|
||||
// CreateFunction is a parsed CREATE [OR REPLACE] FUNCTION|PROCEDURE statement.
|
||||
//
|
||||
// Field order matches source order; appendTokens emits them contiguously so the
|
||||
// node round-trips exactly.
|
||||
type CreateFunction struct {
|
||||
Head []Tok // CREATE [OR REPLACE] FUNCTION|PROCEDURE
|
||||
Name []Tok // name (possibly schema-qualified)
|
||||
LParen Tok //
|
||||
Params []Param // parameter list (may be empty)
|
||||
RParen Tok //
|
||||
Options [][]Tok // option clauses before the body (RETURNS/LANGUAGE/VOLATILE/...)
|
||||
As *Tok // the AS keyword, if present
|
||||
Body *Tok // the body token (dollar-quoted string or string), if present
|
||||
Tail [][]Tok // option clauses after the body (rare)
|
||||
Semi *Tok // terminating semicolon, if present
|
||||
}
|
||||
|
||||
// IsProcedure reports whether this is a PROCEDURE (vs FUNCTION).
|
||||
func (c *CreateFunction) IsProcedure() bool {
|
||||
for _, t := range c.Head {
|
||||
if t.Is("procedure") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *CreateFunction) appendTokens(out *[]Tok) {
|
||||
*out = append(*out, c.Head...)
|
||||
*out = append(*out, c.Name...)
|
||||
*out = append(*out, c.LParen)
|
||||
for _, p := range c.Params {
|
||||
*out = append(*out, p.Toks...)
|
||||
if p.Sep != nil {
|
||||
*out = append(*out, *p.Sep)
|
||||
}
|
||||
}
|
||||
*out = append(*out, c.RParen)
|
||||
for _, cl := range c.Options {
|
||||
*out = append(*out, cl...)
|
||||
}
|
||||
if c.As != nil {
|
||||
*out = append(*out, *c.As)
|
||||
}
|
||||
if c.Body != nil {
|
||||
*out = append(*out, *c.Body)
|
||||
}
|
||||
for _, cl := range c.Tail {
|
||||
*out = append(*out, cl...)
|
||||
}
|
||||
if c.Semi != nil {
|
||||
*out = append(*out, *c.Semi)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user