Skip to content

Commit 91ab697

Browse files
authored
Add hclext.BoundExpr (#205)
1 parent cb843d9 commit 91ab697

File tree

8 files changed

+789
-498
lines changed

8 files changed

+789
-498
lines changed

hclext/expression.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package hclext
2+
3+
import (
4+
"github.com/hashicorp/hcl/v2"
5+
"github.com/zclconf/go-cty/cty"
6+
)
7+
8+
// BoundExpr represents an expression whose a value is bound.
9+
// This is a wrapper for any expression, typically satisfying
10+
// an interface to behave like the wrapped expression.
11+
//
12+
// The difference is that when resolving a value with `Value()`,
13+
// instead of resolving the variables with EvalContext,
14+
// the bound value is returned directly.
15+
type BoundExpr struct {
16+
Val cty.Value
17+
18+
original hcl.Expression
19+
}
20+
21+
var _ hcl.Expression = (*BoundExpr)(nil)
22+
23+
// BindValue binds the passed value to an expression.
24+
// This returns the bound expression.
25+
func BindValue(val cty.Value, expr hcl.Expression) hcl.Expression {
26+
return &BoundExpr{original: expr, Val: val}
27+
}
28+
29+
// Value returns the bound value.
30+
func (e BoundExpr) Value(*hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
31+
return e.Val, nil
32+
}
33+
34+
// Variables delegates to the wrapped expression.
35+
func (e BoundExpr) Variables() []hcl.Traversal {
36+
return e.original.Variables()
37+
}
38+
39+
// Range delegates to the wrapped expression.
40+
func (e BoundExpr) Range() hcl.Range {
41+
return e.original.Range()
42+
}
43+
44+
// StartRange delegates to the wrapped expression.
45+
func (e BoundExpr) StartRange() hcl.Range {
46+
return e.original.StartRange()
47+
}

plugin/fromproto/fromproto.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
99
"github.com/terraform-linters/tflint-plugin-sdk/plugin/proto"
1010
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
11+
"github.com/zclconf/go-cty/cty"
12+
"github.com/zclconf/go-cty/cty/msgpack"
1113
"google.golang.org/grpc/codes"
1214
"google.golang.org/grpc/status"
1315
)
@@ -62,7 +64,13 @@ func BodyContent(body *proto.BodyContent) (*hclext.BodyContent, hcl.Diagnostics)
6264

6365
attributes := hclext.Attributes{}
6466
for key, attr := range body.Attributes {
65-
expr, exprDiags := hclext.ParseExpression(attr.Expr, attr.ExprRange.Filename, Pos(attr.ExprRange.Start))
67+
var expr hcl.Expression
68+
var exprDiags hcl.Diagnostics
69+
if attr.Expression != nil {
70+
expr, exprDiags = Expression(attr.Expression)
71+
} else {
72+
expr, exprDiags = hclext.ParseExpression(attr.Expr, attr.ExprRange.Filename, Pos(attr.ExprRange.Start))
73+
}
6674
diags = diags.Extend(exprDiags)
6775

6876
attributes[key] = &hclext.Attribute{
@@ -146,6 +154,22 @@ func Rule(rule *proto.EmitIssue_Rule) *RuleObject {
146154
}
147155
}
148156

157+
// Expression converts proto.Expression to hcl.Expression
158+
func Expression(expr *proto.Expression) (hcl.Expression, hcl.Diagnostics) {
159+
parsed, diags := hclext.ParseExpression(expr.Bytes, expr.Range.Filename, Pos(expr.Range.Start))
160+
if diags.HasErrors() {
161+
return nil, diags
162+
}
163+
if expr.Value != nil {
164+
val, err := msgpack.Unmarshal(expr.Value, cty.DynamicPseudoType)
165+
if err != nil {
166+
panic(fmt.Errorf("cannot unmarshal the bound expr: %w", err))
167+
}
168+
parsed = hclext.BindValue(val, parsed)
169+
}
170+
return parsed, diags
171+
}
172+
149173
// Severity converts proto.EmitIssue_Severity to severity
150174
func Severity(severity proto.EmitIssue_Severity) tflint.Severity {
151175
switch severity {

plugin/plugin2host/client.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,10 @@ func (c *GRPCClient) EvaluateExpr(expr hcl.Expression, ret interface{}, opts *tf
299299
resp, err := c.Client.EvaluateExpr(
300300
context.Background(),
301301
&proto.EvaluateExpr_Request{
302-
Expr: expr.Range().SliceBytes(file.Bytes),
303-
ExprRange: toproto.Range(expr.Range()),
304-
Option: &proto.EvaluateExpr_Option{Type: tyby, ModuleCtx: toproto.ModuleCtxType(opts.ModuleCtx)},
302+
Expr: expr.Range().SliceBytes(file.Bytes),
303+
ExprRange: toproto.Range(expr.Range()),
304+
Expression: toproto.Expression(expr, file.Bytes),
305+
Option: &proto.EvaluateExpr_Option{Type: tyby, ModuleCtx: toproto.ModuleCtxType(opts.ModuleCtx)},
305306
},
306307
)
307308
if err != nil {

plugin/plugin2host/plugin2host_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,55 @@ volume_size = 10`)
546546
},
547547
ErrCheck: neverHappend,
548548
},
549+
{
550+
Name: "get content with bound expr",
551+
Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) {
552+
return &hclext.BodySchema{
553+
Attributes: []hclext.AttributeSchema{{Name: "value"}},
554+
}, nil
555+
},
556+
ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
557+
file := hclFile("test.tf", "value = each.key")
558+
attrs, diags := file.Body.JustAttributes()
559+
if diags.HasErrors() {
560+
return nil, diags
561+
}
562+
attr := attrs["value"]
563+
564+
return &hclext.BodyContent{
565+
Attributes: hclext.Attributes{
566+
"value": {
567+
Name: attr.Name,
568+
Expr: hclext.BindValue(cty.StringVal("bound value"), attr.Expr),
569+
Range: attr.Range,
570+
NameRange: attr.NameRange,
571+
},
572+
},
573+
Blocks: hclext.Blocks{},
574+
}, nil
575+
},
576+
Want: func(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) {
577+
file := hclFile("test.tf", "value = each.key")
578+
attrs, diags := file.Body.JustAttributes()
579+
if diags.HasErrors() {
580+
return nil, diags
581+
}
582+
attr := attrs["value"]
583+
584+
return &hclext.BodyContent{
585+
Attributes: hclext.Attributes{
586+
"value": {
587+
Name: attr.Name,
588+
Expr: hclext.BindValue(cty.StringVal("bound value"), attr.Expr),
589+
Range: attr.Range,
590+
NameRange: attr.NameRange,
591+
},
592+
},
593+
Blocks: hclext.Blocks{},
594+
}, nil
595+
},
596+
ErrCheck: neverHappend,
597+
},
549598
{
550599
Name: "get content with options",
551600
Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) {
@@ -1537,6 +1586,17 @@ func TestEvaluateExpr(t *testing.T) {
15371586
GetFileImpl: fileExists,
15381587
ErrCheck: neverHappend,
15391588
},
1589+
{
1590+
Name: "bound expr",
1591+
Expr: hclext.BindValue(cty.StringVal("bound value"), hclExpr(`var.foo`)),
1592+
TargetType: reflect.TypeOf(""),
1593+
ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) {
1594+
return evalExpr(expr, &hcl.EvalContext{})
1595+
},
1596+
Want: "bound value",
1597+
GetFileImpl: fileExists,
1598+
ErrCheck: neverHappend,
1599+
},
15401600
{
15411601
Name: "eval with moduleCtx option",
15421602
Expr: hclExpr(`1`),

plugin/plugin2host/server.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,17 +112,32 @@ func (s *GRPCServer) GetRuleConfigContent(ctx context.Context, req *proto.GetRul
112112

113113
// EvaluateExpr evals the passed expression based on the type.
114114
func (s *GRPCServer) EvaluateExpr(ctx context.Context, req *proto.EvaluateExpr_Request) (*proto.EvaluateExpr_Response, error) {
115-
if req.Expr == nil {
116-
return nil, status.Error(codes.InvalidArgument, "expr should not be null")
117-
}
118-
if req.ExprRange == nil {
119-
return nil, status.Error(codes.InvalidArgument, "expr_range should not be null")
115+
if req.Expression == nil {
116+
if req.Expr == nil {
117+
return nil, status.Error(codes.InvalidArgument, "expr should not be null")
118+
}
119+
if req.ExprRange == nil {
120+
return nil, status.Error(codes.InvalidArgument, "expr_range should not be null")
121+
}
122+
} else {
123+
if req.Expression.Bytes == nil {
124+
return nil, status.Error(codes.InvalidArgument, "expression.bytes should not be null")
125+
}
126+
if req.Expression.Range == nil {
127+
return nil, status.Error(codes.InvalidArgument, "expression.range should not be null")
128+
}
120129
}
121130
if req.Option == nil {
122131
return nil, status.Error(codes.InvalidArgument, "option should not be null")
123132
}
124133

125-
expr, diags := hclext.ParseExpression(req.Expr, req.ExprRange.Filename, fromproto.Pos(req.ExprRange.Start))
134+
var expr hcl.Expression
135+
var diags hcl.Diagnostics
136+
if req.Expression != nil {
137+
expr, diags = fromproto.Expression(req.Expression)
138+
} else {
139+
expr, diags = hclext.ParseExpression(req.Expr, req.ExprRange.Filename, fromproto.Pos(req.ExprRange.Start))
140+
}
126141
if diags.HasErrors() {
127142
return nil, toproto.Error(codes.InvalidArgument, diags)
128143
}

0 commit comments

Comments
 (0)