github.com/terraform-linters/tflint@v0.51.2-0.20240520175844-3750771571b6/terraform/lang/funcs/conversion.go (about)

     1  package funcs
     2  
     3  import (
     4  	"strconv"
     5  
     6  	"github.com/terraform-linters/tflint-plugin-sdk/terraform/lang/marks"
     7  	"github.com/zclconf/go-cty/cty"
     8  	"github.com/zclconf/go-cty/cty/convert"
     9  	"github.com/zclconf/go-cty/cty/function"
    10  )
    11  
    12  // MakeToFunc constructs a "to..." function, like "tostring", which converts
    13  // its argument to a specific type or type kind.
    14  //
    15  // The given type wantTy can be any type constraint that cty's "convert" package
    16  // would accept. In particular, this means that you can pass
    17  // cty.List(cty.DynamicPseudoType) to mean "list of any single type", which
    18  // will then cause cty to attempt to unify all of the element types when given
    19  // a tuple.
    20  func MakeToFunc(wantTy cty.Type) function.Function {
    21  	return function.New(&function.Spec{
    22  		Params: []function.Parameter{
    23  			{
    24  				Name: "v",
    25  				// We use DynamicPseudoType rather than wantTy here so that
    26  				// all values will pass through the function API verbatim and
    27  				// we can handle the conversion logic within the Type and
    28  				// Impl functions. This allows us to customize the error
    29  				// messages to be more appropriate for an explicit type
    30  				// conversion, whereas the cty function system produces
    31  				// messages aimed at _implicit_ type conversions.
    32  				Type:             cty.DynamicPseudoType,
    33  				AllowNull:        true,
    34  				AllowMarked:      true,
    35  				AllowDynamicType: true,
    36  			},
    37  		},
    38  		Type: func(args []cty.Value) (cty.Type, error) {
    39  			gotTy := args[0].Type()
    40  			if gotTy.Equals(wantTy) {
    41  				return wantTy, nil
    42  			}
    43  			conv := convert.GetConversionUnsafe(args[0].Type(), wantTy)
    44  			if conv == nil {
    45  				// We'll use some specialized errors for some trickier cases,
    46  				// but most we can handle in a simple way.
    47  				switch {
    48  				case gotTy.IsTupleType() && wantTy.IsTupleType():
    49  					return cty.NilType, function.NewArgErrorf(0, "incompatible tuple type for conversion: %s", convert.MismatchMessage(gotTy, wantTy))
    50  				case gotTy.IsObjectType() && wantTy.IsObjectType():
    51  					return cty.NilType, function.NewArgErrorf(0, "incompatible object type for conversion: %s", convert.MismatchMessage(gotTy, wantTy))
    52  				default:
    53  					return cty.NilType, function.NewArgErrorf(0, "cannot convert %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint())
    54  				}
    55  			}
    56  			// If a conversion is available then everything is fine.
    57  			return wantTy, nil
    58  		},
    59  		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
    60  			// We didn't set "AllowUnknown" on our argument, so it is guaranteed
    61  			// to be known here but may still be null.
    62  			ret, err := convert.Convert(args[0], retType)
    63  			if err != nil {
    64  				val, _ := args[0].UnmarkDeep()
    65  				// Because we used GetConversionUnsafe above, conversion can
    66  				// still potentially fail in here. For example, if the user
    67  				// asks to convert the string "a" to bool then we'll
    68  				// optimistically permit it during type checking but fail here
    69  				// once we note that the value isn't either "true" or "false".
    70  				gotTy := val.Type()
    71  				switch {
    72  				case marks.Contains(args[0], marks.Sensitive):
    73  					// Generic message so we won't inadvertently disclose
    74  					// information about sensitive values.
    75  					return cty.NilVal, function.NewArgErrorf(0, "cannot convert this sensitive %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint())
    76  
    77  				case gotTy == cty.String && wantTy == cty.Bool:
    78  					what := "string"
    79  					if !val.IsNull() {
    80  						what = strconv.Quote(val.AsString())
    81  					}
    82  					return cty.NilVal, function.NewArgErrorf(0, `cannot convert %s to bool; only the strings "true" or "false" are allowed`, what)
    83  				case gotTy == cty.String && wantTy == cty.Number:
    84  					what := "string"
    85  					if !val.IsNull() {
    86  						what = strconv.Quote(val.AsString())
    87  					}
    88  					return cty.NilVal, function.NewArgErrorf(0, `cannot convert %s to number; given string must be a decimal representation of a number`, what)
    89  				default:
    90  					return cty.NilVal, function.NewArgErrorf(0, "cannot convert %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint())
    91  				}
    92  			}
    93  			return ret, nil
    94  		},
    95  	})
    96  }