github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/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  	"github.com/zclconf/go-cty/cty/convert"
     9  )
    10  
    11  const invalidTypeSummary = "Invalid type specification"
    12  
    13  // getType is the internal implementation of Type, TypeConstraint, and
    14  // TypeConstraintWithDefaults, using the passed flags to distinguish. When
    15  // `constraint` is true, the "any" keyword can be used in place of a concrete
    16  // type. When `withDefaults` is true, the "optional" call expression supports
    17  // an additional argument describing a default value.
    18  func getType(expr hcl.Expression, constraint, withDefaults bool) (cty.Type, *Defaults, hcl.Diagnostics) {
    19  	// First we'll try for one of our keywords
    20  	kw := hcl.ExprAsKeyword(expr)
    21  	switch kw {
    22  	case "bool":
    23  		return cty.Bool, nil, nil
    24  	case "string":
    25  		return cty.String, nil, nil
    26  	case "number":
    27  		return cty.Number, nil, nil
    28  	case "any":
    29  		if constraint {
    30  			return cty.DynamicPseudoType, nil, nil
    31  		}
    32  		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
    33  			Severity: hcl.DiagError,
    34  			Summary:  invalidTypeSummary,
    35  			Detail:   fmt.Sprintf("The keyword %q cannot be used in this type specification: an exact type is required.", kw),
    36  			Subject:  expr.Range().Ptr(),
    37  		}}
    38  	case "list", "map", "set":
    39  		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
    40  			Severity: hcl.DiagError,
    41  			Summary:  invalidTypeSummary,
    42  			Detail:   fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", kw),
    43  			Subject:  expr.Range().Ptr(),
    44  		}}
    45  	case "object":
    46  		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
    47  			Severity: hcl.DiagError,
    48  			Summary:  invalidTypeSummary,
    49  			Detail:   "The object type constructor requires one argument specifying the attribute types and values as a map.",
    50  			Subject:  expr.Range().Ptr(),
    51  		}}
    52  	case "tuple":
    53  		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
    54  			Severity: hcl.DiagError,
    55  			Summary:  invalidTypeSummary,
    56  			Detail:   "The tuple type constructor requires one argument specifying the element types as a list.",
    57  			Subject:  expr.Range().Ptr(),
    58  		}}
    59  	case "":
    60  		// okay! we'll fall through and try processing as a call, then.
    61  	default:
    62  		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
    63  			Severity: hcl.DiagError,
    64  			Summary:  invalidTypeSummary,
    65  			Detail:   fmt.Sprintf("The keyword %q is not a valid type specification.", kw),
    66  			Subject:  expr.Range().Ptr(),
    67  		}}
    68  	}
    69  
    70  	// If we get down here then our expression isn't just a keyword, so we'll
    71  	// try to process it as a call instead.
    72  	call, diags := hcl.ExprCall(expr)
    73  	if diags.HasErrors() {
    74  		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
    75  			Severity: hcl.DiagError,
    76  			Summary:  invalidTypeSummary,
    77  			Detail:   "A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).",
    78  			Subject:  expr.Range().Ptr(),
    79  		}}
    80  	}
    81  
    82  	switch call.Name {
    83  	case "bool", "string", "number":
    84  		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
    85  			Severity: hcl.DiagError,
    86  			Summary:  invalidTypeSummary,
    87  			Detail:   fmt.Sprintf("Primitive type keyword %q does not expect arguments.", call.Name),
    88  			Subject:  &call.ArgsRange,
    89  		}}
    90  	case "any":
    91  		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
    92  			Severity: hcl.DiagError,
    93  			Summary:  invalidTypeSummary,
    94  			Detail:   fmt.Sprintf("Type constraint keyword %q does not expect arguments.", call.Name),
    95  			Subject:  &call.ArgsRange,
    96  		}}
    97  	}
    98  
    99  	if len(call.Arguments) != 1 {
   100  		contextRange := call.ArgsRange
   101  		subjectRange := call.ArgsRange
   102  		if len(call.Arguments) > 1 {
   103  			// If we have too many arguments (as opposed to too _few_) then
   104  			// we'll highlight the extraneous arguments as the diagnostic
   105  			// subject.
   106  			subjectRange = hcl.RangeBetween(call.Arguments[1].Range(), call.Arguments[len(call.Arguments)-1].Range())
   107  		}
   108  
   109  		switch call.Name {
   110  		case "list", "set", "map":
   111  			return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
   112  				Severity: hcl.DiagError,
   113  				Summary:  invalidTypeSummary,
   114  				Detail:   fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", call.Name),
   115  				Subject:  &subjectRange,
   116  				Context:  &contextRange,
   117  			}}
   118  		case "object":
   119  			return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
   120  				Severity: hcl.DiagError,
   121  				Summary:  invalidTypeSummary,
   122  				Detail:   "The object type constructor requires one argument specifying the attribute types and values as a map.",
   123  				Subject:  &subjectRange,
   124  				Context:  &contextRange,
   125  			}}
   126  		case "tuple":
   127  			return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
   128  				Severity: hcl.DiagError,
   129  				Summary:  invalidTypeSummary,
   130  				Detail:   "The tuple type constructor requires one argument specifying the element types as a list.",
   131  				Subject:  &subjectRange,
   132  				Context:  &contextRange,
   133  			}}
   134  		}
   135  	}
   136  
   137  	switch call.Name {
   138  
   139  	case "list":
   140  		ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults)
   141  		ty := cty.List(ety)
   142  		return ty, collectionDefaults(ty, defaults), diags
   143  	case "set":
   144  		ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults)
   145  		ty := cty.Set(ety)
   146  		return ty, collectionDefaults(ty, defaults), diags
   147  	case "map":
   148  		ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults)
   149  		ty := cty.Map(ety)
   150  		return ty, collectionDefaults(ty, defaults), diags
   151  	case "object":
   152  		attrDefs, diags := hcl.ExprMap(call.Arguments[0])
   153  		if diags.HasErrors() {
   154  			return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
   155  				Severity: hcl.DiagError,
   156  				Summary:  invalidTypeSummary,
   157  				Detail:   "Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.",
   158  				Subject:  call.Arguments[0].Range().Ptr(),
   159  				Context:  expr.Range().Ptr(),
   160  			}}
   161  		}
   162  
   163  		atys := make(map[string]cty.Type)
   164  		defaultValues := make(map[string]cty.Value)
   165  		children := make(map[string]*Defaults)
   166  		var optAttrs []string
   167  		for _, attrDef := range attrDefs {
   168  			attrName := hcl.ExprAsKeyword(attrDef.Key)
   169  			if attrName == "" {
   170  				diags = append(diags, &hcl.Diagnostic{
   171  					Severity: hcl.DiagError,
   172  					Summary:  invalidTypeSummary,
   173  					Detail:   "Object constructor map keys must be attribute names.",
   174  					Subject:  attrDef.Key.Range().Ptr(),
   175  					Context:  expr.Range().Ptr(),
   176  				})
   177  				continue
   178  			}
   179  			atyExpr := attrDef.Value
   180  
   181  			// the attribute type expression might be wrapped in the special
   182  			// modifier optional(...) to indicate an optional attribute. If
   183  			// so, we'll unwrap that first and make a note about it being
   184  			// optional for when we construct the type below.
   185  			var defaultExpr hcl.Expression
   186  			if call, callDiags := hcl.ExprCall(atyExpr); !callDiags.HasErrors() {
   187  				if call.Name == "optional" {
   188  					if len(call.Arguments) < 1 {
   189  						diags = append(diags, &hcl.Diagnostic{
   190  							Severity: hcl.DiagError,
   191  							Summary:  invalidTypeSummary,
   192  							Detail:   "Optional attribute modifier requires the attribute type as its argument.",
   193  							Subject:  call.ArgsRange.Ptr(),
   194  							Context:  atyExpr.Range().Ptr(),
   195  						})
   196  						continue
   197  					}
   198  					if constraint {
   199  						if withDefaults {
   200  							switch len(call.Arguments) {
   201  							case 2:
   202  								defaultExpr = call.Arguments[1]
   203  								defaultVal, defaultDiags := defaultExpr.Value(nil)
   204  								diags = append(diags, defaultDiags...)
   205  								if !defaultDiags.HasErrors() {
   206  									optAttrs = append(optAttrs, attrName)
   207  									defaultValues[attrName] = defaultVal
   208  								}
   209  							case 1:
   210  								optAttrs = append(optAttrs, attrName)
   211  							default:
   212  								diags = append(diags, &hcl.Diagnostic{
   213  									Severity: hcl.DiagError,
   214  									Summary:  invalidTypeSummary,
   215  									Detail:   "Optional attribute modifier expects at most two arguments: the attribute type, and a default value.",
   216  									Subject:  call.ArgsRange.Ptr(),
   217  									Context:  atyExpr.Range().Ptr(),
   218  								})
   219  							}
   220  						} else {
   221  							if len(call.Arguments) == 1 {
   222  								optAttrs = append(optAttrs, attrName)
   223  							} else {
   224  								diags = append(diags, &hcl.Diagnostic{
   225  									Severity: hcl.DiagError,
   226  									Summary:  invalidTypeSummary,
   227  									Detail:   "Optional attribute modifier expects only one argument: the attribute type.",
   228  									Subject:  call.ArgsRange.Ptr(),
   229  									Context:  atyExpr.Range().Ptr(),
   230  								})
   231  							}
   232  						}
   233  					} else {
   234  						diags = append(diags, &hcl.Diagnostic{
   235  							Severity: hcl.DiagError,
   236  							Summary:  invalidTypeSummary,
   237  							Detail:   "Optional attribute modifier is only for type constraints, not for exact types.",
   238  							Subject:  call.NameRange.Ptr(),
   239  							Context:  atyExpr.Range().Ptr(),
   240  						})
   241  					}
   242  					atyExpr = call.Arguments[0]
   243  				}
   244  			}
   245  
   246  			aty, aDefaults, attrDiags := getType(atyExpr, constraint, withDefaults)
   247  			diags = append(diags, attrDiags...)
   248  
   249  			// If a default is set for an optional attribute, verify that it is
   250  			// convertible to the attribute type.
   251  			if defaultVal, ok := defaultValues[attrName]; ok {
   252  				_, err := convert.Convert(defaultVal, aty)
   253  				if err != nil {
   254  					diags = append(diags, &hcl.Diagnostic{
   255  						Severity: hcl.DiagError,
   256  						Summary:  "Invalid default value for optional attribute",
   257  						Detail:   fmt.Sprintf("This default value is not compatible with the attribute's type constraint: %s.", err),
   258  						Subject:  defaultExpr.Range().Ptr(),
   259  					})
   260  					delete(defaultValues, attrName)
   261  				}
   262  			}
   263  
   264  			atys[attrName] = aty
   265  			if aDefaults != nil {
   266  				children[attrName] = aDefaults
   267  			}
   268  		}
   269  		// NOTE: ObjectWithOptionalAttrs is experimental in cty at the
   270  		// time of writing, so this interface might change even in future
   271  		// minor versions of cty. We're accepting that because Durgaform
   272  		// itself is considering optional attributes as experimental right now.
   273  		ty := cty.ObjectWithOptionalAttrs(atys, optAttrs)
   274  		return ty, structuredDefaults(ty, defaultValues, children), diags
   275  	case "tuple":
   276  		elemDefs, diags := hcl.ExprList(call.Arguments[0])
   277  		if diags.HasErrors() {
   278  			return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
   279  				Severity: hcl.DiagError,
   280  				Summary:  invalidTypeSummary,
   281  				Detail:   "Tuple type constructor requires a list of element types.",
   282  				Subject:  call.Arguments[0].Range().Ptr(),
   283  				Context:  expr.Range().Ptr(),
   284  			}}
   285  		}
   286  		etys := make([]cty.Type, len(elemDefs))
   287  		children := make(map[string]*Defaults, len(elemDefs))
   288  		for i, defExpr := range elemDefs {
   289  			ety, elemDefaults, elemDiags := getType(defExpr, constraint, withDefaults)
   290  			diags = append(diags, elemDiags...)
   291  			etys[i] = ety
   292  			if elemDefaults != nil {
   293  				children[fmt.Sprintf("%d", i)] = elemDefaults
   294  			}
   295  		}
   296  		ty := cty.Tuple(etys)
   297  		return ty, structuredDefaults(ty, nil, children), diags
   298  	case "optional":
   299  		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
   300  			Severity: hcl.DiagError,
   301  			Summary:  invalidTypeSummary,
   302  			Detail:   fmt.Sprintf("Keyword %q is valid only as a modifier for object type attributes.", call.Name),
   303  			Subject:  call.NameRange.Ptr(),
   304  		}}
   305  	default:
   306  		// Can't access call.Arguments in this path because we've not validated
   307  		// that it contains exactly one expression here.
   308  		return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
   309  			Severity: hcl.DiagError,
   310  			Summary:  invalidTypeSummary,
   311  			Detail:   fmt.Sprintf("Keyword %q is not a valid type constructor.", call.Name),
   312  			Subject:  expr.Range().Ptr(),
   313  		}}
   314  	}
   315  }
   316  
   317  func collectionDefaults(ty cty.Type, defaults *Defaults) *Defaults {
   318  	if defaults == nil {
   319  		return nil
   320  	}
   321  	return &Defaults{
   322  		Type: ty,
   323  		Children: map[string]*Defaults{
   324  			"": defaults,
   325  		},
   326  	}
   327  }
   328  
   329  func structuredDefaults(ty cty.Type, defaultValues map[string]cty.Value, children map[string]*Defaults) *Defaults {
   330  	if len(defaultValues) == 0 && len(children) == 0 {
   331  		return nil
   332  	}
   333  
   334  	defaults := &Defaults{
   335  		Type: ty,
   336  	}
   337  	if len(defaultValues) > 0 {
   338  		defaults.DefaultValues = defaultValues
   339  	}
   340  	if len(children) > 0 {
   341  		defaults.Children = children
   342  	}
   343  
   344  	return defaults
   345  }