diff --git a/zcl/zclsyntax/parse_traversal_test.go b/zcl/zclsyntax/parse_traversal_test.go new file mode 100644 index 0000000..8368020 --- /dev/null +++ b/zcl/zclsyntax/parse_traversal_test.go @@ -0,0 +1,227 @@ +package zclsyntax + +import ( + "testing" + + "reflect" + + "github.com/davecgh/go-spew/spew" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-zcl/zcl" +) + +func TestParseTraversalAbs(t *testing.T) { + tests := []struct { + src string + want zcl.Traversal + diagCount int + }{ + { + "", + nil, + 1, // variable name required + }, + { + "foo", + zcl.Traversal{ + zcl.TraverseRoot{ + Name: "foo", + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: zcl.Pos{Line: 1, Column: 4, Byte: 3}, + }, + }, + }, + 0, + }, + { + "foo.bar.baz", + zcl.Traversal{ + zcl.TraverseRoot{ + Name: "foo", + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: zcl.Pos{Line: 1, Column: 4, Byte: 3}, + }, + }, + zcl.TraverseAttr{ + Name: "bar", + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 4, Byte: 3}, + End: zcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + zcl.TraverseAttr{ + Name: "baz", + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: zcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + }, + }, + 0, + }, + { + "foo[1]", + zcl.Traversal{ + zcl.TraverseRoot{ + Name: "foo", + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: zcl.Pos{Line: 1, Column: 4, Byte: 3}, + }, + }, + zcl.TraverseIndex{ + Key: cty.NumberIntVal(1), + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 4, Byte: 3}, + End: zcl.Pos{Line: 1, Column: 7, Byte: 6}, + }, + }, + }, + 0, + }, + { + "foo[1][2]", + zcl.Traversal{ + zcl.TraverseRoot{ + Name: "foo", + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: zcl.Pos{Line: 1, Column: 4, Byte: 3}, + }, + }, + zcl.TraverseIndex{ + Key: cty.NumberIntVal(1), + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 4, Byte: 3}, + End: zcl.Pos{Line: 1, Column: 7, Byte: 6}, + }, + }, + zcl.TraverseIndex{ + Key: cty.NumberIntVal(2), + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 7, Byte: 6}, + End: zcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + 0, + }, + { + "foo[1].bar", + zcl.Traversal{ + zcl.TraverseRoot{ + Name: "foo", + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: zcl.Pos{Line: 1, Column: 4, Byte: 3}, + }, + }, + zcl.TraverseIndex{ + Key: cty.NumberIntVal(1), + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 4, Byte: 3}, + End: zcl.Pos{Line: 1, Column: 7, Byte: 6}, + }, + }, + zcl.TraverseAttr{ + Name: "bar", + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 7, Byte: 6}, + End: zcl.Pos{Line: 1, Column: 11, Byte: 10}, + }, + }, + }, + 0, + }, + { + "foo.", + zcl.Traversal{ + zcl.TraverseRoot{ + Name: "foo", + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: zcl.Pos{Line: 1, Column: 4, Byte: 3}, + }, + }, + }, + 1, // attribute name required + }, + { + "foo[", + zcl.Traversal{ + zcl.TraverseRoot{ + Name: "foo", + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: zcl.Pos{Line: 1, Column: 4, Byte: 3}, + }, + }, + }, + 1, // index required + }, + { + "foo[index]", + zcl.Traversal{ + zcl.TraverseRoot{ + Name: "foo", + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: zcl.Pos{Line: 1, Column: 4, Byte: 3}, + }, + }, + }, + 1, // index must be literal + }, + { + "foo[0", + zcl.Traversal{ + zcl.TraverseRoot{ + Name: "foo", + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: zcl.Pos{Line: 1, Column: 4, Byte: 3}, + }, + }, + zcl.TraverseIndex{ + Key: cty.NumberIntVal(0), + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 4, Byte: 3}, + End: zcl.Pos{Line: 1, Column: 6, Byte: 5}, + }, + }, + }, + 1, // missing close bracket + }, + { + "foo 0", + zcl.Traversal{ + zcl.TraverseRoot{ + Name: "foo", + SrcRange: zcl.Range{ + Start: zcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: zcl.Pos{Line: 1, Column: 4, Byte: 3}, + }, + }, + }, + 1, // extra junk after traversal + }, + } + + for _, test := range tests { + t.Run(test.src, func(t *testing.T) { + got, diags := ParseTraversalAbs([]byte(test.src), "", zcl.Pos{Line: 1, Column: 1}) + if len(diags) != test.diagCount { + for _, diag := range diags { + t.Logf(" - %s", diag.Error()) + } + t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.diagCount) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("wrong result\nsrc: %s\ngot: %s\nwant: %s", test.src, spew.Sdump(got), spew.Sdump(test.want)) + } + }) + } +} diff --git a/zcl/zclsyntax/parser.go b/zcl/zclsyntax/parser.go index c604d15..d44e5a4 100644 --- a/zcl/zclsyntax/parser.go +++ b/zcl/zclsyntax/parser.go @@ -695,32 +695,11 @@ func (p *parser) parseExpressionTerm() (Expression, zcl.Diagnostics) { case TokenNumberLit: tok := p.Read() // eat number token - // We'll lean on the cty converter to do the conversion, to ensure that - // the behavior is the same as what would happen if converting a - // non-literal string to a number. - numStrVal := cty.StringVal(string(tok.Bytes)) - numVal, err := convert.Convert(numStrVal, cty.Number) - if err != nil { - ret := &LiteralValueExpr{ - Val: cty.UnknownVal(cty.Number), - SrcRange: tok.Range, - } - return ret, zcl.Diagnostics{ - { - Severity: zcl.DiagError, - Summary: "Invalid number literal", - // FIXME: not a very good error message, but convert only - // gives us "a number is required", so not much help either. - Detail: "Failed to recognize the value of this number literal.", - Subject: &ret.SrcRange, - }, - } - } - + numVal, diags := p.numberLitValue(tok) return &LiteralValueExpr{ Val: numVal, SrcRange: tok.Range, - }, nil + }, diags case TokenIdent: tok := p.Read() // eat identifier token @@ -838,6 +817,28 @@ func (p *parser) parseExpressionTerm() (Expression, zcl.Diagnostics) { } } +func (p *parser) numberLitValue(tok Token) (cty.Value, zcl.Diagnostics) { + // We'll lean on the cty converter to do the conversion, to ensure that + // the behavior is the same as what would happen if converting a + // non-literal string to a number. + numStrVal := cty.StringVal(string(tok.Bytes)) + numVal, err := convert.Convert(numStrVal, cty.Number) + if err != nil { + ret := cty.UnknownVal(cty.Number) + return ret, zcl.Diagnostics{ + { + Severity: zcl.DiagError, + Summary: "Invalid number literal", + // FIXME: not a very good error message, but convert only + // gives us "a number is required", so not much help either. + Detail: "Failed to recognize the value of this number literal.", + Subject: &tok.Range, + }, + } + } + return numVal, nil +} + // finishParsingFunctionCall parses a function call assuming that the function // name was already read, and so the peeker should be pointing at the opening // parenthesis after the name. diff --git a/zcl/zclsyntax/parser_traversal.go b/zcl/zclsyntax/parser_traversal.go new file mode 100644 index 0000000..27d5702 --- /dev/null +++ b/zcl/zclsyntax/parser_traversal.go @@ -0,0 +1,159 @@ +package zclsyntax + +import ( + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-zcl/zcl" +) + +// ParseTraversalAbs parses an absolute traversal that is assumed to consume +// all of the remaining tokens in the peeker. The usual parser recovery +// behavior is not supported here because traversals are not expected to +// be parsed as part of a larger program. +func (p *parser) ParseTraversalAbs() (zcl.Traversal, zcl.Diagnostics) { + var ret zcl.Traversal + var diags zcl.Diagnostics + + // Absolute traversal must always begin with a variable name + varTok := p.Read() + if varTok.Type != TokenIdent { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Variable name required", + Detail: "Must begin with a variable name.", + Subject: &varTok.Range, + }) + return ret, diags + } + + varName := string(varTok.Bytes) + ret = append(ret, zcl.TraverseRoot{ + Name: varName, + SrcRange: varTok.Range, + }) + + for { + next := p.Peek() + + if next.Type == TokenEOF { + return ret, diags + } + + switch next.Type { + case TokenDot: + // Attribute access + dot := p.Read() // eat dot + nameTok := p.Read() + if nameTok.Type != TokenIdent { + if nameTok.Type == TokenStar { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Attribute name required", + Detail: "Splat expressions (.*) may not be used here.", + Subject: &nameTok.Range, + Context: zcl.RangeBetween(varTok.Range, nameTok.Range).Ptr(), + }) + } else { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Attribute name required", + Detail: "Dot must be followed by attribute name.", + Subject: &nameTok.Range, + Context: zcl.RangeBetween(varTok.Range, nameTok.Range).Ptr(), + }) + } + return ret, diags + } + + attrName := string(nameTok.Bytes) + ret = append(ret, zcl.TraverseAttr{ + Name: attrName, + SrcRange: zcl.RangeBetween(dot.Range, nameTok.Range), + }) + case TokenOBrack: + // Index + open := p.Read() // eat open bracket + next := p.Peek() + + switch next.Type { + case TokenNumberLit: + tok := p.Read() // eat number + numVal, numDiags := p.numberLitValue(tok) + diags = append(diags, numDiags...) + + close := p.Read() + if close.Type != TokenCBrack { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Unclosed index brackets", + Detail: "Index key must be followed by a closing bracket.", + Subject: &close.Range, + Context: zcl.RangeBetween(open.Range, close.Range).Ptr(), + }) + } + + ret = append(ret, zcl.TraverseIndex{ + Key: numVal, + SrcRange: zcl.RangeBetween(open.Range, close.Range), + }) + + if diags.HasErrors() { + return ret, diags + } + + case TokenOQuote: + str, _, strDiags := p.parseQuotedStringLiteral() + diags = append(diags, strDiags...) + + close := p.Read() + if close.Type != TokenCBrack { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Unclosed index brackets", + Detail: "Index key must be followed by a closing bracket.", + Subject: &close.Range, + Context: zcl.RangeBetween(open.Range, close.Range).Ptr(), + }) + } + + ret = append(ret, zcl.TraverseIndex{ + Key: cty.StringVal(str), + SrcRange: zcl.RangeBetween(open.Range, close.Range), + }) + + if diags.HasErrors() { + return ret, diags + } + + default: + if next.Type == TokenStar { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Attribute name required", + Detail: "Splat expressions ([*]) may not be used here.", + Subject: &next.Range, + Context: zcl.RangeBetween(varTok.Range, next.Range).Ptr(), + }) + } else { + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Index value required", + Detail: "Index brackets must contain either a literal number or a literal string.", + Subject: &next.Range, + Context: zcl.RangeBetween(varTok.Range, next.Range).Ptr(), + }) + } + return ret, diags + } + + default: + diags = append(diags, &zcl.Diagnostic{ + Severity: zcl.DiagError, + Summary: "Invalid character", + Detail: "Expected an attribute access or an index operator.", + Subject: &next.Range, + Context: zcl.RangeBetween(varTok.Range, next.Range).Ptr(), + }) + return ret, diags + } + } +} diff --git a/zcl/zclsyntax/public.go b/zcl/zclsyntax/public.go index e6c665b..c87f346 100644 --- a/zcl/zclsyntax/public.go +++ b/zcl/zclsyntax/public.go @@ -68,6 +68,26 @@ func ParseTemplate(src []byte, filename string, start zcl.Pos) (Expression, zcl. return expr, diags } +// ParseTraversalAbs parses the given buffer as a standalone absolute traversal. +// +// Parsing as a traversal is more limited than parsing as an expession since +// it allows only attribute and indexing operations on variables. Traverals +// are useful as a syntax for referring to objects without necessarily +// evaluating them. +func ParseTraversalAbs(src []byte, filename string, start zcl.Pos) (zcl.Traversal, zcl.Diagnostics) { + tokens, diags := LexExpression(src, filename, start) + peeker := newPeeker(tokens, false) + parser := &parser{peeker: peeker} + + // Bare traverals are always parsed in "ignore newlines" mode, as if + // they were wrapped in parentheses. + parser.PushIncludeNewlines(false) + + expr, parseDiags := parser.ParseTraversalAbs() + diags = append(diags, parseDiags...) + return expr, diags +} + // LexConfig performs lexical analysis on the given buffer, treating it as a // whole zcl config file, and returns the resulting tokens. //