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

New Checks: S035 through S037 for invalid attribute references #105

Merged
merged 1 commit into from
Mar 19, 2020
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# v0.12.0

BREAKING CHANGES

* helper/astutils: The `IsFunctionParameterType*` functions have been renamed `IsExprType*` to better denote their purpose.

FEATURES

* **New Check:** `S035`: check for `Schema` with invalid `AtLeastOneOf` attribute references
* **New Check:** `S036`: check for `Schema` with invalid `ConflictsWith` attribute references
* **New Check:** `S037`: check for `Schema` with invalid `ExactlyOneOf` attribute references

# v0.11.0

ENHANCEMENTS
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ Standard lint checks are enabled by default in the `tfproviderlint` tool. Opt-in
| [S032](passes/S032/README.md) | check for `Schema` of `Computed` only with `MinItems` configured | AST |
| [S033](passes/S033/README.md) | check for `Schema` of `Computed` only with `StateFunc` configured | AST |
| [S034](passes/S034/README.md) | check for `Schema` that configure `PromoteSingle` | AST |
| [S035](passes/S035/README.md) | check for `Schema` with invalid `AtLeastOneOf` attribute references | AST |
| [S036](passes/S036/README.md) | check for `Schema` with invalid `ConflictsWith` attribute references | AST |
| [S037](passes/S037/README.md) | check for `Schema` with invalid `ExactlyOneOf` attribute references | AST |

### Standard Validation Checks

Expand Down
29 changes: 29 additions & 0 deletions helper/analysisutils/schema_analyzers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package analysisutils

import (
"fmt"

"github.com/bflad/tfproviderlint/passes/commentignore"
"github.com/bflad/tfproviderlint/passes/helper/schema/schemainfo"
"golang.org/x/tools/go/analysis"
)

// SchemaAttributeReferencesAnalyzer returns an Analyzer for fields that use schema attribute references
func SchemaAttributeReferencesAnalyzer(analyzerName string, fieldName string) *analysis.Analyzer {
doc := fmt.Sprintf(`check for Schema with invalid %[2]s references
The %[1]s analyzer ensures schema attribute references in the Schema %[2]s
field use valid syntax. The Terraform Plugin SDK can unit test attribute
references to verify the references against the full schema.
`, analyzerName, fieldName)

return &analysis.Analyzer{
Name: analyzerName,
Doc: doc,
Requires: []*analysis.Analyzer{
commentignore.Analyzer,
schemainfo.Analyzer,
},
Run: SchemaAttributeReferencesRunner(analyzerName, fieldName),
}
}
50 changes: 50 additions & 0 deletions helper/analysisutils/schema_runners.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package analysisutils

import (
"go/ast"

"github.com/bflad/tfproviderlint/helper/astutils"
"github.com/bflad/tfproviderlint/helper/terraformtype/helper/schema"
"github.com/bflad/tfproviderlint/passes/commentignore"
"github.com/bflad/tfproviderlint/passes/helper/schema/schemainfo"
"golang.org/x/tools/go/analysis"
)

// SchemaAttributeReferencesRunner returns an Analyzer runner for fields that use schema attribute references
func SchemaAttributeReferencesRunner(analyzerName string, fieldName string) func(*analysis.Pass) (interface{}, error) {
return func(pass *analysis.Pass) (interface{}, error) {
ignorer := pass.ResultOf[commentignore.Analyzer].(*commentignore.Ignorer)
schemaInfos := pass.ResultOf[schemainfo.Analyzer].([]*schema.SchemaInfo)

for _, schemaInfo := range schemaInfos {
if ignorer.ShouldIgnore(analyzerName, schemaInfo.AstCompositeLit) {
continue
}

if !schemaInfo.DeclaresField(fieldName) {
continue
}

switch value := schemaInfo.Fields[fieldName].Value.(type) {
case *ast.CompositeLit:
if !astutils.IsExprTypeArrayString(value.Type) {
continue
}

for _, elt := range value.Elts {
attributeReference := astutils.ExprStringValue(elt)

if attributeReference == nil {
continue
}

if _, err := schema.ParseAttributeReference(*attributeReference); err != nil {
pass.Reportf(elt.Pos(), "%s: invalid %s attribute reference: %s", analyzerName, fieldName, err)
}
}
}
}

return nil, nil
}
}
40 changes: 40 additions & 0 deletions helper/astutils/expr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package astutils

import (
"go/ast"
)

// IsExprTypeArrayString returns true if the expression matches []string
func IsExprTypeArrayString(e ast.Expr) bool {
arrayType, ok := e.(*ast.ArrayType)

return ok && IsExprTypeString(arrayType.Elt)
}

// IsExprTypeArrayError returns true if the expression matches []error
func IsExprTypeArrayError(e ast.Expr) bool {
arrayType, ok := e.(*ast.ArrayType)

return ok && IsExprTypeError(arrayType.Elt)
}

// IsExprTypeError returns true if the expression matches string
func IsExprTypeError(e ast.Expr) bool {
ident, ok := e.(*ast.Ident)

return ok && ident.Name == "error"
}

// IsExprTypeInterface returns true if the expression matches interface{}
func IsExprTypeInterface(e ast.Expr) bool {
_, ok := e.(*ast.InterfaceType)

return ok
}

// IsExprTypeString returns true if the expression matches string
func IsExprTypeString(e ast.Expr) bool {
ident, ok := e.(*ast.Ident)

return ok && ident.Name == "string"
}
40 changes: 0 additions & 40 deletions helper/astutils/function_parameters.go

This file was deleted.

61 changes: 61 additions & 0 deletions helper/terraformtype/helper/schema/attributes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package schema

import (
"fmt"
"math"
"regexp"
"strconv"
"strings"
)

const (
// Pattern for schema attribute names
AttributeNameRegexpPattern = `^[a-z0-9_]+$`

// Pattern for schema references to attributes, such as ConflictsWith values
AttributeReferenceRegexpPattern = `^[a-z0-9_]+(\.[a-z0-9_]+)*$`
)

var (
AttributeNameRegexp = regexp.MustCompile(AttributeNameRegexpPattern)
AttributeReferenceRegexp = regexp.MustCompile(AttributeReferenceRegexpPattern)
)

// ParseAttributeReference validates and returns the split representation of schema attribute reference.
// Attribute references are used in Schema fields such as AtLeastOneOf, ConflictsWith, and ExactlyOneOf.
func ParseAttributeReference(reference string) ([]string, error) {
if !AttributeReferenceRegexp.MatchString(reference) {
return nil, fmt.Errorf("%q must contain only valid attribute names, separated by periods", reference)
}

attributeReferenceParts := strings.Split(reference, ".")

if len(attributeReferenceParts) == 1 {
return attributeReferenceParts, nil
}

configurationBlockReferenceErr := fmt.Errorf("%q configuration block attribute references are only valid for TypeList and MaxItems: 1 attributes and nested attributes must be separated by .0.", reference)

if math.Mod(float64(len(attributeReferenceParts)), 2) == 0 {
return attributeReferenceParts, configurationBlockReferenceErr
}

// All even parts of an attribute reference must be 0
for idx, attributeReferencePart := range attributeReferenceParts {
if math.Mod(float64(idx), 2) == 0 {
continue
}

attributeReferencePartInt, err := strconv.Atoi(attributeReferencePart)

if err != nil {
return attributeReferenceParts, configurationBlockReferenceErr
}

if attributeReferencePartInt != 0 {
return attributeReferenceParts, configurationBlockReferenceErr
}
}

return attributeReferenceParts, nil
}
5 changes: 1 addition & 4 deletions passes/S015/S015.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package S015

import (
"go/ast"
"regexp"
"strings"

"golang.org/x/tools/go/analysis"
Expand Down Expand Up @@ -37,8 +36,6 @@ func run(pass *analysis.Pass) (interface{}, error) {
ignorer := pass.ResultOf[commentignore.Analyzer].(*commentignore.Ignorer)
schemamapcompositelits := pass.ResultOf[schemamapcompositelit.Analyzer].([]*ast.CompositeLit)

attributeNameRegex := regexp.MustCompile(`^[a-z0-9_]+$`)

for _, smap := range schemamapcompositelits {
if ignorer.ShouldIgnore(analyzerName, smap) {
continue
Expand All @@ -51,7 +48,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
case *ast.BasicLit:
value := strings.Trim(t.Value, `"`)

if !attributeNameRegex.MatchString(value) {
if !schema.AttributeNameRegexp.MatchString(value) {
pass.Reportf(t.Pos(), "%s: schema attribute names should only be lowercase alphanumeric characters or underscores", analyzerName)
}
}
Expand Down
32 changes: 32 additions & 0 deletions passes/S035/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# S035

The S035 analyzer reports cases of Schemas which include `AtLeastOneOf` and have invalid schema attribute references.

NOTE: This only verifies the syntax of attribute references. The Terraform Plugin SDK can unit test attribute references to verify the references against the full schema.

## Flagged Code

```go
&schema.Schema{
AtLeastOneOf: []string{"config_block_attr.nested_attr"},
}
```

## Passing Code

```go
&schema.Schema{
AtLeastOneOf: []string{"config_block_attr.0.nested_attr"},
}
```

## Ignoring Reports

Singular reports can be ignored by adding the a `//lintignore:S035` Go code comment at the end of the offending line or on the line immediately proceding, e.g.

```go
//lintignore:S035
&schema.Schema{
AtLeastOneOf: []string{"config_block_attr.nested_attr"},
}
```
8 changes: 8 additions & 0 deletions passes/S035/S035.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package S035

import (
"github.com/bflad/tfproviderlint/helper/analysisutils"
"github.com/bflad/tfproviderlint/helper/terraformtype/helper/schema"
)

var Analyzer = analysisutils.SchemaAttributeReferencesAnalyzer("S035", schema.SchemaFieldAtLeastOneOf)
12 changes: 12 additions & 0 deletions passes/S035/S035_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package S035

import (
"testing"

"golang.org/x/tools/go/analysis/analysistest"
)

func TestS035(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, Analyzer, "a")
}
21 changes: 21 additions & 0 deletions passes/S035/testdata/src/a/alias.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package a

import (
s "github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)

func falias() {
_ = s.Schema{
AtLeastOneOf: []string{
".invalidreference", // want "invalid AtLeastOneOf attribute reference"
},
}

_ = map[string]*s.Schema{
"name": {
AtLeastOneOf: []string{
".invalidreference", // want "invalid AtLeastOneOf attribute reference"
},
},
}
}
Loading