hclsyntax: Splat expression on non-sequence null gives empty tuple
This allows using a splat expression to conveniently coerce a possibly-null scalar into a zero- or one-item tuple, which is helpful because in HCL we prefer "for each item in sequence" operations over pure conditionals in many situations just because they compose better in our declarative language. For example, in a language that uses the "dynblock" extension we can turn a possibly-null object into zero or one blocks using its for_each argument with a splat operation: dynamic "thingy" { for_each = maybe_null.* content { name = thingy.value.name } } This fixes #66.
This commit is contained in:
parent
6631d7cd0a
commit
2b184cace4
@ -1235,19 +1235,6 @@ func (e *SplatExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
||||
return cty.DynamicVal, diags
|
||||
}
|
||||
|
||||
if sourceVal.IsNull() {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Splat of null value",
|
||||
Detail: "Splat expressions (with the * symbol) cannot be applied to null values.",
|
||||
Subject: e.Source.Range().Ptr(),
|
||||
Context: hcl.RangeBetween(e.Source.Range(), e.MarkerRange).Ptr(),
|
||||
Expression: e.Source,
|
||||
EvalContext: ctx,
|
||||
})
|
||||
return cty.DynamicVal, diags
|
||||
}
|
||||
|
||||
sourceTy := sourceVal.Type()
|
||||
if sourceTy == cty.DynamicPseudoType {
|
||||
// If we don't even know the _type_ of our source value yet then
|
||||
@ -1258,9 +1245,27 @@ func (e *SplatExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
||||
|
||||
// A "special power" of splat expressions is that they can be applied
|
||||
// both to tuples/lists and to other values, and in the latter case
|
||||
// the value will be treated as an implicit single-value tuple. We'll
|
||||
// deal with that here first.
|
||||
if !(sourceTy.IsTupleType() || sourceTy.IsListType() || sourceTy.IsSetType()) {
|
||||
// the value will be treated as an implicit single-item tuple, or as
|
||||
// an empty tuple if the value is null.
|
||||
autoUpgrade := !(sourceTy.IsTupleType() || sourceTy.IsListType() || sourceTy.IsSetType())
|
||||
|
||||
if sourceVal.IsNull() {
|
||||
if autoUpgrade {
|
||||
return cty.EmptyTupleVal, diags
|
||||
}
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Splat of null value",
|
||||
Detail: "Splat expressions (with the * symbol) cannot be applied to null sequences.",
|
||||
Subject: e.Source.Range().Ptr(),
|
||||
Context: hcl.RangeBetween(e.Source.Range(), e.MarkerRange).Ptr(),
|
||||
Expression: e.Source,
|
||||
EvalContext: ctx,
|
||||
})
|
||||
return cty.DynamicVal, diags
|
||||
}
|
||||
|
||||
if autoUpgrade {
|
||||
sourceVal = cty.TupleVal([]cty.Value{sourceVal})
|
||||
sourceTy = sourceVal.Type()
|
||||
}
|
||||
|
@ -870,6 +870,30 @@ upper(
|
||||
cty.UnknownVal(cty.Tuple([]cty.Type{cty.String, cty.Bool})),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`nullobj.*.name`,
|
||||
&hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"nullobj": cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"name": cty.String,
|
||||
})),
|
||||
},
|
||||
},
|
||||
cty.TupleVal([]cty.Value{}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`nulllist.*.name`,
|
||||
&hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"nulllist": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
|
||||
"name": cty.String,
|
||||
}))),
|
||||
},
|
||||
},
|
||||
cty.DynamicVal,
|
||||
1, // splat cannot be applied to null sequence
|
||||
},
|
||||
{
|
||||
`["hello", "goodbye"].*`,
|
||||
nil,
|
||||
|
@ -567,10 +567,10 @@ elements in a tuple, list, or set value.
|
||||
|
||||
There are two kinds of "splat" operator:
|
||||
|
||||
* The _attribute-only_ splat operator supports only attribute lookups into
|
||||
- The _attribute-only_ splat operator supports only attribute lookups into
|
||||
the elements from a list, but supports an arbitrary number of them.
|
||||
|
||||
* The _full_ splat operator additionally supports indexing into the elements
|
||||
- The _full_ splat operator additionally supports indexing into the elements
|
||||
from a list, and allows any combination of attribute access and index
|
||||
operations.
|
||||
|
||||
@ -583,9 +583,9 @@ fullSplat = "[" "*" "]" (GetAttr | Index)*;
|
||||
The splat operators can be thought of as shorthands for common operations that
|
||||
could otherwise be performed using _for expressions_:
|
||||
|
||||
* `tuple.*.foo.bar[0]` is approximately equivalent to
|
||||
- `tuple.*.foo.bar[0]` is approximately equivalent to
|
||||
`[for v in tuple: v.foo.bar][0]`.
|
||||
* `tuple[*].foo.bar[0]` is approximately equivalent to
|
||||
- `tuple[*].foo.bar[0]` is approximately equivalent to
|
||||
`[for v in tuple: v.foo.bar[0]]`
|
||||
|
||||
Note the difference in how the trailing index operator is interpreted in
|
||||
@ -597,13 +597,15 @@ _for expressions_ shown above: if a splat operator is applied to a value that
|
||||
is _not_ of tuple, list, or set type, the value is coerced automatically into
|
||||
a single-value list of the value type:
|
||||
|
||||
* `any_object.*.id` is equivalent to `[any_object.id]`, assuming that `any_object`
|
||||
- `any_object.*.id` is equivalent to `[any_object.id]`, assuming that `any_object`
|
||||
is a single object.
|
||||
* `any_number.*` is equivalent to `[any_number]`, assuming that `any_number`
|
||||
- `any_number.*` is equivalent to `[any_number]`, assuming that `any_number`
|
||||
is a single number.
|
||||
|
||||
If the left operand of a splat operator is an unknown value of any type, the
|
||||
result is a value of the dynamic pseudo-type.
|
||||
If applied to a null value that is not tuple, list, or set, the result is always
|
||||
an empty tuple, which allows conveniently converting a possibly-null scalar
|
||||
value into a tuple of zero or one elements. It is illegal to apply a splat
|
||||
operator to a null value of tuple, list, or set type.
|
||||
|
||||
### Operations
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user