Skip to content
/ go-cty Public

Commit 77d87c2

Browse files
stdlib: no MergeFunc crash with null values of object type
This was failing due to the special case of returning the type of the first argument when all arguments have the same type. This function treats a null value of any object type as if it were a non-null value of the empty object type, but the Type function wasn't respecting that special case and so it was proposing a different return type than the Impl function actually produces.
1 parent 6854da9 commit 77d87c2

3 files changed

Lines changed: 58 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# 1.18.1 (Unreleased)
22

33
- stdlib: `ContainsFunc` now allows its second argument to be null, to test whether the given collection contains any null elements.
4+
- stdlib: `MergeFunc` no longer panics if all of its arguments are null values of the same object type with at least one attribute.
45

56
# 1.18.0 (February 23, 2026)
67

cty/function/stdlib/collection.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package stdlib
33
import (
44
"errors"
55
"fmt"
6+
"maps"
67
"sort"
78

89
"github.com/zclconf/go-cty/cty"
@@ -794,10 +795,13 @@ var MergeFunc = function.New(&function.Spec{
794795
arg, _ = arg.Unmark()
795796

796797
switch {
797-
case ty.IsObjectType() && !arg.IsNull():
798-
for attr, aty := range ty.AttributeTypes() {
799-
attrs[attr] = aty
798+
case ty.IsObjectType():
799+
if arg.IsNull() {
800+
// Null objects are treated by this function as if they have
801+
// no attributes at all.
802+
ty = cty.EmptyObject
800803
}
804+
maps.Copy(attrs, ty.AttributeTypes())
801805
case ty.IsMapType():
802806
switch {
803807
case arg.IsNull():
@@ -817,11 +821,11 @@ var MergeFunc = function.New(&function.Spec{
817821

818822
// record the first argument type for comparison
819823
if i == 0 {
820-
first = arg.Type()
824+
first = ty
821825
continue
822826
}
823827

824-
if !ty.Equals(first) && matching {
828+
if matching && !ty.Equals(first) {
825829
matching = false
826830
}
827831
}

cty/function/stdlib/collection_test.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,54 @@ func TestMerge(t *testing.T) {
529529
cty.EmptyObjectVal,
530530
false,
531531
},
532-
{ // single null input
532+
{ // single null object with attributes
533+
[]cty.Value{
534+
cty.NullVal(cty.Object(map[string]cty.Type{
535+
"a": cty.String,
536+
})),
537+
},
538+
cty.EmptyObjectVal,
539+
false,
540+
},
541+
{ // multible null objects with the same attributes
542+
[]cty.Value{
543+
cty.NullVal(cty.Object(map[string]cty.Type{
544+
"a": cty.String,
545+
})),
546+
cty.NullVal(cty.Object(map[string]cty.Type{
547+
"a": cty.String,
548+
})),
549+
},
550+
cty.EmptyObjectVal,
551+
false,
552+
},
553+
{ // multible null objects with the differing attributes
554+
[]cty.Value{
555+
cty.NullVal(cty.Object(map[string]cty.Type{
556+
"a": cty.String,
557+
})),
558+
cty.NullVal(cty.Object(map[string]cty.Type{
559+
"b": cty.String,
560+
})),
561+
},
562+
cty.EmptyObjectVal,
563+
false,
564+
},
565+
{ // mixture of null and non-null objects of the same type
566+
[]cty.Value{
567+
cty.NullVal(cty.Object(map[string]cty.Type{
568+
"a": cty.String,
569+
})),
570+
cty.ObjectVal(map[string]cty.Value{
571+
"a": cty.StringVal("a value"),
572+
}),
573+
},
574+
cty.ObjectVal(map[string]cty.Value{
575+
"a": cty.StringVal("a value"),
576+
}),
577+
false,
578+
},
579+
{ // single empty map
533580
[]cty.Value{
534581
cty.MapValEmpty(cty.String),
535582
},

0 commit comments

Comments
 (0)