hcl/zcl/zclsyntax/parser.go

430 lines
11 KiB
Go
Raw Normal View History

package zclsyntax
import (
"bytes"
"fmt"
"github.com/zclconf/go-zcl/zcl"
)
type parser struct {
*peeker
// set to true if any recovery is attempted. The parser can use this
// to attempt to reduce error noise by suppressing "bad token" errors
// in recovery mode, assuming that the recovery heuristics have failed
// in this case and left the peeker in a wrong place.
recovery bool
}
func (p *parser) ParseBody(end TokenType) (*Body, zcl.Diagnostics) {
attrs := Attributes{}
blocks := Blocks{}
var diags zcl.Diagnostics
startRange := p.PrevRange()
var endRange zcl.Range
Token:
for {
next := p.Peek()
if next.Type == end {
endRange = p.NextRange()
p.Read()
break Token
}
switch next.Type {
case TokenNewline:
p.Read()
continue
case TokenIdent:
item, itemDiags := p.ParseBodyItem()
diags = append(diags, itemDiags...)
switch titem := item.(type) {
case *Block:
blocks = append(blocks, titem)
case *Attribute:
if existing, exists := attrs[titem.Name]; exists {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Attribute redefined",
Detail: fmt.Sprintf(
"The attribute %q was already defined at %s. Each attribute may be defined only once.",
titem.Name, existing.NameRange.String(),
),
Subject: &titem.NameRange,
})
} else {
attrs[titem.Name] = titem
}
default:
// This should never happen for valid input, but may if a
// syntax error was detected in ParseBodyItem that prevented
// it from even producing a partially-broken item. In that
// case, it would've left at least one error in the diagnostics
// slice we already dealt with above.
//
// We'll assume ParseBodyItem attempted recovery to leave
// us in a reasonable position to try parsing the next item.
continue
}
default:
bad := p.Read()
if bad.Type == TokenOQuote {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid attribute name",
Detail: "Attribute names must not be quoted.",
Subject: &bad.Range,
})
} else {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Attribute or block definition required",
Detail: "An attribute or block definition is required here.",
Subject: &bad.Range,
})
}
endRange = p.PrevRange() // arbitrary, but somewhere inside the body means better diagnostics
p.recover(end) // attempt to recover to the token after the end of this body
break Token
}
}
return &Body{
Attributes: attrs,
Blocks: blocks,
SrcRange: zcl.RangeBetween(startRange, endRange),
EndRange: zcl.Range{
Filename: endRange.Filename,
Start: endRange.End,
End: endRange.End,
},
}, diags
}
func (p *parser) ParseBodyItem() (Node, zcl.Diagnostics) {
ident := p.Read()
if ident.Type != TokenIdent {
p.recoverAfterBodyItem()
return nil, zcl.Diagnostics{
{
Severity: zcl.DiagError,
Summary: "Attribute or block definition required",
Detail: "An attribute or block definition is required here.",
Subject: &ident.Range,
},
}
}
next := p.Peek()
switch next.Type {
case TokenEqual:
return p.finishParsingBodyAttribute(ident)
case TokenOQuote, TokenOBrace:
return p.finishParsingBodyBlock(ident)
default:
p.recoverAfterBodyItem()
return nil, zcl.Diagnostics{
{
Severity: zcl.DiagError,
Summary: "Attribute or block definition required",
Detail: "An attribute or block definition is required here. To define an attribute, use the equals sign \"=\" to introduce the attribute value.",
Subject: &ident.Range,
},
}
}
return nil, nil
}
func (p *parser) finishParsingBodyAttribute(ident Token) (Node, zcl.Diagnostics) {
panic("attribute parsing not yet implemented")
}
func (p *parser) finishParsingBodyBlock(ident Token) (Node, zcl.Diagnostics) {
var blockType = string(ident.Bytes)
var diags zcl.Diagnostics
var labels []string
var labelRanges []zcl.Range
var oBrace Token
Token:
for {
tok := p.Peek()
switch tok.Type {
case TokenOBrace:
oBrace = p.Read()
break Token
case TokenOQuote:
label, labelRange, labelDiags := p.parseQuotedStringLiteral()
diags = append(diags, labelDiags...)
labels = append(labels, label)
labelRanges = append(labelRanges, labelRange)
default:
switch tok.Type {
case TokenEqual:
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid block definition",
Detail: "The equals sign \"=\" indicates an attribute definition, and must not be used when defining a block.",
Subject: &tok.Range,
Context: zcl.RangeBetween(ident.Range, tok.Range).Ptr(),
})
case TokenNewline:
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid block definition",
Detail: "A block definition must have block content delimited by \"{\" and \"}\", starting on the same line as the block header.",
Subject: &tok.Range,
Context: zcl.RangeBetween(ident.Range, tok.Range).Ptr(),
})
default:
if !p.recovery {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid block definition",
Detail: "Either a quoted string block label or an opening brace (\"{\") is expected here.",
Subject: &tok.Range,
Context: zcl.RangeBetween(ident.Range, tok.Range).Ptr(),
})
}
}
p.recoverAfterBodyItem()
return &Block{
Type: blockType,
Labels: labels,
Body: nil,
TypeRange: ident.Range,
LabelRanges: labelRanges,
OpenBraceRange: ident.Range, // placeholder
CloseBraceRange: ident.Range, // placeholder
}, diags
}
}
// Once we fall out here, the peeker is pointed just after our opening
// brace, so we can begin our nested body parsing.
body, bodyDiags := p.ParseBody(TokenCBrace)
diags = append(diags, bodyDiags...)
cBraceRange := p.PrevRange()
return &Block{
Type: blockType,
Labels: labels,
Body: body,
TypeRange: ident.Range,
LabelRanges: labelRanges,
OpenBraceRange: oBrace.Range,
CloseBraceRange: cBraceRange,
}, diags
}
// parseQuotedStringLiteral is a helper for parsing quoted strings that
// aren't allowed to contain any interpolations, such as block labels.
func (p *parser) parseQuotedStringLiteral() (string, zcl.Range, zcl.Diagnostics) {
oQuote := p.Read()
if oQuote.Type != TokenOQuote {
return "", oQuote.Range, zcl.Diagnostics{
{
Severity: zcl.DiagError,
Summary: "Invalid string literal",
Detail: "A quoted string is required here.",
Subject: &oQuote.Range,
},
}
}
var diags zcl.Diagnostics
ret := &bytes.Buffer{}
var cQuote Token
Token:
for {
tok := p.Read()
switch tok.Type {
case TokenCQuote:
cQuote = tok
break Token
case TokenStringLit:
// TODO: Remove any escape sequences from the string, once we
// have a function with which to do that.
ret.Write(tok.Bytes)
case TokenTemplateControl, TokenTemplateInterp:
which := "$"
if tok.Type == TokenTemplateControl {
which = "!"
}
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid string literal",
Detail: fmt.Sprintf(
"Template sequences are not allowed in this string. To include a literal %q, double it (as \"%s%s\") to escape it.",
which, which, which,
),
Subject: &tok.Range,
Context: zcl.RangeBetween(oQuote.Range, tok.Range).Ptr(),
})
p.recover(TokenTemplateSeqEnd)
case TokenEOF:
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Unterminated string literal",
Detail: "Unable to find the closing quote mark before the end of the file.",
Subject: &tok.Range,
Context: zcl.RangeBetween(oQuote.Range, tok.Range).Ptr(),
})
break Token
default:
// Should never happen, as long as the scanner is behaving itself
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid string literal",
Detail: "This item is not valid in a string literal.",
Subject: &tok.Range,
Context: zcl.RangeBetween(oQuote.Range, tok.Range).Ptr(),
})
p.recover(TokenOQuote)
break Token
}
}
return ret.String(), zcl.RangeBetween(oQuote.Range, cQuote.Range), diags
}
// recover seeks forward in the token stream until it finds TokenType "end",
// then returns with the peeker pointed at the following token.
//
// If the given token type is a bracketer, this function will additionally
// count nested instances of the brackets to try to leave the peeker at
// the end of the _current_ instance of that bracketer, skipping over any
// nested instances. This is a best-effort operation and may have
// unpredictable results on input with bad bracketer nesting.
func (p *parser) recover(end TokenType) {
start := p.oppositeBracket(end)
p.recovery = true
nest := 0
for {
tok := p.Read()
ty := tok.Type
if end == TokenTemplateSeqEnd && ty == TokenTemplateControl {
// normalize so that our matching behavior can work, since
// TokenTemplateControl/TokenTemplateInterp are asymmetrical
// with TokenTemplateSeqEnd and thus we need to count both
// openers if that's the closer we're looking for.
ty = TokenTemplateInterp
}
switch ty {
case start:
nest++
case end:
if nest < 1 {
return
}
nest--
}
}
}
// recoverOver seeks forward in the token stream until it finds a block
// starting with TokenType "start", then finds the corresponding end token,
// leaving the peeker pointed at the token after that end token.
//
// The given token type _must_ be a bracketer. For example, if the given
// start token is TokenOBrace then the parser will be left at the _end_ of
// the next brace-delimited block encountered, or at EOF if no such block
// is found or it is unclosed.
func (p *parser) recoverOver(start TokenType) {
end := p.oppositeBracket(start)
// find the opening bracket first
Token:
for {
tok := p.Read()
switch tok.Type {
case start:
break Token
}
}
// Now use our existing recover function to locate the _end_ of the
// container we've found.
p.recover(end)
}
func (p *parser) recoverAfterBodyItem() {
// TODO: Seek forward until we find a newline that isn't inside any
// bracketer, and return with the peeker placed after it so we're
// ready to try to parse another body item.
p.recovery = true
}
// oppositeBracket finds the bracket that opposes the given bracketer, or
// NilToken if the given token isn't a bracketer.
//
// "Bracketer", for the sake of this function, is one end of a matching
// open/close set of tokens that establish a bracketing context.
func (p *parser) oppositeBracket(ty TokenType) TokenType {
switch ty {
case TokenOBrace:
return TokenCBrace
case TokenOBrack:
return TokenCBrack
case TokenOParen:
return TokenCParen
case TokenOQuote:
return TokenCQuote
case TokenOHeredoc:
return TokenCHeredoc
case TokenCBrace:
return TokenOBrace
case TokenCBrack:
return TokenOBrack
case TokenCParen:
return TokenOParen
case TokenCQuote:
return TokenOQuote
case TokenCHeredoc:
return TokenOHeredoc
case TokenTemplateControl:
return TokenTemplateSeqEnd
case TokenTemplateInterp:
return TokenTemplateSeqEnd
case TokenTemplateSeqEnd:
// This is ambigous, but we return Interp here because that's
// what's assumed by the "recover" method.
return TokenTemplateInterp
default:
return TokenNil
}
}