Repository: opentofu/opentofu
File: internal/lang/funcs/ephemeral.go (Go)

Code before:

import (
	"github.com/opentofu/opentofu/internal/lang/marks"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/function"
)

var EphemeralAsNullFunc = function.New(&function.Spec{
	Params: []function.Parameter{
		{
			Name:             "value",
			Type:             cty.DynamicPseudoType,
			AllowUnknown:     true,
			AllowNull:        true,
			AllowMarked:      true,
			AllowDynamicType: true,
		},
	},
	Type: func(args []cty.Value) (cty.Type, error) {
		// The result type is always the same as the argument type.
		return args[0].Type(), nil
	},
	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
		var ephemeralAsNull func(cty.Value) cty.Value
		// Inspired heavily by https://github.com/zclconf/go-cty/blob/v1.16.3/cty/unknown_as_null.go#L10
		ephemeralAsNull = func(val cty.Value) cty.Value {
			ty := val.Type()

			// Preserve non-ephemeral marks
			nonEphemeralMarks := val.Marks()
			delete(nonEphemeralMarks, marks.Ephemeral)

			switch {
			case val.IsNull():
				return cty.NullVal(ty).WithMarks(nonEphemeralMarks)
			case !val.IsKnown():
				// This mirrors the logic in IsSensitive()
				//
				// When a value is unknown its ephemerality is also not yet
				// finalized, because authors can write expressions where the
				// ephemerality of the result is decided based on some other
				// value that isn't yet known itself. As a simple example:
				//    var.unknown_bool ? var.some_ephemeral_value : "b"
				//
				// Therefore we must report that we can't predict whether an
				// unknown value will be ephemeral or not. For more information,
				// refer to https://github.com/opentofu/opentofu/issues/2415

				return cty.UnknownVal(val.Type()).WithMarks(nonEphemeralMarks)
			case val.HasMark(marks.Ephemeral):
				// This whole value is marked as ephemeral and should be null

Review comment (suggestion):
Although the details are slightly different, it seems like the tree-walking part of this is essentially the same as [`cty.Transform`](https://pkg.go.dev/github.com/zclconf/go-cty@v1.16.3/cty#Transform) so maybe it would work to use that so the implementation here can focus only on the "return null if the value has the ephemeral mark" rule.

(I've not looked closely at whether this function is producing a materially different walk order than `cty.Transform`, though. If there's a good reason for this to be implemented separately then I have no objection, just wanted to make sure you knew about `cty.Transform`.)


Code after:

import (
	"github.com/opentofu/opentofu/internal/lang/marks"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/function"
)

var EphemeralAsNullFunc = function.New(&function.Spec{
	Params: []function.Parameter{
		{
			Name:             "value",
			Type:             cty.DynamicPseudoType,
			AllowUnknown:     true,
			AllowNull:        true,
			AllowMarked:      true,
			AllowDynamicType: true,
		},
	},
	Type: func(args []cty.Value) (cty.Type, error) {
		// The result type is always the same as the argument type.
		return args[0].Type(), nil
	},
	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
		return cty.Transform(args[0], func(_ cty.Path, val cty.Value) (cty.Value, error) {
			ty := val.Type()

			// Preserve non-ephemeral marks
			nonEphemeralMarks := val.Marks()
			delete(nonEphemeralMarks, marks.Ephemeral)

			switch {
			case val.IsNull():
				return cty.NullVal(ty).WithMarks(nonEphemeralMarks), nil
			case !val.IsKnown():
				// This mirrors the logic in IsSensitive()
				//
				// When a value is unknown its ephemerality is also not yet
				// finalized, because authors can write expressions where the
				// ephemerality of the result is decided based on some other
				// value that isn't yet known itself. As a simple example:
				//    var.unknown_bool ? var.some_ephemeral_value : "b"
				//
				// Therefore we must report that we can't predict whether an
				// unknown value will be ephemeral or not. For more information,
				// refer to https://github.com/opentofu/opentofu/issues/2415

				return cty.UnknownVal(val.Type()).WithMarks(nonEphemeralMarks), nil
			case val.HasMark(marks.Ephemeral):
				// This whole value is marked as ephemeral and should be null
				return cty.NullVal(val.Type()).WithMarks(nonEphemeralMarks), nil
			default: