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

     1  package lang
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/hashicorp/hcl/v2"
     7  	"github.com/terraform-linters/tflint-plugin-sdk/hclext"
     8  	"github.com/terraform-linters/tflint/terraform/addrs"
     9  	"github.com/terraform-linters/tflint/terraform/tfdiags"
    10  	"github.com/terraform-linters/tflint/terraform/tfhcl"
    11  	"github.com/zclconf/go-cty/cty"
    12  	"github.com/zclconf/go-cty/cty/convert"
    13  )
    14  
    15  // ExpandBlock expands "dynamic" blocks and resources/modules with count/for_each.
    16  // Note that Terraform only expands dynamic blocks, but TFLint also expands
    17  // count/for_each here.
    18  //
    19  // Expressions in expanded blocks are evaluated immediately, so all variables and
    20  // function calls contained in attributes specified in the body schema are gathered.
    21  func (s *Scope) ExpandBlock(body hcl.Body, schema *hclext.BodySchema) (hcl.Body, hcl.Diagnostics) {
    22  	traversals := tfhcl.ExpandVariablesHCLExt(body, schema)
    23  	refs, diags := References(traversals)
    24  
    25  	exprs := tfhcl.ExpandExpressionsHCLExt(body, schema)
    26  	funcCalls := []*FunctionCall{}
    27  	for _, expr := range exprs {
    28  		calls, funcDiags := FunctionCallsInExpr(expr)
    29  		diags = diags.Extend(funcDiags)
    30  		funcCalls = append(funcCalls, calls...)
    31  	}
    32  
    33  	ctx, ctxDiags := s.EvalContext(refs, funcCalls)
    34  	diags = diags.Extend(ctxDiags)
    35  
    36  	return tfhcl.Expand(body, ctx), diags
    37  }
    38  
    39  // EvalExpr evaluates a single expression in the receiving context and returns
    40  // the resulting value. The value will be converted to the given type before
    41  // it is returned if possible, or else an error diagnostic will be produced
    42  // describing the conversion error.
    43  //
    44  // Pass an expected type of cty.DynamicPseudoType to skip automatic conversion
    45  // and just obtain the returned value directly.
    46  //
    47  // If the returned diagnostics contains errors then the result may be
    48  // incomplete, but will always be of the requested type.
    49  func (s *Scope) EvalExpr(expr hcl.Expression, wantType cty.Type) (cty.Value, hcl.Diagnostics) {
    50  	refs, diags := ReferencesInExpr(expr)
    51  	funcCalls, funcDiags := FunctionCallsInExpr(expr)
    52  	diags = diags.Extend(funcDiags)
    53  
    54  	ctx, ctxDiags := s.EvalContext(refs, funcCalls)
    55  	diags = diags.Extend(ctxDiags)
    56  	if diags.HasErrors() {
    57  		// We'll stop early if we found problems in the references, because
    58  		// it's likely evaluation will produce redundant copies of the same errors.
    59  		return cty.UnknownVal(wantType), diags
    60  	}
    61  
    62  	val, evalDiags := expr.Value(ctx)
    63  	diags = diags.Extend(evalDiags)
    64  
    65  	if wantType != cty.DynamicPseudoType {
    66  		var convErr error
    67  		val, convErr = convert.Convert(val, wantType)
    68  		if convErr != nil {
    69  			val = cty.UnknownVal(wantType)
    70  			diags = diags.Append(&hcl.Diagnostic{
    71  				Severity:    hcl.DiagError,
    72  				Summary:     "Incorrect value type",
    73  				Detail:      fmt.Sprintf("Invalid expression value: %s.", tfdiags.FormatError(convErr)),
    74  				Subject:     expr.Range().Ptr(),
    75  				Expression:  expr,
    76  				EvalContext: ctx,
    77  			})
    78  		}
    79  	}
    80  
    81  	return val, diags
    82  }
    83  
    84  // EvalContext constructs a HCL expression evaluation context whose variable
    85  // scope contains sufficient values to satisfy the given set of references
    86  // and function calls.
    87  //
    88  // Most callers should prefer to use the evaluation helper methods that
    89  // this type offers, but this is here for less common situations where the
    90  // caller will handle the evaluation calls itself.
    91  func (s *Scope) EvalContext(refs []*addrs.Reference, funcCalls []*FunctionCall) (*hcl.EvalContext, hcl.Diagnostics) {
    92  	return s.evalContext(refs, s.SelfAddr, funcCalls)
    93  }
    94  
    95  func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceable, funcCalls []*FunctionCall) (*hcl.EvalContext, hcl.Diagnostics) {
    96  	if s == nil {
    97  		panic("attempt to construct EvalContext for nil Scope")
    98  	}
    99  
   100  	var diags hcl.Diagnostics
   101  	vals := make(map[string]cty.Value)
   102  	funcs := s.Functions()
   103  	// Provider-defined functions introduced in Terraform v1.8 cannot be
   104  	// evaluated statically in many cases. Here, we avoid the error by dynamically
   105  	// generating an evaluation context in which the provider-defined functions
   106  	// in the given expression are replaced with mock functions.
   107  	for _, call := range funcCalls {
   108  		if !call.IsProviderDefined() {
   109  			continue
   110  		}
   111  		// Some provider-defined functions are supported,
   112  		// so only generate mocks for undefined functions
   113  		if _, exists := funcs[call.Name]; !exists {
   114  			funcs[call.Name] = NewMockFunction(call)
   115  		}
   116  	}
   117  	ctx := &hcl.EvalContext{
   118  		Variables: vals,
   119  		Functions: funcs,
   120  	}
   121  
   122  	if len(refs) == 0 {
   123  		// Easy path for common case where there are no references at all.
   124  		return ctx, diags
   125  	}
   126  
   127  	// The reference set we are given has not been de-duped, and so there can
   128  	// be redundant requests in it for two reasons:
   129  	//  - The same item is referenced multiple times
   130  	//  - Both an item and that item's container are separately referenced.
   131  	// We will still visit every reference here and ask our data source for
   132  	// it, since that allows us to gather a full set of any errors and
   133  	// warnings, but once we've gathered all the data we'll then skip anything
   134  	// that's redundant in the process of populating our values map.
   135  	managedResources := map[string]cty.Value{}
   136  	inputVariables := map[string]cty.Value{}
   137  	localValues := map[string]cty.Value{}
   138  	pathAttrs := map[string]cty.Value{}
   139  	terraformAttrs := map[string]cty.Value{}
   140  	countAttrs := map[string]cty.Value{}
   141  	forEachAttrs := map[string]cty.Value{}
   142  
   143  	for _, ref := range refs {
   144  		rng := ref.SourceRange
   145  
   146  		rawSubj := ref.Subject
   147  
   148  		// This type switch must cover all of the "Referenceable" implementations
   149  		// in package addrs, however we are removing the possibility of
   150  		// Instances beforehand.
   151  		switch addr := rawSubj.(type) {
   152  		case addrs.ResourceInstance:
   153  			rawSubj = addr.ContainingResource()
   154  		}
   155  
   156  		switch subj := rawSubj.(type) {
   157  		case addrs.Resource:
   158  			// Managed resources are not supported by TFLint, but it does support arbitrary
   159  			// key names, so it gathers the referenced resource names.
   160  			if subj.Mode != addrs.ManagedResourceMode {
   161  				continue
   162  			}
   163  			managedResources[subj.Type] = cty.UnknownVal(cty.DynamicPseudoType)
   164  
   165  		case addrs.InputVariable:
   166  			val, valDiags := normalizeRefValue(s.Data.GetInputVariable(subj, rng))
   167  			diags = diags.Extend(valDiags)
   168  			inputVariables[subj.Name] = val
   169  
   170  		case addrs.LocalValue:
   171  			val, valDiags := normalizeRefValue(s.Data.GetLocalValue(subj, rng))
   172  			diags = diags.Extend(valDiags)
   173  			localValues[subj.Name] = val
   174  
   175  		case addrs.PathAttr:
   176  			val, valDiags := normalizeRefValue(s.Data.GetPathAttr(subj, rng))
   177  			diags = diags.Extend(valDiags)
   178  			pathAttrs[subj.Name] = val
   179  
   180  		case addrs.TerraformAttr:
   181  			val, valDiags := normalizeRefValue(s.Data.GetTerraformAttr(subj, rng))
   182  			diags = diags.Extend(valDiags)
   183  			terraformAttrs[subj.Name] = val
   184  
   185  		case addrs.CountAttr:
   186  			val, valDiags := normalizeRefValue(s.Data.GetCountAttr(subj, rng))
   187  			diags = diags.Extend(valDiags)
   188  			countAttrs[subj.Name] = val
   189  
   190  		case addrs.ForEachAttr:
   191  			val, valDiags := normalizeRefValue(s.Data.GetForEachAttr(subj, rng))
   192  			diags = diags.Extend(valDiags)
   193  			forEachAttrs[subj.Name] = val
   194  		}
   195  	}
   196  
   197  	// Managed resources are exposed in two different locations. This is
   198  	// at the top level where the resource type name is the root of the
   199  	// traversal.
   200  	for k, v := range managedResources {
   201  		vals[k] = v
   202  	}
   203  
   204  	vals["var"] = cty.ObjectVal(inputVariables)
   205  	vals["local"] = cty.ObjectVal(localValues)
   206  	vals["path"] = cty.ObjectVal(pathAttrs)
   207  	vals["terraform"] = cty.ObjectVal(terraformAttrs)
   208  	vals["count"] = cty.ObjectVal(countAttrs)
   209  	vals["each"] = cty.ObjectVal(forEachAttrs)
   210  
   211  	// The following are unknown values as they are not supported by TFLint.
   212  	vals["resource"] = cty.UnknownVal(cty.DynamicPseudoType)
   213  	vals["data"] = cty.UnknownVal(cty.DynamicPseudoType)
   214  	vals["module"] = cty.UnknownVal(cty.DynamicPseudoType)
   215  	vals["self"] = cty.UnknownVal(cty.DynamicPseudoType)
   216  
   217  	return ctx, diags
   218  }
   219  
   220  func normalizeRefValue(val cty.Value, diags hcl.Diagnostics) (cty.Value, hcl.Diagnostics) {
   221  	if diags.HasErrors() {
   222  		// If there are errors then we will force an unknown result so that
   223  		// we can still evaluate and catch type errors but we'll avoid
   224  		// producing redundant re-statements of the same errors we've already
   225  		// dealt with here.
   226  		return cty.UnknownVal(val.Type()), diags
   227  	}
   228  	return val, diags
   229  }