diff --git a/hclsyntax/expression.go b/hclsyntax/expression.go index f4c3a6d7..55a43f6d 100644 --- a/hclsyntax/expression.go +++ b/hclsyntax/expression.go @@ -1938,6 +1938,22 @@ func (e *SplatExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { diags = append(diags, tyDiags...) return cty.ListValEmpty(ty.ElementType()).WithMarks(marks), diags } + // Unfortunately it's possible for a nested splat on scalar values to + // generate non-homogenously-typed vals, and we discovered this bad + // interaction after the two conflicting behaviors were both + // well-established so it isn't clear how to change them without + // breaking existing code. Therefore we just make that an error for + // now, to avoid crashing trying to constuct an impossible list. + if !cty.CanListVal(vals) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid nested splat expressions", + 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.", + Subject: e.Each.Range().Ptr(), + Context: e.Range().Ptr(), // encourage a diagnostic renderer to also include the "source" part of the expression in its code snippet + }) + return cty.DynamicVal, diags + } return cty.ListVal(vals).WithMarks(marks), diags default: return cty.TupleVal(vals).WithMarks(marks), diags diff --git a/hclsyntax/expression_test.go b/hclsyntax/expression_test.go index 843847df..c2132c43 100644 --- a/hclsyntax/expression_test.go +++ b/hclsyntax/expression_test.go @@ -1387,6 +1387,54 @@ upper( cty.DynamicVal, 1, // splat cannot be applied to null sequence }, + { + `listofobj[*].scalar[*]`, + &hcl.EvalContext{ + Variables: map[string]cty.Value{ + "listofobj": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "scalar": cty.StringVal("foo"), + }), + cty.ObjectVal(map[string]cty.Value{ + "scalar": cty.StringVal("bar"), + }), + }), + }, + }, + cty.ListVal([]cty.Value{ + // The second-level splat promotes the scalars to single-element tuples. + cty.TupleVal([]cty.Value{cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("bar")}), + }), + 0, + }, + { + // This is a particularly tricky case where two splat rules interact in + // a sub-optimal way: + // 1. The top-level splat is applied to a list and so it wants to return a list. + // 2. The nested splat is applied to a scalar, and so it wants to return different tuple types depending on the nullness. + // Rule 2 breaks rule 1, because we can't make a list with elements of different types. + // For now we're treating this as an error because we didn't learn of this bad + // interaction until long after both of these rules were in separate wide use, + // and so it isn't clear how to make this work without potentially breaking other + // behavior. Perhaps this can become valid in future if we find a viable way to + // do it. + `listofobj[*].scalar[*]`, + &hcl.EvalContext{ + Variables: map[string]cty.Value{ + "listofobj": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "scalar": cty.NullVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "scalar": cty.StringVal("bar"), + }), + }), + }, + }, + cty.DynamicVal, + 1, // nested splat produces non-homogenously-typed results in this case, so cannot produce a valid list + }, { `["hello", "goodbye"].*`, nil,