github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/typeexpr/get_type.go (about)

     1  package typeexpr
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/hashicorp/hcl/v2"
     7  	"github.com/zclconf/go-cty/cty"
     8  )
     9  
    10  const invalidTypeSummary = "Invalid type specification"
    11  
    12  // getType is the internal implementation of both Type and TypeConstraint,
    13  // using the passed flag to distinguish. When constraint is false, the "any"
    14  // keyword will produce an error.
    15  func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
    16  	// First we'll try for one of our keywords
    17  	kw := hcl.ExprAsKeyword(expr)
    18  	switch kw {
    19  	case "bool":
    20  		return cty.Bool, nil
    21  	case "string":
    22  		return cty.String, nil
    23  	case "number":
    24  		return cty.Number, nil
    25  	case "any":
    26  		if constraint {
    27  			return cty.DynamicPseudoType, nil
    28  		}
    29  		return cty.DynamicPseudoType, hcl.Diagnostics{{
    30  			Severity: hcl.DiagError,
    31  			Summary:  invalidTypeSummary,
    32  			Detail:   fmt.Sprintf("The keyword %q cannot be used in this type specification: an exact type is required.", kw),
    33  			Subject:  expr.Range().Ptr(),
    34  		}}
    35  	case "list", "map", "set":
    36  		return cty.DynamicPseudoType, hcl.Diagnostics{{
    37  			Severity: hcl.DiagError,
    38  			Summary:  invalidTypeSummary,
    39  			Detail:   fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", kw),
    40  			Subject:  expr.Range().Ptr(),
    41  		}}
    42  	case "object":
    43  		return cty.DynamicPseudoType, hcl.Diagnostics{{
    44  			Severity: hcl.DiagError,
    45  			Summary:  invalidTypeSummary,
    46  			Detail:   "The object type constructor requires one argument specifying the attribute types and values as a map.",
    47  			Subject:  expr.Range().Ptr(),
    48  		}}
    49  	case "tuple":
    50  		return cty.DynamicPseudoType, hcl.Diagnostics{{
    51  			Severity: hcl.DiagError,
    52  			Summary:  invalidTypeSummary,
    53  			Detail:   "The tuple type constructor requires one argument specifying the element types as a list.",
    54  			Subject:  expr.Range().Ptr(),
    55  		}}
    56  	case "":
    57  		// okay! we'll fall through and try processing as a call, then.
    58  	default:
    59  		return cty.DynamicPseudoType, hcl.Diagnostics{{
    60  			Severity: hcl.DiagError,
    61  			Summary:  invalidTypeSummary,
    62  			Detail:   fmt.Sprintf("The keyword %q is not a valid type specification.", kw),
    63  			Subject:  expr.Range().Ptr(),
    64  		}}
    65  	}
    66  
    67  	// If we get down here then our expression isn't just a keyword, so we'll
    68  	// try to process it as a call instead.
    69  	call, diags := hcl.ExprCall(expr)
    70  	if diags.HasErrors() {
    71  		return cty.DynamicPseudoType, hcl.Diagnostics{{
    72  			Severity: hcl.DiagError,
    73  			Summary:  invalidTypeSummary,
    74  			Detail:   "A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).",
    75  			Subject:  expr.Range().Ptr(),
    76  		}}
    77  	}
    78  
    79  	switch call.Name {
    80  	case "bool", "string", "number", "any":
    81  		return cty.DynamicPseudoType, hcl.Diagnostics{{
    82  			Severity: hcl.DiagError,
    83  			Summary:  invalidTypeSummary,
    84  			Detail:   fmt.Sprintf("Primitive type keyword %q does not expect arguments.", call.Name),
    85  			Subject:  &call.ArgsRange,
    86  		}}
    87  	}
    88  
    89  	if len(call.Arguments) != 1 {
    90  		contextRange := call.ArgsRange
    91  		subjectRange := call.ArgsRange
    92  		if len(call.Arguments) > 1 {
    93  			// If we have too many arguments (as opposed to too _few_) then
    94  			// we'll highlight the extraneous arguments as the diagnostic
    95  			// subject.
    96  			subjectRange = hcl.RangeBetween(call.Arguments[1].Range(), call.Arguments[len(call.Arguments)-1].Range())
    97  		}
    98  
    99  		switch call.Name {
   100  		case "list", "set", "map":
   101  			return cty.DynamicPseudoType, hcl.Diagnostics{{
   102  				Severity: hcl.DiagError,
   103  				Summary:  invalidTypeSummary,
   104  				Detail:   fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", call.Name),
   105  				Subject:  &subjectRange,
   106  				Context:  &contextRange,
   107  			}}
   108  		case "object":
   109  			return cty.DynamicPseudoType, hcl.Diagnostics{{
   110  				Severity: hcl.DiagError,
   111  				Summary:  invalidTypeSummary,
   112  				Detail:   "The object type constructor requires one argument specifying the attribute types and values as a map.",
   113  				Subject:  &subjectRange,
   114  				Context:  &contextRange,
   115  			}}
   116  		case "tuple":
   117  			return cty.DynamicPseudoType, hcl.Diagnostics{{
   118  				Severity: hcl.DiagError,
   119  				Summary:  invalidTypeSummary,
   120  				Detail:   "The tuple type constructor requires one argument specifying the element types as a list.",
   121  				Subject:  &subjectRange,
   122  				Context:  &contextRange,
   123  			}}
   124  		}
   125  	}
   126  
   127  	switch call.Name {
   128  
   129  	case "list":
   130  		ety, diags := getType(call.Arguments[0], constraint)
   131  		return cty.List(ety), diags
   132  	case "set":
   133  		ety, diags := getType(call.Arguments[0], constraint)
   134  		return cty.Set(ety), diags
   135  	case "map":
   136  		ety, diags := getType(call.Arguments[0], constraint)
   137  		return cty.Map(ety), diags
   138  	case "object":
   139  		attrDefs, diags := hcl.ExprMap(call.Arguments[0])
   140  		if diags.HasErrors() {
   141  			return cty.DynamicPseudoType, hcl.Diagnostics{{
   142  				Severity: hcl.DiagError,
   143  				Summary:  invalidTypeSummary,
   144  				Detail:   "Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.",
   145  				Subject:  call.Arguments[0].Range().Ptr(),
   146  				Context:  expr.Range().Ptr(),
   147  			}}
   148  		}
   149  
   150  		atys := make(map[string]cty.Type)
   151  		var optAttrs []string
   152  		for _, attrDef := range attrDefs {
   153  			attrName := hcl.ExprAsKeyword(attrDef.Key)
   154  			if attrName == "" {
   155  				diags = append(diags, &hcl.Diagnostic{
   156  					Severity: hcl.DiagError,
   157  					Summary:  invalidTypeSummary,
   158  					Detail:   "Object constructor map keys must be attribute names.",
   159  					Subject:  attrDef.Key.Range().Ptr(),
   160  					Context:  expr.Range().Ptr(),
   161  				})
   162  				continue
   163  			}
   164  			atyExpr := attrDef.Value
   165  
   166  			// the attribute type expression might be wrapped in the special
   167  			// modifier optional(...) to indicate an optional attribute. If
   168  			// so, we'll unwrap that first and make a note about it being
   169  			// optional for when we construct the type below.
   170  			if call, callDiags := hcl.ExprCall(atyExpr); !callDiags.HasErrors() {
   171  				if call.Name == "optional" {
   172  					if len(call.Arguments) < 1 {
   173  						diags = append(diags, &hcl.Diagnostic{
   174  							Severity: hcl.DiagError,
   175  							Summary:  invalidTypeSummary,
   176  							Detail:   "Optional attribute modifier requires the attribute type as its argument.",
   177  							Subject:  call.ArgsRange.Ptr(),
   178  							Context:  atyExpr.Range().Ptr(),
   179  						})
   180  						continue
   181  					}
   182  					if constraint {
   183  						if len(call.Arguments) > 1 {
   184  							diags = append(diags, &hcl.Diagnostic{
   185  								Severity: hcl.DiagError,
   186  								Summary:  invalidTypeSummary,
   187  								Detail:   "Optional attribute modifier expects only one argument: the attribute type.",
   188  								Subject:  call.ArgsRange.Ptr(),
   189  								Context:  atyExpr.Range().Ptr(),
   190  							})
   191  						}
   192  						optAttrs = append(optAttrs, attrName)
   193  					} else {
   194  						diags = append(diags, &hcl.Diagnostic{
   195  							Severity: hcl.DiagError,
   196  							Summary:  invalidTypeSummary,
   197  							Detail:   "Optional attribute modifier is only for type constraints, not for exact types.",
   198  							Subject:  call.NameRange.Ptr(),
   199  							Context:  atyExpr.Range().Ptr(),
   200  						})
   201  					}
   202  					atyExpr = call.Arguments[0]
   203  				}
   204  			}
   205  
   206  			aty, attrDiags := getType(atyExpr, constraint)
   207  			diags = append(diags, attrDiags...)
   208  			atys[attrName] = aty
   209  		}
   210  		// NOTE: ObjectWithOptionalAttrs is experimental in cty at the
   211  		// time of writing, so this interface might change even in future
   212  		// minor versions of cty. We're accepting that because Terraform
   213  		// itself is considering optional attributes as experimental right now.
   214  		return cty.ObjectWithOptionalAttrs(atys, optAttrs), diags
   215  	case "tuple":
   216  		elemDefs, diags := hcl.ExprList(call.Arguments[0])
   217  		if diags.HasErrors() {
   218  			return cty.DynamicPseudoType, hcl.Diagnostics{{
   219  				Severity: hcl.DiagError,
   220  				Summary:  invalidTypeSummary,
   221  				Detail:   "Tuple type constructor requires a list of element types.",
   222  				Subject:  call.Arguments[0].Range().Ptr(),
   223  				Context:  expr.Range().Ptr(),
   224  			}}
   225  		}
   226  		etys := make([]cty.Type, len(elemDefs))
   227  		for i, defExpr := range elemDefs {
   228  			ety, elemDiags := getType(defExpr, constraint)
   229  			diags = append(diags, elemDiags...)
   230  			etys[i] = ety
   231  		}
   232  		return cty.Tuple(etys), diags
   233  	case "optional":
   234  		return cty.DynamicPseudoType, hcl.Diagnostics{{
   235  			Severity: hcl.DiagError,
   236  			Summary:  invalidTypeSummary,
   237  			Detail:   fmt.Sprintf("Keyword %q is valid only as a modifier for object type attributes.", call.Name),
   238  			Subject:  call.NameRange.Ptr(),
   239  		}}
   240  	default:
   241  		// Can't access call.Arguments in this path because we've not validated
   242  		// that it contains exactly one expression here.
   243  		return cty.DynamicPseudoType, hcl.Diagnostics{{
   244  			Severity: hcl.DiagError,
   245  			Summary:  invalidTypeSummary,
   246  			Detail:   fmt.Sprintf("Keyword %q is not a valid type constructor.", call.Name),
   247  			Subject:  expr.Range().Ptr(),
   248  		}}
   249  	}
   250  }