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:
Martin Atkins 2019-01-03 08:44:27 -08:00
parent 6631d7cd0a
commit 2b184cace4
3 changed files with 55 additions and 24 deletions

View File

@ -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()
}

View File

@ -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,

View File

@ -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