github.com/mineiros-io/terradoc@v0.0.9-0.20220711062319-018bd4ae81f5/internal/parsers/hclparser/hcltype.go (about)

     1  package hclparser
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/hashicorp/hcl/v2"
     8  	"github.com/hashicorp/hcl/v2/hclsyntax"
     9  	"github.com/mineiros-io/terradoc/internal/entities"
    10  	"github.com/mineiros-io/terradoc/internal/types"
    11  	"github.com/zclconf/go-cty/cty"
    12  	"github.com/zclconf/go-cty/cty/function"
    13  	"github.com/zclconf/go-cty/cty/gocty"
    14  )
    15  
    16  func GetVarTypeFromExpression(expr hcl.Expression) (entities.Type, error) {
    17  	return getTypeFromExpression(expr, varFunctions())
    18  }
    19  
    20  func GetOutputTypeFromExpression(expr hcl.Expression) (entities.Type, error) {
    21  	return getTypeFromExpression(expr, outputFunctions())
    22  }
    23  
    24  func getTypeFromExpression(expr hcl.Expression, ctxFunctions map[string]function.Function) (entities.Type, error) {
    25  	kw := hcl.ExprAsKeyword(expr)
    26  
    27  	switch kw {
    28  	case "string", "number", "bool", "any":
    29  		tfType, ok := types.TerraformTypes(kw)
    30  		if !ok {
    31  			return entities.Type{}, fmt.Errorf("could not get terraform type for %q", kw)
    32  		}
    33  
    34  		return entities.Type{TFType: tfType}, nil
    35  	case "list", "object", "map", "tuple":
    36  		// invalid as these types should be function calls
    37  		return entities.Type{}, fmt.Errorf("type %q needs an argument", kw)
    38  	}
    39  
    40  	// TODO: how to make this decent?
    41  	if kw != "" && !(strings.HasPrefix(kw, "list") ||
    42  		strings.HasPrefix(kw, "object") ||
    43  		strings.HasPrefix(kw, "map") ||
    44  		strings.HasPrefix(kw, "tuple")) {
    45  		return entities.Type{}, fmt.Errorf("type %q is invalid", kw)
    46  	}
    47  
    48  	return getComplexType(expr, ctxFunctions)
    49  }
    50  
    51  // this function exists to make it possible to parse `type` attribute expressions and `readme_type`
    52  // attribute strings in the same way, so they are compatible even though they have different types
    53  func getVarTypeFromString(str string, startRange hcl.Pos) (entities.Type, error) {
    54  	expr, parseDiags := hclsyntax.ParseExpression([]byte(str), "", startRange)
    55  	if parseDiags.HasErrors() {
    56  		return entities.Type{}, fmt.Errorf("parsing type string expression: %v", parseDiags.Errs())
    57  	}
    58  
    59  	return GetVarTypeFromExpression(expr)
    60  }
    61  
    62  func getComplexType(expr hcl.Expression, ctxFunctions map[string]function.Function) (entities.Type, error) {
    63  	got, exprDiags := expr.Value(getEvalContextForExpr(expr, ctxFunctions))
    64  	if exprDiags.HasErrors() {
    65  		return entities.Type{}, fmt.Errorf("getting expression value: %v", exprDiags.Errs())
    66  	}
    67  	var tfType, nestedTFType types.TerraformType
    68  
    69  	err := gocty.FromCtyValue(got.GetAttr("type"), &tfType)
    70  	if err != nil {
    71  		return entities.Type{}, fmt.Errorf("getting type definition: %v", err)
    72  	}
    73  
    74  	err = gocty.FromCtyValue(got.GetAttr("nestedType"), &nestedTFType)
    75  	if err != nil {
    76  		return entities.Type{}, fmt.Errorf("getting nested type definition: %v", err)
    77  	}
    78  
    79  	var typeLabel, nestedTypeLabel string
    80  	err = gocty.FromCtyValue(got.GetAttr("typeLabel"), &typeLabel)
    81  	if err != nil {
    82  		return entities.Type{}, fmt.Errorf("getting type label: %v", err)
    83  	}
    84  
    85  	err = gocty.FromCtyValue(got.GetAttr("nestedTypeLabel"), &nestedTypeLabel)
    86  	if err != nil {
    87  		return entities.Type{}, fmt.Errorf("getting nested type label: %v", err)
    88  	}
    89  
    90  	typeDef := entities.Type{
    91  		TFType: tfType,
    92  		Label:  typeLabel,
    93  	}
    94  
    95  	if nestedTFType != types.TerraformEmptyType {
    96  		typeDef.Nested = &entities.Type{
    97  			TFType: nestedTFType,
    98  			Label:  nestedTypeLabel,
    99  		}
   100  	}
   101  
   102  	return typeDef, nil
   103  }
   104  
   105  func nestedTypeFunc(tfType types.TerraformType) function.Function {
   106  	return function.New(&function.Spec{
   107  		Params: []function.Parameter{
   108  			{
   109  				Name:             "nestedTypeLabel",
   110  				Type:             cty.String,
   111  				AllowDynamicType: true,
   112  			},
   113  		},
   114  		Type: function.StaticReturnType(cty.Object(typeObj())),
   115  		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   116  			var nestedType types.TerraformType
   117  			var nestedLabel string
   118  
   119  			nestedTypeName := args[0].AsString()
   120  
   121  			nestedType, ok := types.TerraformTypes(nestedTypeName)
   122  			if !ok {
   123  				nestedType = types.TerraformObject
   124  				nestedLabel = nestedTypeName
   125  			}
   126  
   127  			return cty.ObjectVal(map[string]cty.Value{
   128  				"type":            cty.NumberIntVal(int64(tfType)),
   129  				"typeLabel":       cty.StringVal(""), // need to pass empty value here so cty doesn't panic
   130  				"nestedType":      cty.NumberIntVal(int64(nestedType)),
   131  				"nestedTypeLabel": cty.StringVal(nestedLabel),
   132  			}), nil
   133  		},
   134  	})
   135  }
   136  
   137  func complexTypeFunc(tfType types.TerraformType) function.Function {
   138  	return function.New(&function.Spec{
   139  		Params: []function.Parameter{
   140  			{
   141  				Name:             "typeLabel",
   142  				Type:             cty.String,
   143  				AllowDynamicType: true,
   144  			},
   145  		},
   146  		Type: function.StaticReturnType(cty.Object(typeObj())),
   147  		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   148  			typeLabel := args[0].AsString()
   149  
   150  			return cty.ObjectVal(map[string]cty.Value{
   151  				"type":      cty.NumberIntVal(int64(tfType)),
   152  				"typeLabel": cty.StringVal(typeLabel),
   153  				// the following empty values need to be set to the attributes
   154  				// otherwise cty panics
   155  				"nestedType":      cty.NumberIntVal(int64(types.TerraformEmptyType)),
   156  				"nestedTypeLabel": cty.StringVal(""), //
   157  			}), nil
   158  		},
   159  	})
   160  }
   161  
   162  func getVariablesMap(expr hcl.Expression) map[string]cty.Value {
   163  	varMap := make(map[string]cty.Value)
   164  	for _, variable := range expr.Variables() {
   165  		name := variable.RootName()
   166  
   167  		varMap[name] = cty.StringVal(name)
   168  	}
   169  
   170  	return varMap
   171  }
   172  
   173  func getEvalContextForExpr(expr hcl.Expression, ctxFunctions map[string]function.Function) *hcl.EvalContext {
   174  	return &hcl.EvalContext{
   175  		Functions: ctxFunctions,
   176  		Variables: getVariablesMap(expr),
   177  	}
   178  }
   179  
   180  func varFunctions() map[string]function.Function {
   181  	return map[string]function.Function{
   182  		"object": complexTypeFunc(types.TerraformObject),
   183  		"map":    nestedTypeFunc(types.TerraformMap),
   184  		"list":   nestedTypeFunc(types.TerraformList),
   185  		"set":    nestedTypeFunc(types.TerraformSet),
   186  	}
   187  }
   188  
   189  func outputFunctions() map[string]function.Function {
   190  	return map[string]function.Function{
   191  		"resource": complexTypeFunc(types.TerraformResource),
   192  		"object":   complexTypeFunc(types.TerraformObject),
   193  		"map":      nestedTypeFunc(types.TerraformMap),
   194  		"list":     nestedTypeFunc(types.TerraformList),
   195  		"set":      nestedTypeFunc(types.TerraformSet),
   196  	}
   197  }
   198  
   199  func typeObj() map[string]cty.Type {
   200  	return map[string]cty.Type{
   201  		"type":            cty.Number,
   202  		"typeLabel":       cty.String,
   203  		"nestedType":      cty.Number,
   204  		"nestedTypeLabel": cty.String,
   205  	}
   206  }