Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for function callbacks as the target of EvaluateExpr #246

Merged
merged 1 commit into from
Mar 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 43 additions & 5 deletions helper/runner.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package helper

import (
"errors"
"fmt"
"os"
"reflect"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
Expand Down Expand Up @@ -196,9 +198,45 @@ func (r *Runner) DecodeRuleConfig(name string, ret interface{}) error {
return nil
}

var errRefTy = reflect.TypeOf((*error)(nil)).Elem()

// EvaluateExpr returns a value of the passed expression.
// Note that some features are limited
func (r *Runner) EvaluateExpr(expr hcl.Expression, ret interface{}, opts *tflint.EvaluateExprOption) error {
func (r *Runner) EvaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error {
var callback bool
rval := reflect.ValueOf(target)
rty := rval.Type()
// Callback must meet the following requirements:
// - It must be a function
// - It must take an argument
// - It must return an error
if rty.Kind() == reflect.Func && rty.NumIn() == 1 && rty.NumOut() == 1 && rty.Out(0).Implements(errRefTy) {
callback = true
target = reflect.New(rty.In(0)).Interface()
}

err := r.evaluateExpr(expr, target, opts)
if !callback {
// error should be handled in the caller
return err
}

if err != nil {
// If it cannot be represented as a Go value, exit without invoking the callback rather than returning an error.
if errors.Is(err, tflint.ErrUnknownValue) || errors.Is(err, tflint.ErrNullValue) || errors.Is(err, tflint.ErrSensitive) || errors.Is(err, tflint.ErrUnevaluable) {
return nil
}
return err
}

rerr := rval.Call([]reflect.Value{reflect.ValueOf(target).Elem()})
if rerr[0].IsNil() {
return nil
}
return rerr[0].Interface().(error)
}

func (r *Runner) evaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error {
if opts == nil {
opts = &tflint.EvaluateExprOption{}
}
Expand All @@ -207,7 +245,7 @@ func (r *Runner) EvaluateExpr(expr hcl.Expression, ret interface{}, opts *tflint
if opts.WantType != nil {
ty = *opts.WantType
} else {
switch ret.(type) {
switch target.(type) {
case *string, string:
ty = cty.String
case *int, int:
Expand All @@ -223,7 +261,7 @@ func (r *Runner) EvaluateExpr(expr hcl.Expression, ret interface{}, opts *tflint
case cty.Value, *cty.Value:
ty = cty.DynamicPseudoType
default:
return fmt.Errorf("unsupported result type: %T", ret)
return fmt.Errorf("unsupported target type: %T", target)
}
}

Expand Down Expand Up @@ -251,7 +289,7 @@ func (r *Runner) EvaluateExpr(expr hcl.Expression, ret interface{}, opts *tflint
return err
}

return gocty.FromCtyValue(val, ret)
return gocty.FromCtyValue(val, target)
}

// EmitIssue adds an issue to the runner itself.
Expand All @@ -266,7 +304,7 @@ func (r *Runner) EmitIssue(rule tflint.Rule, message string, location hcl.Range)

// EnsureNoError is a method that simply runs a function if there is no error.
//
// Deprecated: Use errors.Is() instead to determine which errors can be ignored.
// Deprecated: Use EvaluateExpr with a function callback. e.g. EvaluateExpr(expr, func (val T) error {}, ...)
func (r *Runner) EnsureNoError(err error, proc func() error) error {
if err == nil {
return proc()
Expand Down
11 changes: 11 additions & 0 deletions helper/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,7 @@ resource "aws_instance" "foo" {
}

for _, resource := range resources.Blocks {
// raw value
var instanceType string
if err := runner.EvaluateExpr(resource.Body.Attributes["instance_type"].Expr, &instanceType, nil); err != nil {
t.Fatal(err)
Expand All @@ -584,6 +585,16 @@ resource "aws_instance" "foo" {
if instanceType != test.Want {
t.Fatalf(`"%s" is expected, but got "%s"`, test.Want, instanceType)
}

// callback
if err := runner.EvaluateExpr(resource.Body.Attributes["instance_type"].Expr, func(val string) error {
if instanceType != test.Want {
t.Fatalf(`"%s" is expected, but got "%s"`, test.Want, instanceType)
}
return nil
}, nil); err != nil {
t.Fatal(err)
}
}
})
}
Expand Down
49 changes: 44 additions & 5 deletions plugin/plugin2host/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"os"
"reflect"
"strings"

"github.com/hashicorp/hcl/v2"
Expand Down Expand Up @@ -274,8 +275,46 @@ func (c *GRPCClient) DecodeRuleConfig(name string, ret interface{}) error {
return nil
}

var errRefTy = reflect.TypeOf((*error)(nil)).Elem()

// EvaluateExpr evals the passed expression based on the type.
func (c *GRPCClient) EvaluateExpr(expr hcl.Expression, ret interface{}, opts *tflint.EvaluateExprOption) error {
// Passing a callback function instead of a value as the target will invoke the callback,
// passing the evaluated value to the argument.
func (c *GRPCClient) EvaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error {
var callback bool
rval := reflect.ValueOf(target)
rty := rval.Type()
// Callback must meet the following requirements:
// - It must be a function
// - It must take an argument
// - It must return an error
if rty.Kind() == reflect.Func && rty.NumIn() == 1 && rty.NumOut() == 1 && rty.Out(0).Implements(errRefTy) {
callback = true
target = reflect.New(rty.In(0)).Interface()
}

err := c.evaluateExpr(expr, target, opts)
if !callback {
// error should be handled in the caller
return err
}

if err != nil {
// If it cannot be represented as a Go value, exit without invoking the callback rather than returning an error.
if errors.Is(err, tflint.ErrUnknownValue) || errors.Is(err, tflint.ErrNullValue) || errors.Is(err, tflint.ErrSensitive) || errors.Is(err, tflint.ErrUnevaluable) {
return nil
}
return err
}

rerr := rval.Call([]reflect.Value{reflect.ValueOf(target).Elem()})
if rerr[0].IsNil() {
return nil
}
return rerr[0].Interface().(error)
}

func (c *GRPCClient) evaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error {
if opts == nil {
opts = &tflint.EvaluateExprOption{}
}
Expand All @@ -284,7 +323,7 @@ func (c *GRPCClient) EvaluateExpr(expr hcl.Expression, ret interface{}, opts *tf
if opts.WantType != nil {
ty = *opts.WantType
} else {
switch ret.(type) {
switch target.(type) {
case *string, string:
ty = cty.String
case *int, int:
Expand All @@ -300,7 +339,7 @@ func (c *GRPCClient) EvaluateExpr(expr hcl.Expression, ret interface{}, opts *tf
case cty.Value, *cty.Value:
ty = cty.DynamicPseudoType
default:
panic(fmt.Sprintf("unsupported result type: %T", ret))
panic(fmt.Sprintf("unsupported target type: %T", target))
}
}
tyby, err := json.MarshalType(ty)
Expand Down Expand Up @@ -332,7 +371,7 @@ func (c *GRPCClient) EvaluateExpr(expr hcl.Expression, ret interface{}, opts *tf
}

if ty == cty.DynamicPseudoType {
return gocty.FromCtyValue(val, ret)
return gocty.FromCtyValue(val, target)
}

// Returns an error if the value cannot be decoded to a Go value (e.g. unknown, null, sensitive).
Expand All @@ -356,7 +395,7 @@ func (c *GRPCClient) EvaluateExpr(expr hcl.Expression, ret interface{}, opts *tf
return err
}

return gocty.FromCtyValue(val, ret)
return gocty.FromCtyValue(val, target)
}

// EmitIssue emits the issue with the passed rule, message, location
Expand Down
Loading