Skip to content

Commit

Permalink
Add additional function for parsing traversals with [*] keys (#673)
Browse files Browse the repository at this point in the history
* Add additional function for parsing traversals with [*] keys

* add more context around skipped test cases
  • Loading branch information
liamcervante committed Apr 22, 2024
1 parent 303be61 commit f7cd61a
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 2 deletions.
81 changes: 80 additions & 1 deletion hclsyntax/parse_traversal_test.go
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
}
})
}
}
51 changes: 50 additions & 1 deletion hclsyntax/parser_traversal.go
Expand Up @@ -4,15 +4,36 @@
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
// 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() (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

Expand Down Expand Up @@ -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{
Expand Down
31 changes: 31 additions & 0 deletions hclsyntax/public.go
Expand Up @@ -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.
//
Expand Down

0 comments on commit f7cd61a

Please sign in to comment.