Skip to content

Commit 2894e4c

Browse files
hclsyntax: Detect and reject invalid nested splat result (#724)
1 parent 923b06b commit 2894e4c

File tree

2 files changed

+64
-0
lines changed

2 files changed

+64
-0
lines changed

hclsyntax/expression.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1938,6 +1938,22 @@ func (e *SplatExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
19381938
diags = append(diags, tyDiags...)
19391939
return cty.ListValEmpty(ty.ElementType()).WithMarks(marks), diags
19401940
}
1941+
// Unfortunately it's possible for a nested splat on scalar values to
1942+
// generate non-homogenously-typed vals, and we discovered this bad
1943+
// interaction after the two conflicting behaviors were both
1944+
// well-established so it isn't clear how to change them without
1945+
// breaking existing code. Therefore we just make that an error for
1946+
// now, to avoid crashing trying to constuct an impossible list.
1947+
if !cty.CanListVal(vals) {
1948+
diags = append(diags, &hcl.Diagnostic{
1949+
Severity: hcl.DiagError,
1950+
Summary: "Invalid nested splat expressions",
1951+
Detail: "The second level of splat expression produced elements of different types, so it isn't possible to construct a valid list to represent the top-level result.\n\nConsider using a for expression instead, to produce a tuple-typed result which can therefore have non-homogenous element types.",
1952+
Subject: e.Each.Range().Ptr(),
1953+
Context: e.Range().Ptr(), // encourage a diagnostic renderer to also include the "source" part of the expression in its code snippet
1954+
})
1955+
return cty.DynamicVal, diags
1956+
}
19411957
return cty.ListVal(vals).WithMarks(marks), diags
19421958
default:
19431959
return cty.TupleVal(vals).WithMarks(marks), diags

hclsyntax/expression_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,6 +1387,54 @@ upper(
13871387
cty.DynamicVal,
13881388
1, // splat cannot be applied to null sequence
13891389
},
1390+
{
1391+
`listofobj[*].scalar[*]`,
1392+
&hcl.EvalContext{
1393+
Variables: map[string]cty.Value{
1394+
"listofobj": cty.ListVal([]cty.Value{
1395+
cty.ObjectVal(map[string]cty.Value{
1396+
"scalar": cty.StringVal("foo"),
1397+
}),
1398+
cty.ObjectVal(map[string]cty.Value{
1399+
"scalar": cty.StringVal("bar"),
1400+
}),
1401+
}),
1402+
},
1403+
},
1404+
cty.ListVal([]cty.Value{
1405+
// The second-level splat promotes the scalars to single-element tuples.
1406+
cty.TupleVal([]cty.Value{cty.StringVal("foo")}),
1407+
cty.TupleVal([]cty.Value{cty.StringVal("bar")}),
1408+
}),
1409+
0,
1410+
},
1411+
{
1412+
// This is a particularly tricky case where two splat rules interact in
1413+
// a sub-optimal way:
1414+
// 1. The top-level splat is applied to a list and so it wants to return a list.
1415+
// 2. The nested splat is applied to a scalar, and so it wants to return different tuple types depending on the nullness.
1416+
// Rule 2 breaks rule 1, because we can't make a list with elements of different types.
1417+
// For now we're treating this as an error because we didn't learn of this bad
1418+
// interaction until long after both of these rules were in separate wide use,
1419+
// and so it isn't clear how to make this work without potentially breaking other
1420+
// behavior. Perhaps this can become valid in future if we find a viable way to
1421+
// do it.
1422+
`listofobj[*].scalar[*]`,
1423+
&hcl.EvalContext{
1424+
Variables: map[string]cty.Value{
1425+
"listofobj": cty.ListVal([]cty.Value{
1426+
cty.ObjectVal(map[string]cty.Value{
1427+
"scalar": cty.NullVal(cty.String),
1428+
}),
1429+
cty.ObjectVal(map[string]cty.Value{
1430+
"scalar": cty.StringVal("bar"),
1431+
}),
1432+
}),
1433+
},
1434+
},
1435+
cty.DynamicVal,
1436+
1, // nested splat produces non-homogenously-typed results in this case, so cannot produce a valid list
1437+
},
13901438
{
13911439
`["hello", "goodbye"].*`,
13921440
nil,

0 commit comments

Comments
 (0)