diff --git a/hclsyntax/parse_traversal_test.go b/hclsyntax/parse_traversal_test.go index 3ca5fc2b..5d8d24ea 100644 --- a/hclsyntax/parse_traversal_test.go +++ b/hclsyntax/parse_traversal_test.go @@ -4,11 +4,13 @@ package hclsyntax import ( + "fmt" "testing" "github.com/go-test/deep" - "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/hcl/v2" ) func TestParseTraversalAbs(t *testing.T) { @@ -208,10 +210,63 @@ func TestParseTraversalAbs(t *testing.T) { }, 1, // extra junk after traversal }, + + { + "foo[*]", + hcl.Traversal{ + hcl.TraverseRoot{ + Name: "foo", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 4, Byte: 3}, + }, + }, + hcl.TraverseSplat{ + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 4, Byte: 3}, + End: hcl.Pos{Line: 1, Column: 7, Byte: 6}, + }, + }, + }, + 0, + }, + { + "foo.*", // Still not supporting this. + hcl.Traversal{ + hcl.TraverseRoot{ + Name: "foo", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 4, Byte: 3}, + }, + }, + }, + 1, + }, + { + "foo[*].bar", // Run this through the unsupported function. + hcl.Traversal{ + hcl.TraverseRoot{ + Name: "foo", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 4, Byte: 3}, + }, + }, + }, + 1, + }, } for _, test := range tests { t.Run(test.src, func(t *testing.T) { + if test.src == "foo[*]" { + // The foo[*] test will fail because the function we test in + // this branch does not support the splat syntax. So we will + // skip this test case here. + t.Skip("skipping test for unsupported splat syntax") + } + got, diags := ParseTraversalAbs([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1}) if len(diags) != test.diagCount { for _, diag := range diags { @@ -226,5 +281,29 @@ func TestParseTraversalAbs(t *testing.T) { } } }) + + t.Run(fmt.Sprintf("partial_%s", test.src), func(t *testing.T) { + if test.src == "foo[*].bar" { + // The foo[*].bar test will fail because the function we test in + // this branch does support the splat syntax and this test is + // designed to make sure that the other branch still fails with + // the splat syntax. So we will skip this test case here. + t.Skip("skipping test that fails for splat syntax") + } + + got, diags := ParseTraversalPartial([]byte(test.src), "", hcl.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 diff := deep.Equal(got, test.want); diff != nil { + for _, problem := range diff { + t.Error(problem) + } + } + }) } } diff --git a/hclsyntax/parser_traversal.go b/hclsyntax/parser_traversal.go index 3afa6ab0..f7d4062f 100644 --- a/hclsyntax/parser_traversal.go +++ b/hclsyntax/parser_traversal.go @@ -4,8 +4,9 @@ package hclsyntax import ( - "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/hcl/v2" ) // ParseTraversalAbs parses an absolute traversal that is assumed to consume @@ -13,6 +14,26 @@ import ( // behavior is not supported here because traversals are not expected to // be parsed as part of a larger program. func (p *parser) ParseTraversalAbs() (hcl.Traversal, hcl.Diagnostics) { + return p.parseTraversal(false) +} + +// ParseTraversalPartial parses an absolute traversal that is permitted +// to contain splat ([*]) expressions. Only splat expressions within square +// brackets are permitted ([*]); splat expressions within attribute names are +// not permitted (.*). +// +// The meaning of partial here is that the traversal may be incomplete, in that +// any splat expression indicates reference to a potentially unknown number of +// elements. +// +// Traversals that include splats cannot be automatically traversed by HCL using +// the TraversalAbs or TraversalRel methods. Instead, the caller must handle +// the traversals manually. +func (p *parser) ParseTraversalPartial() (hcl.Traversal, hcl.Diagnostics) { + return p.parseTraversal(true) +} + +func (p *parser) parseTraversal(allowSplats bool) (hcl.Traversal, hcl.Diagnostics) { var ret hcl.Traversal var diags hcl.Diagnostics @@ -127,6 +148,34 @@ func (p *parser) ParseTraversalAbs() (hcl.Traversal, hcl.Diagnostics) { return ret, diags } + case TokenStar: + if allowSplats { + + p.Read() // Eat the star. + close := p.Read() + if close.Type != TokenCBrack { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unclosed index brackets", + Detail: "Index key must be followed by a closing bracket.", + Subject: &close.Range, + Context: hcl.RangeBetween(open.Range, close.Range).Ptr(), + }) + } + + ret = append(ret, hcl.TraverseSplat{ + SrcRange: hcl.RangeBetween(open.Range, close.Range), + }) + + if diags.HasErrors() { + return ret, diags + } + + continue + } + + // Otherwise, return the error below for the star. + fallthrough default: if next.Type == TokenStar { diags = append(diags, &hcl.Diagnostic{ diff --git a/hclsyntax/public.go b/hclsyntax/public.go index d56f8e50..17dc1ed4 100644 --- a/hclsyntax/public.go +++ b/hclsyntax/public.go @@ -118,6 +118,37 @@ func ParseTraversalAbs(src []byte, filename string, start hcl.Pos) (hcl.Traversa return expr, diags } +// ParseTraversalPartial matches the behavior of ParseTraversalAbs except +// that it allows splat expressions ([*]) to appear in the traversal. +// +// The returned traversals are "partial" in that the splat expression indicates +// an unknown value for the index. +// +// Traversals that include splats cannot be automatically traversed by HCL using +// the TraversalAbs or TraversalRel methods. Instead, the caller must handle +// the traversals manually. +func ParseTraversalPartial(src []byte, filename string, start hcl.Pos) (hcl.Traversal, hcl.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.ParseTraversalPartial() + diags = append(diags, parseDiags...) + + parser.PopIncludeNewlines() + + // Panic if the parser uses incorrect stack discipline with the peeker's + // newlines stack, since otherwise it will produce confusing downstream + // errors. + peeker.AssertEmptyIncludeNewlinesStack() + + return expr, diags +} + // LexConfig performs lexical analysis on the given buffer, treating it as a // whole HCL config file, and returns the resulting tokens. //