Skip to content

Commit 5fdb81b

Browse files
committed
Add additional function for parsing traversals with [*] keys
1 parent 303be61 commit 5fdb81b

File tree

3 files changed

+156
-2
lines changed

3 files changed

+156
-2
lines changed

hclsyntax/parse_traversal_test.go

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
package hclsyntax
55

66
import (
7+
"fmt"
78
"testing"
89

910
"github.com/go-test/deep"
10-
"github.com/hashicorp/hcl/v2"
1111
"github.com/zclconf/go-cty/cty"
12+
13+
"github.com/hashicorp/hcl/v2"
1214
)
1315

1416
func TestParseTraversalAbs(t *testing.T) {
@@ -208,10 +210,61 @@ func TestParseTraversalAbs(t *testing.T) {
208210
},
209211
1, // extra junk after traversal
210212
},
213+
214+
{
215+
"foo[*]",
216+
hcl.Traversal{
217+
hcl.TraverseRoot{
218+
Name: "foo",
219+
SrcRange: hcl.Range{
220+
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
221+
End: hcl.Pos{Line: 1, Column: 4, Byte: 3},
222+
},
223+
},
224+
hcl.TraverseSplat{
225+
SrcRange: hcl.Range{
226+
Start: hcl.Pos{Line: 1, Column: 4, Byte: 3},
227+
End: hcl.Pos{Line: 1, Column: 7, Byte: 6},
228+
},
229+
},
230+
},
231+
0,
232+
},
233+
{
234+
"foo.*", // Still not supporting this.
235+
hcl.Traversal{
236+
hcl.TraverseRoot{
237+
Name: "foo",
238+
SrcRange: hcl.Range{
239+
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
240+
End: hcl.Pos{Line: 1, Column: 4, Byte: 3},
241+
},
242+
},
243+
},
244+
1,
245+
},
246+
{
247+
"foo[*].bar", // Run this through the unsupported function.
248+
hcl.Traversal{
249+
hcl.TraverseRoot{
250+
Name: "foo",
251+
SrcRange: hcl.Range{
252+
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
253+
End: hcl.Pos{Line: 1, Column: 4, Byte: 3},
254+
},
255+
},
256+
},
257+
1,
258+
},
211259
}
212260

213261
for _, test := range tests {
214262
t.Run(test.src, func(t *testing.T) {
263+
if test.src == "foo[*]" {
264+
// Skip the test that introduces splat syntax.
265+
t.Skip("skipping test for unsupported splat syntax")
266+
}
267+
215268
got, diags := ParseTraversalAbs([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1})
216269
if len(diags) != test.diagCount {
217270
for _, diag := range diags {
@@ -226,5 +279,26 @@ func TestParseTraversalAbs(t *testing.T) {
226279
}
227280
}
228281
})
282+
283+
t.Run(fmt.Sprintf("partial_%s", test.src), func(t *testing.T) {
284+
if test.src == "foo[*].bar" {
285+
// Skip the test that's supposed to fail for splat syntax.
286+
t.Skip("skipping test for unsupported splat syntax")
287+
}
288+
289+
got, diags := ParseTraversalPartial([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1})
290+
if len(diags) != test.diagCount {
291+
for _, diag := range diags {
292+
t.Logf(" - %s", diag.Error())
293+
}
294+
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.diagCount)
295+
}
296+
297+
if diff := deep.Equal(got, test.want); diff != nil {
298+
for _, problem := range diff {
299+
t.Error(problem)
300+
}
301+
}
302+
})
229303
}
230304
}

hclsyntax/parser_traversal.go

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,36 @@
44
package hclsyntax
55

66
import (
7-
"github.com/hashicorp/hcl/v2"
87
"github.com/zclconf/go-cty/cty"
8+
9+
"github.com/hashicorp/hcl/v2"
910
)
1011

1112
// ParseTraversalAbs parses an absolute traversal that is assumed to consume
1213
// all of the remaining tokens in the peeker. The usual parser recovery
1314
// behavior is not supported here because traversals are not expected to
1415
// be parsed as part of a larger program.
1516
func (p *parser) ParseTraversalAbs() (hcl.Traversal, hcl.Diagnostics) {
17+
return p.parseTraversal(false)
18+
}
19+
20+
// ParseTraversalPartial parses an absolute traversal that is permitted
21+
// to contain splat ([*]) expressions. Only splat expressions within square
22+
// brackets are permitted ([*]); splat expressions within attribute names are
23+
// not permitted (.*).
24+
//
25+
// The meaning of partial here is that the traversal may be incomplete, in that
26+
// any splat expression indicates reference to a potentially unknown number of
27+
// elements.
28+
//
29+
// Traversals that include splats cannot be automatically traversed by HCL using
30+
// the TraversalAbs or TraversalRel methods. Instead, the caller must handle
31+
// the traversals manually.
32+
func (p *parser) ParseTraversalPartial() (hcl.Traversal, hcl.Diagnostics) {
33+
return p.parseTraversal(true)
34+
}
35+
36+
func (p *parser) parseTraversal(allowSplats bool) (hcl.Traversal, hcl.Diagnostics) {
1637
var ret hcl.Traversal
1738
var diags hcl.Diagnostics
1839

@@ -127,6 +148,34 @@ func (p *parser) ParseTraversalAbs() (hcl.Traversal, hcl.Diagnostics) {
127148
return ret, diags
128149
}
129150

151+
case TokenStar:
152+
if allowSplats {
153+
154+
p.Read() // Eat the star.
155+
close := p.Read()
156+
if close.Type != TokenCBrack {
157+
diags = append(diags, &hcl.Diagnostic{
158+
Severity: hcl.DiagError,
159+
Summary: "Unclosed index brackets",
160+
Detail: "Index key must be followed by a closing bracket.",
161+
Subject: &close.Range,
162+
Context: hcl.RangeBetween(open.Range, close.Range).Ptr(),
163+
})
164+
}
165+
166+
ret = append(ret, hcl.TraverseSplat{
167+
SrcRange: hcl.RangeBetween(open.Range, close.Range),
168+
})
169+
170+
if diags.HasErrors() {
171+
return ret, diags
172+
}
173+
174+
continue
175+
}
176+
177+
// Otherwise, return the error below for the star.
178+
fallthrough
130179
default:
131180
if next.Type == TokenStar {
132181
diags = append(diags, &hcl.Diagnostic{

hclsyntax/public.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,37 @@ func ParseTraversalAbs(src []byte, filename string, start hcl.Pos) (hcl.Traversa
118118
return expr, diags
119119
}
120120

121+
// ParseTraversalPartial matches the behavior of ParseTraversalAbs except
122+
// that it allows splat expressions ([*]) to appear in the traversal.
123+
//
124+
// The returned traversals are "partial" in that the splat expression indicates
125+
// an unknown value for the index.
126+
//
127+
// Traversals that include splats cannot be automatically traversed by HCL using
128+
// the TraversalAbs or TraversalRel methods. Instead, the caller must handle
129+
// the traversals manually.
130+
func ParseTraversalPartial(src []byte, filename string, start hcl.Pos) (hcl.Traversal, hcl.Diagnostics) {
131+
tokens, diags := LexExpression(src, filename, start)
132+
peeker := newPeeker(tokens, false)
133+
parser := &parser{peeker: peeker}
134+
135+
// Bare traverals are always parsed in "ignore newlines" mode, as if
136+
// they were wrapped in parentheses.
137+
parser.PushIncludeNewlines(false)
138+
139+
expr, parseDiags := parser.ParseTraversalPartial()
140+
diags = append(diags, parseDiags...)
141+
142+
parser.PopIncludeNewlines()
143+
144+
// Panic if the parser uses incorrect stack discipline with the peeker's
145+
// newlines stack, since otherwise it will produce confusing downstream
146+
// errors.
147+
peeker.AssertEmptyIncludeNewlinesStack()
148+
149+
return expr, diags
150+
}
151+
121152
// LexConfig performs lexical analysis on the given buffer, treating it as a
122153
// whole HCL config file, and returns the resulting tokens.
123154
//

0 commit comments

Comments
 (0)