Skip to content

Commit bfbc701

Browse files
committed
Add "expr" type as a schema type
1 parent 8246785 commit bfbc701

File tree

2 files changed

+130
-13
lines changed

2 files changed

+130
-13
lines changed

opa/conversion.go

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7+
"reflect"
78

89
"github.com/hashicorp/hcl/v2"
910
"github.com/hashicorp/hcl/v2/ext/typeexpr"
@@ -24,6 +25,10 @@ var schemaTy = types.NewObject(
2425
types.NewDynamicProperty(types.S, types.A),
2526
)
2627

28+
// Capsule type corresponding to "expr" in the extended schema type syntax.
29+
// It is not intended as a general capsule type in the cty type system, but acts as the identifier for the keyword.
30+
var exprCty cty.Type = cty.Capsule("expr", reflect.TypeOf((*hcl.Expression)(nil)))
31+
2732
func jsonToSchema(in map[string]any, tyMap map[string]cty.Type, path string) (*hclext.BodySchema, map[string]cty.Type, error) {
2833
schema := &hclext.BodySchema{}
2934

@@ -32,13 +37,19 @@ func jsonToSchema(in map[string]any, tyMap map[string]cty.Type, path string) (*h
3237

3338
switch cv := v.(type) {
3439
case string:
35-
expr, diags := hclsyntax.ParseExpression([]byte(cv), "", hcl.InitialPos)
36-
if diags.HasErrors() {
37-
return schema, tyMap, fmt.Errorf("type expr parse error in %s; %s", key, withoutSubject(diags))
38-
}
39-
ty, diags := typeexpr.TypeConstraint(expr)
40-
if diags.HasErrors() {
41-
return schema, tyMap, fmt.Errorf("type constraint parse error in %s; %s", key, withoutSubject(diags))
40+
var ty cty.Type
41+
if cv == "expr" {
42+
// "expr" is a special type that allows you to get the raw expression without evaluating it.
43+
ty = exprCty
44+
} else {
45+
expr, diags := hclsyntax.ParseExpression([]byte(cv), "", hcl.InitialPos)
46+
if diags.HasErrors() {
47+
return schema, tyMap, fmt.Errorf("type expr parse error in %s; %s", key, withoutSubject(diags))
48+
}
49+
ty, diags = typeexpr.TypeConstraint(expr)
50+
if diags.HasErrors() {
51+
return schema, tyMap, fmt.Errorf("type constraint parse error in %s; %s", key, withoutSubject(diags))
52+
}
4253
}
4354
tyMap[key] = ty
4455

@@ -277,7 +288,36 @@ var exprTy = types.NewObject(
277288
nil,
278289
)
279290

291+
// expr (object<expr: string, range: range>) representation of a raw expression
292+
var rawExprTy = types.NewObject(
293+
[]*types.StaticProperty{
294+
types.NewStaticProperty("value", types.S),
295+
types.NewStaticProperty("range", rangeTy),
296+
},
297+
nil,
298+
)
299+
280300
func exprToJSON(expr hcl.Expression, tyMap map[string]cty.Type, path string, runner tflint.Runner) (map[string]any, error) {
301+
ty, exists := tyMap[path]
302+
if !exists {
303+
// should never happen
304+
panic(fmt.Sprintf("cannot get type of %s", path))
305+
}
306+
// For the "expr" type, the expression is not evaluated
307+
// and the "value" is the raw expression syntax.
308+
if ty == exprCty {
309+
file, err := runner.GetFile(expr.Range().Filename)
310+
if err != nil {
311+
return map[string]any{}, fmt.Errorf("type error in %s; %w", expr.Range(), err)
312+
}
313+
// "unknown", "sensitive", and "ephemeral" are undefined
314+
// because the "value" has not been evaluated.
315+
return map[string]any{
316+
"value": string(expr.Range().SliceBytes(file.Bytes)),
317+
"range": rangeToJSON(expr.Range()),
318+
}, nil
319+
}
320+
281321
ret := map[string]any{
282322
"unknown": false,
283323
"sensitive": false,
@@ -311,17 +351,11 @@ func exprToJSON(expr hcl.Expression, tyMap map[string]cty.Type, path string, run
311351
return ret, nil
312352
}
313353

314-
ty, exists := tyMap[path]
315-
if !exists {
316-
// should never happen
317-
panic(fmt.Sprintf("cannot get type of %s", path))
318-
}
319354
if ty.HasDynamicTypes() {
320355
// If a type has "any", it will be converted to JSON as a dynamic type, (e.g. {"value": 1, "type": "number"})
321356
// so it will take advantage of the inferred type.
322357
ty = value.Type()
323358
}
324-
325359
value, err = convert.Convert(value, ty)
326360
if err != nil {
327361
return ret, fmt.Errorf("type error in %s; %w", expr.Range(), err)

opa/functions.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func Functions(runner tflint.Runner) []func(*rego.Rego) {
3232
removedBlocksFunc(runner).asOption(),
3333
ephemeralResourcesFunc(runner).asOption(),
3434
moduleRangeFunc(runner).asOption(),
35+
exprList(runner).asOption(),
3536
issueFunc().asOption(),
3637
}
3738
}
@@ -53,6 +54,7 @@ func TesterFunctions(runner tflint.Runner) []*tester.Builtin {
5354
removedBlocksFunc(runner).asTester(),
5455
ephemeralResourcesFunc(runner).asTester(),
5556
moduleRangeFunc(runner).asTester(),
57+
exprList(runner).asTester(),
5658
issueFunc().asTester(),
5759
}
5860
}
@@ -642,6 +644,56 @@ func moduleRangeFunc(runner tflint.Runner) *functionDyn {
642644
}
643645
}
644646

647+
// hcl.expr_list: exprs = hcl.expr_list(expr)
648+
//
649+
// Extracts a list of expressions from the given static list expression.
650+
// This is equivalent to hcl.ExprList in hashicorp/hcl.
651+
//
652+
// expr (expr) static list expression. Typically this is the value of the attribute retrieved by the terraform functions.
653+
//
654+
// Returns:
655+
//
656+
// exprs (array[expr]) list of expressions
657+
func exprList(runner tflint.Runner) *function1 {
658+
return &function1{
659+
function: function{
660+
Decl: &rego.Function{
661+
Name: "hcl.expr_list",
662+
Decl: types.NewFunction(types.Args(rawExprTy), types.NewArray(nil, rawExprTy)),
663+
Memoize: true,
664+
},
665+
},
666+
Func: func(_ rego.BuiltinContext, exprArg *ast.Term) (*ast.Term, error) {
667+
expr, err := astAsExpr(exprArg)
668+
if err != nil {
669+
return nil, err
670+
}
671+
file, err := runner.GetFile(expr.Range().Filename)
672+
if err != nil {
673+
return nil, fmt.Errorf("cannot get file %s: %w", expr.Range().Filename, err)
674+
}
675+
676+
exprs, diags := hcl.ExprList(expr)
677+
if diags.HasErrors() {
678+
return nil, diags
679+
}
680+
ret := make([]map[string]any, len(exprs))
681+
for i, e := range exprs {
682+
ret[i] = map[string]any{
683+
"value": string(e.Range().SliceBytes(file.Bytes)),
684+
"range": rangeToJSON(e.Range()),
685+
}
686+
}
687+
688+
v, err := ast.InterfaceToValue(ret)
689+
if err != nil {
690+
return nil, err
691+
}
692+
return ast.NewTerm(v), nil
693+
},
694+
}
695+
}
696+
645697
// tflint.issue: issue := tflint.issue(msg, range)
646698
//
647699
// Returns issue object
@@ -810,6 +862,37 @@ func blockFunc(schemaArg *ast.Term, optionArg *ast.Term, blockType string, runne
810862
return ast.NewTerm(v), nil
811863
}
812864

865+
func astAsExpr(v *ast.Term) (hcl.Expression, error) {
866+
var exprMap map[string]any
867+
if err := ast.As(v.Value, &exprMap); err != nil {
868+
return nil, err
869+
}
870+
871+
value, ok := exprMap["value"]
872+
if !ok {
873+
return nil, fmt.Errorf("expr must have a 'value' key")
874+
}
875+
valueStr, ok := value.(string)
876+
if !ok {
877+
return nil, fmt.Errorf("expr value must be a string")
878+
}
879+
880+
rngArg, ok := exprMap["range"]
881+
if !ok {
882+
return nil, fmt.Errorf("expr must have a 'range' key")
883+
}
884+
rng, err := jsonToRange(rngArg, "expr.range")
885+
if err != nil {
886+
return nil, err
887+
}
888+
889+
expr, diags := hclext.ParseExpression([]byte(valueStr), rng.Filename, rng.Start)
890+
if diags.HasErrors() {
891+
return nil, diags
892+
}
893+
return expr, nil
894+
}
895+
813896
type function struct {
814897
Decl *rego.Function
815898
}

0 commit comments

Comments
 (0)