From ea698071da7e3b0ea099f8535d59ccaaf656b788 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 3 Feb 2023 10:39:13 -0800 Subject: [PATCH] ext/typeexpr: Refinements when applying defaults with unknown values If either the given value is refined non-null or if the default value is refined non-null then the final attribute value after defaults processing is also guaranteed non-null even if we don't yet know exactly what the value will be. This rule is pretty marginal on its own, but refining some types of value as non-null creates opportunities to deduce further information when the value is used under other operations later, such as collapsing an unknown but definitely not null list of a known length into a known list of that length containing unknown values. --- ext/typeexpr/defaults.go | 3 ++ ext/typeexpr/defaults_test.go | 68 +++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/ext/typeexpr/defaults.go b/ext/typeexpr/defaults.go index 4aeb1fb9..43a2539d 100644 --- a/ext/typeexpr/defaults.go +++ b/ext/typeexpr/defaults.go @@ -95,6 +95,9 @@ func (d *Defaults) apply(v cty.Value) cty.Value { } values[key] = defaultValue } + if defaultRng := defaultValue.Range(); defaultRng.DefinitelyNotNull() { + values[key] = values[key].RefineNotNull() + } } if v.Type().IsMapType() { diff --git a/ext/typeexpr/defaults_test.go b/ext/typeexpr/defaults_test.go index 8b90e752..5f0588f5 100644 --- a/ext/typeexpr/defaults_test.go +++ b/ext/typeexpr/defaults_test.go @@ -830,6 +830,74 @@ func TestDefaults_Apply(t *testing.T) { }), }), }, + "optional attribute with a default can never be null": { + defaults: &Defaults{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + }, []string{"foo"}), + DefaultValues: map[string]cty.Value{ + "foo": cty.StringVal("bar"), // Important: default is non-null + }, + }, + value: cty.ObjectVal(map[string]cty.Value{ + "foo": cty.UnknownVal(cty.String), // could potentially be null once known + }), + want: cty.ObjectVal(map[string]cty.Value{ + // Because the default isn't null we can guarantee that the + // result cannot be null even if the given value turns out to be. + "foo": cty.UnknownVal(cty.String).RefineNotNull(), + }), + }, + "optional attribute with a null default could be null": { + defaults: &Defaults{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + }, []string{"foo"}), + DefaultValues: map[string]cty.Value{ + "foo": cty.NullVal(cty.String), // Important: default is null + }, + }, + value: cty.ObjectVal(map[string]cty.Value{ + "foo": cty.UnknownVal(cty.String), // could potentially be null once known + }), + want: cty.ObjectVal(map[string]cty.Value{ + // The default value is itself null, so this result is nullable. + "foo": cty.UnknownVal(cty.String), + }), + }, + "optional attribute with no default could be null": { + defaults: &Defaults{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + }, []string{"foo"}), + }, + value: cty.ObjectVal(map[string]cty.Value{ + "foo": cty.UnknownVal(cty.String), // could potentially be null once known + }), + want: cty.ObjectVal(map[string]cty.Value{ + // The default value is itself null, so this result is nullable. + "foo": cty.UnknownVal(cty.String), + }), + }, + "optional attribute with non-null unknown value cannot be null": { + defaults: &Defaults{ + Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ + "foo": cty.String, + }, []string{"foo"}), + DefaultValues: map[string]cty.Value{ + "foo": cty.NullVal(cty.String), // Important: default is null + }, + }, + value: cty.ObjectVal(map[string]cty.Value{ + "foo": cty.UnknownVal(cty.String).RefineNotNull(), + }), + want: cty.ObjectVal(map[string]cty.Value{ + // If the input is guaranteed not null then the default + // value can't possibly be selected, and so the result can + // also not be null. + "foo": cty.UnknownVal(cty.String).RefineNotNull(), + }), + }, } for name, tc := range testCases {