interfaces/interface_variable.go (207 lines of code) (raw):

package interfaces import ( "fmt" "github.com/hashicorp/hcl/v2" "github.com/matt-FFFFFF/tfvarcheck/check" "github.com/matt-FFFFFF/tfvarcheck/varcheck" "github.com/terraform-linters/tflint-plugin-sdk/hclext" "github.com/terraform-linters/tflint-plugin-sdk/tflint" "github.com/zclconf/go-cty/cty" ) // variableBodySchema is the schema for the variable block that we want to extract from the config. var variableBodySchema = &hclext.BodySchema{ Blocks: []hclext.BlockSchema{ { Type: "variable", LabelNames: []string{"name"}, Body: &hclext.BodySchema{ Attributes: []hclext.AttributeSchema{ {Name: "type"}, {Name: "default"}, {Name: "nullable"}, }, // We do not do anything with the validation data at the moment. Blocks: []hclext.BlockSchema{ { Type: "validation", Body: &hclext.BodySchema{ Attributes: []hclext.AttributeSchema{ {Name: "condition"}, {Name: "error_message"}, }, }, }, }, }, }, }, } // Check interface compliance with the tflint.Rule. var _ tflint.Rule = new(InterfaceVarCheckRule) // InterfaceVarCheckRule is the struct that represents a rule that // check for the correct usage of an interface. type InterfaceVarCheckRule struct { tflint.DefaultRule AvmInterface // This is the interface we are checking for. } // NewVarCheckRuleFromAvmInterface returns a new rule with the given variable. func NewVarCheckRuleFromAvmInterface(ifce AvmInterface) *InterfaceVarCheckRule { return &InterfaceVarCheckRule{ AvmInterface: ifce, } } // Name returns the rule name. func (vcr *InterfaceVarCheckRule) Name() string { return vcr.RuleName } // Link returns the link to the rule documentation. func (vcr *InterfaceVarCheckRule) Link() string { return vcr.RuleLink } // Enabled returns whether the rule is enabled. func (vcr *InterfaceVarCheckRule) Enabled() bool { return vcr.RuleEnabled } // Severity returns the severity of the rule. func (vcr *InterfaceVarCheckRule) Severity() tflint.Severity { return vcr.RuleSeverity } // Check checks whether the module satisfies the interface. // It will search for a variable with the same name as the interface. // It will check the type, default value and nullable attributes. func (vcr *InterfaceVarCheckRule) Check(r tflint.Runner) error { path, err := r.GetModulePath() if err != nil { return err } if !path.IsRoot() { // This rule does not evaluate child modules. return nil } // Define the schema that we want to pull out of the module content. body, err := r.GetModuleContent( variableBodySchema, &tflint.GetModuleContentOption{ExpandMode: tflint.ExpandModeNone}) if err != nil { return err } // Iterate over the variables and check for the name we are interested in. for _, b := range body.Blocks { if b.Labels[0] != vcr.RuleName { continue } typeAttr, c := CheckWithReturnValue(NewChecker(), getAttr(vcr, r, b, "type")) var defaultAttr *hclext.Attribute if vcr.Default.IsKnown() { defaultAttr, c = CheckWithReturnValue(c, getAttr(vcr, r, b, "default")) } else { c = c.Check(attributeNotExist(vcr, r, b, "default")) } if c = c.Check(checkVarType(vcr, r, typeAttr)). Check(checkDefaultValue(vcr, r, b, defaultAttr)). Check(checkNullableValue(vcr, r, b)); c.err != nil { return c.err } // TODO: Check validation rules. return nil } return nil } func attributeNotExist(vcr *InterfaceVarCheckRule, r tflint.Runner, b *hclext.Block, attrName string) func() (bool, error) { return func() (bool, error) { _, exist := b.Body.Attributes[attrName] if exist { return false, r.EmitIssue(vcr, fmt.Sprintf("`%s` %s should not be declared", b.Labels[0], attrName), b.DefRange) } return true, nil } } // getAttr returns a function that will return the attribute from a given hcl block. // It is designed to be used with the CheckWithReturnValue function. func getAttr(rule tflint.Rule, r tflint.Runner, b *hclext.Block, attrName string) func() (*hclext.Attribute, bool, error) { return func() (*hclext.Attribute, bool, error) { attr, exists := b.Body.Attributes[attrName] if !exists { return attr, false, r.EmitIssue( rule, fmt.Sprintf("`%s` %s not declared", b.Labels[0], attrName), b.DefRange, ) } return attr, true, nil } } // checkNullableValue checks if the nullable attribute is correct. // It is designed to be supplied to the Checker.Check() function. func checkNullableValue(vcr *InterfaceVarCheckRule, r tflint.Runner, b *hclext.Block) func() (bool, error) { return func() (bool, error) { nullableAttr, nullableExists := b.Body.Attributes["nullable"] nullableVal := cty.NullVal(cty.Bool) var diags hcl.Diagnostics if nullableExists { nullableVal, diags = nullableAttr.Expr.Value(nil) } if diags.HasErrors() { return false, diags } // Check nullable attribute. if ok := check.Nullable(nullableVal, vcr.Nullable); ok { return true, nil } msg := "nullable should not be set." if !vcr.Nullable { msg = "nullable should be set to false" } rg := b.DefRange if nullableAttr != nil { rg = nullableAttr.Range } return false, r.EmitIssue(vcr, msg, rg) } } // checkVarType checks if the type of the variable is correct. // It is designed to be supplied to the Checker.Check() function. func checkVarType(vcr *InterfaceVarCheckRule, r tflint.Runner, typeAttr *hclext.Attribute) func() (bool, error) { return func() (bool, error) { // Check if the type interface is correct. gotType, diags := varcheck.NewTypeConstraintWithDefaultsFromExp(typeAttr.Expr) if diags.HasErrors() { return false, diags } if eq := check.EqualTypeConstraints(gotType, vcr.TypeConstraintWithDefs); !eq { return true, r.EmitIssue(vcr, fmt.Sprintf("variable type does not comply with the interface specification:\n\n%s", vcr.VarTypeString), typeAttr.Range, ) } return true, nil } } // checkDefaultValue checks if the default value of a variable is correct. // It is designed to be supplied to the Checker.Check() function. func checkDefaultValue(vcr *InterfaceVarCheckRule, r tflint.Runner, b *hclext.Block, defaultAttr *hclext.Attribute) func() (bool, error) { return func() (bool, error) { // Check if the default value is correct. if !vcr.Default.IsKnown() { if defaultAttr != nil { return true, r.EmitIssue( vcr, fmt.Sprintf("default value should not be set, see: %s", vcr.Link()), defaultAttr.Range, ) } return true, nil } defaultVal, _ := defaultAttr.Expr.Value(nil) if !check.EqualCtyValue(defaultVal, vcr.Default) { return true, r.EmitIssue( vcr, fmt.Sprintf("default value is not correct, see: %s", vcr.Link()), b.DefRange, ) } return true, nil } } // NewChecker is the constructor for the Checker type. func NewChecker() Checker { return Checker{ continueCheck: true, } } // Checker is a struct that is used to chain checks together. type Checker struct { continueCheck bool err error } // Check is a executes a supplied function that returns a bool and an error. // The bool is a continueCheck value that is used to determine if the check should continue. // The error is the error that is returned from the check. // // This function returns a new Checker, so it can be chained with other checks in a fluent style. func (c Checker) Check(check func() (bool, error)) Checker { if c.err != nil || !c.continueCheck { return c } continueCheck, err := check() return Checker{ continueCheck: continueCheck, err: err, } } // CheckWithReturnValue is a generic function that runs a check func() that, as well as // returning a bool & error, also returns a value. // The main function will then return the value and a new Checker with the continueCheck and err. func CheckWithReturnValue[TR any](c Checker, check func() (TR, bool, error)) (ret TR, rc Checker) { if c.err != nil || !c.continueCheck { rc = c return } tr, continueCheck, err := check() return tr, Checker{ continueCheck: continueCheck, err: err, } }