Skip to content

Commit 8f09608

Browse files
authored
Add support for function callbacks as the target of EvaluateExpr (#246)
1 parent f5d1807 commit 8f09608

File tree

5 files changed

+394
-28
lines changed

5 files changed

+394
-28
lines changed

helper/runner.go

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package helper
22

33
import (
4+
"errors"
45
"fmt"
56
"os"
7+
"reflect"
68

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

201+
var errRefTy = reflect.TypeOf((*error)(nil)).Elem()
202+
199203
// EvaluateExpr returns a value of the passed expression.
200204
// Note that some features are limited
201-
func (r *Runner) EvaluateExpr(expr hcl.Expression, ret interface{}, opts *tflint.EvaluateExprOption) error {
205+
func (r *Runner) EvaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error {
206+
var callback bool
207+
rval := reflect.ValueOf(target)
208+
rty := rval.Type()
209+
// Callback must meet the following requirements:
210+
// - It must be a function
211+
// - It must take an argument
212+
// - It must return an error
213+
if rty.Kind() == reflect.Func && rty.NumIn() == 1 && rty.NumOut() == 1 && rty.Out(0).Implements(errRefTy) {
214+
callback = true
215+
target = reflect.New(rty.In(0)).Interface()
216+
}
217+
218+
err := r.evaluateExpr(expr, target, opts)
219+
if !callback {
220+
// error should be handled in the caller
221+
return err
222+
}
223+
224+
if err != nil {
225+
// If it cannot be represented as a Go value, exit without invoking the callback rather than returning an error.
226+
if errors.Is(err, tflint.ErrUnknownValue) || errors.Is(err, tflint.ErrNullValue) || errors.Is(err, tflint.ErrSensitive) || errors.Is(err, tflint.ErrUnevaluable) {
227+
return nil
228+
}
229+
return err
230+
}
231+
232+
rerr := rval.Call([]reflect.Value{reflect.ValueOf(target).Elem()})
233+
if rerr[0].IsNil() {
234+
return nil
235+
}
236+
return rerr[0].Interface().(error)
237+
}
238+
239+
func (r *Runner) evaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error {
202240
if opts == nil {
203241
opts = &tflint.EvaluateExprOption{}
204242
}
@@ -207,7 +245,7 @@ func (r *Runner) EvaluateExpr(expr hcl.Expression, ret interface{}, opts *tflint
207245
if opts.WantType != nil {
208246
ty = *opts.WantType
209247
} else {
210-
switch ret.(type) {
248+
switch target.(type) {
211249
case *string, string:
212250
ty = cty.String
213251
case *int, int:
@@ -223,7 +261,7 @@ func (r *Runner) EvaluateExpr(expr hcl.Expression, ret interface{}, opts *tflint
223261
case cty.Value, *cty.Value:
224262
ty = cty.DynamicPseudoType
225263
default:
226-
return fmt.Errorf("unsupported result type: %T", ret)
264+
return fmt.Errorf("unsupported target type: %T", target)
227265
}
228266
}
229267

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

254-
return gocty.FromCtyValue(val, ret)
292+
return gocty.FromCtyValue(val, target)
255293
}
256294

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

267305
// EnsureNoError is a method that simply runs a function if there is no error.
268306
//
269-
// Deprecated: Use errors.Is() instead to determine which errors can be ignored.
307+
// Deprecated: Use EvaluateExpr with a function callback. e.g. EvaluateExpr(expr, func (val T) error {}, ...)
270308
func (r *Runner) EnsureNoError(err error, proc func() error) error {
271309
if err == nil {
272310
return proc()

helper/runner_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ resource "aws_instance" "foo" {
576576
}
577577

578578
for _, resource := range resources.Blocks {
579+
// raw value
579580
var instanceType string
580581
if err := runner.EvaluateExpr(resource.Body.Attributes["instance_type"].Expr, &instanceType, nil); err != nil {
581582
t.Fatal(err)
@@ -584,6 +585,16 @@ resource "aws_instance" "foo" {
584585
if instanceType != test.Want {
585586
t.Fatalf(`"%s" is expected, but got "%s"`, test.Want, instanceType)
586587
}
588+
589+
// callback
590+
if err := runner.EvaluateExpr(resource.Body.Attributes["instance_type"].Expr, func(val string) error {
591+
if instanceType != test.Want {
592+
t.Fatalf(`"%s" is expected, but got "%s"`, test.Want, instanceType)
593+
}
594+
return nil
595+
}, nil); err != nil {
596+
t.Fatal(err)
597+
}
587598
}
588599
})
589600
}

plugin/plugin2host/client.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"os"
8+
"reflect"
89
"strings"
910

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

278+
var errRefTy = reflect.TypeOf((*error)(nil)).Elem()
279+
277280
// EvaluateExpr evals the passed expression based on the type.
278-
func (c *GRPCClient) EvaluateExpr(expr hcl.Expression, ret interface{}, opts *tflint.EvaluateExprOption) error {
281+
// Passing a callback function instead of a value as the target will invoke the callback,
282+
// passing the evaluated value to the argument.
283+
func (c *GRPCClient) EvaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error {
284+
var callback bool
285+
rval := reflect.ValueOf(target)
286+
rty := rval.Type()
287+
// Callback must meet the following requirements:
288+
// - It must be a function
289+
// - It must take an argument
290+
// - It must return an error
291+
if rty.Kind() == reflect.Func && rty.NumIn() == 1 && rty.NumOut() == 1 && rty.Out(0).Implements(errRefTy) {
292+
callback = true
293+
target = reflect.New(rty.In(0)).Interface()
294+
}
295+
296+
err := c.evaluateExpr(expr, target, opts)
297+
if !callback {
298+
// error should be handled in the caller
299+
return err
300+
}
301+
302+
if err != nil {
303+
// If it cannot be represented as a Go value, exit without invoking the callback rather than returning an error.
304+
if errors.Is(err, tflint.ErrUnknownValue) || errors.Is(err, tflint.ErrNullValue) || errors.Is(err, tflint.ErrSensitive) || errors.Is(err, tflint.ErrUnevaluable) {
305+
return nil
306+
}
307+
return err
308+
}
309+
310+
rerr := rval.Call([]reflect.Value{reflect.ValueOf(target).Elem()})
311+
if rerr[0].IsNil() {
312+
return nil
313+
}
314+
return rerr[0].Interface().(error)
315+
}
316+
317+
func (c *GRPCClient) evaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error {
279318
if opts == nil {
280319
opts = &tflint.EvaluateExprOption{}
281320
}
@@ -284,7 +323,7 @@ func (c *GRPCClient) EvaluateExpr(expr hcl.Expression, ret interface{}, opts *tf
284323
if opts.WantType != nil {
285324
ty = *opts.WantType
286325
} else {
287-
switch ret.(type) {
326+
switch target.(type) {
288327
case *string, string:
289328
ty = cty.String
290329
case *int, int:
@@ -300,7 +339,7 @@ func (c *GRPCClient) EvaluateExpr(expr hcl.Expression, ret interface{}, opts *tf
300339
case cty.Value, *cty.Value:
301340
ty = cty.DynamicPseudoType
302341
default:
303-
panic(fmt.Sprintf("unsupported result type: %T", ret))
342+
panic(fmt.Sprintf("unsupported target type: %T", target))
304343
}
305344
}
306345
tyby, err := json.MarshalType(ty)
@@ -332,7 +371,7 @@ func (c *GRPCClient) EvaluateExpr(expr hcl.Expression, ret interface{}, opts *tf
332371
}
333372

334373
if ty == cty.DynamicPseudoType {
335-
return gocty.FromCtyValue(val, ret)
374+
return gocty.FromCtyValue(val, target)
336375
}
337376

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

359-
return gocty.FromCtyValue(val, ret)
398+
return gocty.FromCtyValue(val, target)
360399
}
361400

362401
// EmitIssue emits the issue with the passed rule, message, location

0 commit comments

Comments
 (0)