From 2a7b8580ffe80933158e3c4c99f49658e2a1aaba Mon Sep 17 00:00:00 2001 From: magodo Date: Sun, 12 Apr 2020 15:06:11 +0800 Subject: [PATCH 1/2] New Check: S038 for invalid attribute reference semantically (AtLeastOneOf) --- helper/analysisutils/schema_analyzers.go | 28 +++++- helper/analysisutils/schema_runners.go | 74 ++++++++++++++- .../terraformtype/helper/schema/attributes.go | 40 +++++++++ passes/S035/S035.go | 2 +- passes/S036/S036.go | 2 +- passes/S037/S037.go | 2 +- passes/S038/README.md | 86 ++++++++++++++++++ passes/S038/S038.go | 8 ++ passes/S038/S038_test.go | 12 +++ passes/S038/testdata/src/a/alias.go | 26 ++++++ passes/S038/testdata/src/a/main.go | 90 +++++++++++++++++++ passes/S038/testdata/src/a/vendor | 1 + passes/checks.go | 2 + 13 files changed, 363 insertions(+), 10 deletions(-) create mode 100644 passes/S038/README.md create mode 100644 passes/S038/S038.go create mode 100644 passes/S038/S038_test.go create mode 100644 passes/S038/testdata/src/a/alias.go create mode 100644 passes/S038/testdata/src/a/main.go create mode 120000 passes/S038/testdata/src/a/vendor diff --git a/helper/analysisutils/schema_analyzers.go b/helper/analysisutils/schema_analyzers.go index 396148ce..8e839c90 100644 --- a/helper/analysisutils/schema_analyzers.go +++ b/helper/analysisutils/schema_analyzers.go @@ -2,15 +2,16 @@ package analysisutils import ( "fmt" + "github.com/bflad/tfproviderlint/passes/helper/schema/resourceinfo" "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 +// SchemaAttributeReferencesSyntaxAnalyzer returns an Analyzer for fields that use schema attribute references +func SchemaAttributeReferencesSyntaxAnalyzer(analyzerName string, fieldName string) *analysis.Analyzer { + doc := fmt.Sprintf(`check for Schema %[2]s references with invalid syntax 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 @@ -24,6 +25,25 @@ references to verify the references against the full schema. commentignore.Analyzer, schemainfo.Analyzer, }, - Run: SchemaAttributeReferencesRunner(analyzerName, fieldName), + Run: SchemaAttributeReferencesSyntaxRunner(analyzerName, fieldName), +} +} + +// SchemaAttributeReferencesSemanticsAnalyzer returns an Analyzer for fields that use schema attribute references +func SchemaAttributeReferencesSemanticsAnalyzer(analyzerName string, fieldName string) *analysis.Analyzer { + doc := fmt.Sprintf(`check for Schema %[2]s references with invalid semantics + +The %[1]s analyzer ensures schema attribute references in the Schema %[2]s +field refers to valid attribute. +`, analyzerName, fieldName) + + return &analysis.Analyzer{ + Name: analyzerName, + Doc: doc, + Requires: []*analysis.Analyzer{ + commentignore.Analyzer, + resourceinfo.Analyzer, + }, + Run: SchemaAttributeReferencesSemanticsRunner(analyzerName, fieldName), } } diff --git a/helper/analysisutils/schema_runners.go b/helper/analysisutils/schema_runners.go index fde4b9ab..d184bbe7 100644 --- a/helper/analysisutils/schema_runners.go +++ b/helper/analysisutils/schema_runners.go @@ -1,6 +1,7 @@ package analysisutils import ( + "github.com/bflad/tfproviderlint/passes/helper/schema/resourceinfo" "go/ast" "github.com/bflad/tfproviderlint/helper/astutils" @@ -10,8 +11,8 @@ import ( "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) { +// SchemaAttributeReferencesSyntaxRunner returns an Analyzer runner for fields that use schema attribute references +func SchemaAttributeReferencesSyntaxRunner(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) @@ -39,7 +40,74 @@ func SchemaAttributeReferencesRunner(analyzerName string, fieldName string) func } if _, err := schema.ParseAttributeReference(*attributeReference); err != nil { - pass.Reportf(elt.Pos(), "%s: invalid %s attribute reference: %s", analyzerName, fieldName, err) + pass.Reportf(elt.Pos(), "%s: invalid %s attribute reference syntax: %s", analyzerName, fieldName, err) + } + } + } + } + + return nil, nil + } +} + +// SchemaAttributeReferencesSemanticsRunner returns an Analyzer runner for fields that use schema attribute references +func SchemaAttributeReferencesSemanticsRunner(analyzerName string, fieldName string) func(*analysis.Pass) (interface{}, error) { + return func(pass *analysis.Pass) (interface{}, error) { + ignorer := pass.ResultOf[commentignore.Analyzer].(*commentignore.Ignorer) + resourceInfos := pass.ResultOf[resourceinfo.Analyzer].([]*schema.ResourceInfo) + + for _, resourceInfo := range resourceInfos { + // Handle "root" schema.Resource only since we will use the resourceInfo of that to validate attribute reference. + if !resourceInfo.IsDataSource() && !resourceInfo.IsResource() { + continue + } + + // Collection schemaInfo under current resourceInfo + var schemaInfos []*schema.SchemaInfo + ast.Inspect(resourceInfo.AstCompositeLit, func(n ast.Node) bool { + compositeLit, ok := n.(*ast.CompositeLit) + + if !ok { + return true + } + + if schema.IsMapStringSchema(compositeLit, pass.TypesInfo) { + for _, mapSchema := range schema.GetSchemaMapSchemas(compositeLit) { + schemaInfos = append(schemaInfos, schema.NewSchemaInfo(mapSchema, pass.TypesInfo)) + } + } else if schema.IsTypeSchema(pass.TypesInfo.TypeOf(compositeLit.Type)) { + schemaInfos = append(schemaInfos, schema.NewSchemaInfo(compositeLit, pass.TypesInfo)) + } + + return true + }) + + // Validate each schemaInfo under current resourceInfo + 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.ValidateAttributeReference(resourceInfo.Resource, *attributeReference); err != nil { + pass.Reportf(elt.Pos(), "%s: invalid %s attribute reference semantics: %s", analyzerName, fieldName, err) + } } } } diff --git a/helper/terraformtype/helper/schema/attributes.go b/helper/terraformtype/helper/schema/attributes.go index 8888276a..4835c15e 100644 --- a/helper/terraformtype/helper/schema/attributes.go +++ b/helper/terraformtype/helper/schema/attributes.go @@ -2,6 +2,7 @@ package schema import ( "fmt" + tfschema "github.com/hashicorp/terraform-plugin-sdk/helper/schema" "math" "regexp" "strconv" @@ -59,3 +60,42 @@ func ParseAttributeReference(reference string) ([]string, error) { return attributeReferenceParts, nil } + +// ValidateAttributeReference validates schema attribute reference. +// Attribute references are used in Schema fields such as AtLeastOneOf, ConflictsWith, and ExactlyOneOf. +func ValidateAttributeReference(tfresource *tfschema.Resource, reference string) ([]string, error) { + var curSchemaOrResource interface{} = tfresource + attributeReferenceParts := strings.Split(reference, ".") + for idx, attributeReferencePart := range attributeReferenceParts { + attributeReference := strings.Join(attributeReferenceParts[:idx+1], ".") + if math.Mod(float64(idx), 2) == 1 { + // For odd part, ensure it is a 0 and the containing schema has `MaxItems` set to `1`. This is because + // reference among multiple instances of the same nested block is not supported in current plugin SDK. + attributeReferencePartInt, err := strconv.Atoi(attributeReferencePart) + + configurationBlockReferenceErr := fmt.Errorf("%q configuration block attribute references must be separated by .0", attributeReference) + if err != nil { + return nil, configurationBlockReferenceErr + } + if attributeReferencePartInt != 0 { + return nil, configurationBlockReferenceErr + } + + curSchema := curSchemaOrResource.(*tfschema.Schema) + if curSchema.MaxItems != 1 || curSchema.Type != tfschema.TypeList { + return nil, fmt.Errorf("%q configuration block attribute references are only valid for TypeList and MaxItems: 1 attributes", attributeReference) + } + curSchemaOrResource = curSchema.Elem + } else { + // For even part, ensure it references to defined attribute + schema := curSchemaOrResource.(*tfschema.Resource).Schema[attributeReferencePart] + if schema == nil { + return nil, fmt.Errorf("%q references to unknown attribute", attributeReference) + } + curSchemaOrResource = schema + } + } + + return attributeReferenceParts, nil +} + diff --git a/passes/S035/S035.go b/passes/S035/S035.go index 48a57473..1327a9d9 100644 --- a/passes/S035/S035.go +++ b/passes/S035/S035.go @@ -5,4 +5,4 @@ import ( "github.com/bflad/tfproviderlint/helper/terraformtype/helper/schema" ) -var Analyzer = analysisutils.SchemaAttributeReferencesAnalyzer("S035", schema.SchemaFieldAtLeastOneOf) +var Analyzer = analysisutils.SchemaAttributeReferencesSyntaxAnalyzer("S035", schema.SchemaFieldAtLeastOneOf) diff --git a/passes/S036/S036.go b/passes/S036/S036.go index 8121a618..1276298d 100644 --- a/passes/S036/S036.go +++ b/passes/S036/S036.go @@ -5,4 +5,4 @@ import ( "github.com/bflad/tfproviderlint/helper/terraformtype/helper/schema" ) -var Analyzer = analysisutils.SchemaAttributeReferencesAnalyzer("S036", schema.SchemaFieldConflictsWith) +var Analyzer = analysisutils.SchemaAttributeReferencesSyntaxAnalyzer("S036", schema.SchemaFieldConflictsWith) diff --git a/passes/S037/S037.go b/passes/S037/S037.go index aa9ebe5e..4d0ab1b2 100644 --- a/passes/S037/S037.go +++ b/passes/S037/S037.go @@ -5,4 +5,4 @@ import ( "github.com/bflad/tfproviderlint/helper/terraformtype/helper/schema" ) -var Analyzer = analysisutils.SchemaAttributeReferencesAnalyzer("S037", schema.SchemaFieldExactlyOneOf) +var Analyzer = analysisutils.SchemaAttributeReferencesSyntaxAnalyzer("S037", schema.SchemaFieldExactlyOneOf) diff --git a/passes/S038/README.md b/passes/S038/README.md new file mode 100644 index 00000000..b7b1d54f --- /dev/null +++ b/passes/S038/README.md @@ -0,0 +1,86 @@ +# S038 + +The S038 analyzer reports cases of Schemas which include `ConflictsWith` and have invalid schema attribute references. + +NOTE: This pass only works with Terraform resources that are fully defined in a single function. + +## Flagged Code + +```go +// Attribute reference in multi nested block is not supported +&schema.Resource{ + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeList, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + AtLeastOneOf: []string{"x.0.bar"}, + }, + "bar": { + AtLeastOneOf: []string{"x.0.foo"}, + }, + }, + }, + }, + }, +} +``` + +or + +```go +// Non-existed attribute reference +&schema.Resource{ + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + AtLeastOneOf: []string{"x.1.bar"}, + }, + "bar": { + AtLeastOneOf: []string{"x.1.foo"}, + }, + }, + }, + }, + }, +} +``` + +## Passing Code + +```go +&schema.Resource{ + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + AtLeastOneOf: []string{"x.0.bar"}, + }, + "bar": { + AtLeastOneOf: []string{"x.0.foo"}, + }, + }, + }, + }, + }, +} +``` + +## Ignoring Reports + +Singular reports can be ignored by adding the a `//lintignore:S038` Go code comment at the end of the offending line or on the line immediately proceding, e.g. + +```go +//lintignore:S038 +&schema.Resource{ + // ... +} +``` diff --git a/passes/S038/S038.go b/passes/S038/S038.go new file mode 100644 index 00000000..d11d6029 --- /dev/null +++ b/passes/S038/S038.go @@ -0,0 +1,8 @@ +package S038 + +import ( + "github.com/bflad/tfproviderlint/helper/analysisutils" + "github.com/bflad/tfproviderlint/helper/terraformtype/helper/schema" +) + +var Analyzer = analysisutils.SchemaAttributeReferencesSemanticsAnalyzer("S038", schema.SchemaFieldAtLeastOneOf) diff --git a/passes/S038/S038_test.go b/passes/S038/S038_test.go new file mode 100644 index 00000000..970737ce --- /dev/null +++ b/passes/S038/S038_test.go @@ -0,0 +1,12 @@ +package S038 + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" +) + +func TestS038(t *testing.T) { + testdata := analysistest.TestData() + analysistest.Run(t, testdata, Analyzer, "a") +} diff --git a/passes/S038/testdata/src/a/alias.go b/passes/S038/testdata/src/a/alias.go new file mode 100644 index 00000000..0f9c2851 --- /dev/null +++ b/passes/S038/testdata/src/a/alias.go @@ -0,0 +1,26 @@ +package a + +import ( + s "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func falias() { + _ = &s.Resource{ + Read: func(*s.ResourceData, interface{}) error { return nil }, + Schema: map[string]*s.Schema{ + "x": { + Type: s.TypeList, + Elem: &s.Resource{ + Schema: map[string]*s.Schema{ + "foo": { + AtLeastOneOf: []string{"x.0.bar"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "x.0" configuration block attribute references are only valid for TypeList and MaxItems: 1 attributes` + }, + "bar": { + AtLeastOneOf: []string{"x.0.foo"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "x.0" configuration block attribute references are only valid for TypeList and MaxItems: 1 attributes` + }, + }, + }, + }, + }, + } +} diff --git a/passes/S038/testdata/src/a/main.go b/passes/S038/testdata/src/a/main.go new file mode 100644 index 00000000..13295956 --- /dev/null +++ b/passes/S038/testdata/src/a/main.go @@ -0,0 +1,90 @@ +package a + +import "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + +func f() { + // Comment ignored + + //lintignore:S038 + _ = &schema.Resource{ + Read: func(*schema.ResourceData, interface{}) error { return nil }, + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeList, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + AtLeastOneOf: []string{"x.0.bar"}, + }, + "bar": { + AtLeastOneOf: []string{"x.0.foo"}, + }, + }, + }, + }, + }, + } + // Failing + + _ = &schema.Resource{ + Read: func(*schema.ResourceData, interface{}) error { return nil }, + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeList, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + AtLeastOneOf: []string{"x.0.bar"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "x.0" configuration block attribute references are only valid for TypeList and MaxItems: 1 attributes` + }, + "bar": { + AtLeastOneOf: []string{"x.0.foo"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "x.0" configuration block attribute references are only valid for TypeList and MaxItems: 1 attributes` + }, + }, + }, + }, + }, + } + + _ = &schema.Resource{ + Read: func(*schema.ResourceData, interface{}) error { return nil }, + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + AtLeastOneOf: []string{"x.1.bar"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "x.1" configuration block attribute references must be separated by .0` + }, + "bar": { + AtLeastOneOf: []string{"x.1.foo"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "x.1" configuration block attribute references must be separated by .0` + }, + }, + }, + }, + }, + } + + // Passing + + _ = &schema.Resource{ + Read: func(*schema.ResourceData, interface{}) error { return nil }, + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + AtLeastOneOf: []string{"x.0.bar"}, + }, + "bar": { + AtLeastOneOf: []string{"x.0.foo"}, + }, + }, + }, + }, + }, + } + +} diff --git a/passes/S038/testdata/src/a/vendor b/passes/S038/testdata/src/a/vendor new file mode 120000 index 00000000..776800d2 --- /dev/null +++ b/passes/S038/testdata/src/a/vendor @@ -0,0 +1 @@ +../../../../../vendor \ No newline at end of file diff --git a/passes/checks.go b/passes/checks.go index e42e9570..53aff344 100644 --- a/passes/checks.go +++ b/passes/checks.go @@ -60,6 +60,7 @@ import ( "github.com/bflad/tfproviderlint/passes/S035" "github.com/bflad/tfproviderlint/passes/S036" "github.com/bflad/tfproviderlint/passes/S037" + "github.com/bflad/tfproviderlint/passes/S038" "github.com/bflad/tfproviderlint/passes/V001" "github.com/bflad/tfproviderlint/passes/V002" "github.com/bflad/tfproviderlint/passes/V003" @@ -134,6 +135,7 @@ var AllChecks = []*analysis.Analyzer{ S035.Analyzer, S036.Analyzer, S037.Analyzer, + S038.Analyzer, V001.Analyzer, V002.Analyzer, V003.Analyzer, From a97547bb44327f6791e6b1213fd879acb3e42d53 Mon Sep 17 00:00:00 2001 From: magodo Date: Wed, 15 Apr 2020 10:57:23 +0800 Subject: [PATCH 2/2] More test, type assertion (avoid crash), change func signature 1. Add more test cases for different scenarios, both pass and failure cases. 2. Add type assertion to avoid crash on unepxected schema declarations. 3. Modify the signature of `ValidateAttributeReference()` to only return the validation resutl. --- helper/analysisutils/schema_runners.go | 2 +- .../terraformtype/helper/schema/attributes.go | 38 ++- passes/S038/README.md | 6 +- passes/S038/testdata/src/a/main.go | 261 +++++++++++++++++- 4 files changed, 288 insertions(+), 19 deletions(-) diff --git a/helper/analysisutils/schema_runners.go b/helper/analysisutils/schema_runners.go index d184bbe7..7c2ce6f4 100644 --- a/helper/analysisutils/schema_runners.go +++ b/helper/analysisutils/schema_runners.go @@ -105,7 +105,7 @@ func SchemaAttributeReferencesSemanticsRunner(analyzerName string, fieldName str continue } - if _, err := schema.ValidateAttributeReference(resourceInfo.Resource, *attributeReference); err != nil { + if err := schema.ValidateAttributeReference(resourceInfo.Resource, *attributeReference); err != nil { pass.Reportf(elt.Pos(), "%s: invalid %s attribute reference semantics: %s", analyzerName, fieldName, err) } } diff --git a/helper/terraformtype/helper/schema/attributes.go b/helper/terraformtype/helper/schema/attributes.go index 4835c15e..235c82b9 100644 --- a/helper/terraformtype/helper/schema/attributes.go +++ b/helper/terraformtype/helper/schema/attributes.go @@ -61,9 +61,9 @@ func ParseAttributeReference(reference string) ([]string, error) { return attributeReferenceParts, nil } -// ValidateAttributeReference validates schema attribute reference. +// ValidateAttributeReference validates schema attribute reference semantically. // Attribute references are used in Schema fields such as AtLeastOneOf, ConflictsWith, and ExactlyOneOf. -func ValidateAttributeReference(tfresource *tfschema.Resource, reference string) ([]string, error) { +func ValidateAttributeReference(tfresource *tfschema.Resource, reference string) error { var curSchemaOrResource interface{} = tfresource attributeReferenceParts := strings.Split(reference, ".") for idx, attributeReferencePart := range attributeReferenceParts { @@ -75,27 +75,39 @@ func ValidateAttributeReference(tfresource *tfschema.Resource, reference string) configurationBlockReferenceErr := fmt.Errorf("%q configuration block attribute references must be separated by .0", attributeReference) if err != nil { - return nil, configurationBlockReferenceErr + return configurationBlockReferenceErr } if attributeReferencePartInt != 0 { - return nil, configurationBlockReferenceErr + return configurationBlockReferenceErr } - curSchema := curSchemaOrResource.(*tfschema.Schema) - if curSchema.MaxItems != 1 || curSchema.Type != tfschema.TypeList { - return nil, fmt.Errorf("%q configuration block attribute references are only valid for TypeList and MaxItems: 1 attributes", attributeReference) + switch curSchema := curSchemaOrResource.(type) { + case *tfschema.Schema: + if curSchema.MaxItems != 1 || curSchema.Type != tfschema.TypeList { + return fmt.Errorf("%q configuration block attribute references are only valid for TypeList and MaxItems: 1 attributes", attributeReference) + } + curSchemaOrResource = curSchema.Elem + default: + return nil } - curSchemaOrResource = curSchema.Elem } else { // For even part, ensure it references to defined attribute - schema := curSchemaOrResource.(*tfschema.Resource).Schema[attributeReferencePart] - if schema == nil { - return nil, fmt.Errorf("%q references to unknown attribute", attributeReference) + switch curResource := curSchemaOrResource.(type) { + case *tfschema.Resource: + if curResource.Schema == nil { + return fmt.Errorf("%q references resource attribute without schema", attributeReference) + } + schema := curResource.Schema[attributeReferencePart] + if schema == nil { + return fmt.Errorf("%q references to unknown attribute", attributeReference) + } + curSchemaOrResource = schema + default: + return nil } - curSchemaOrResource = schema } } - return attributeReferenceParts, nil + return nil } diff --git a/passes/S038/README.md b/passes/S038/README.md index b7b1d54f..69e93742 100644 --- a/passes/S038/README.md +++ b/passes/S038/README.md @@ -1,13 +1,13 @@ # S038 -The S038 analyzer reports cases of Schemas which include `ConflictsWith` and have invalid schema attribute references. +The S038 analyzer reports cases of Schemas which include `AtLeastOneOf` and have invalid schema attribute references. -NOTE: This pass only works with Terraform resources that are fully defined in a single function. +NOTE: This pass only works with `schema.Resource` that are wholly declared. ## Flagged Code ```go -// Attribute reference in multi nested block is not supported +// Attribute reference in configuration block with potentially more than one element is not supported &schema.Resource{ Schema: map[string]*schema.Schema{ "x": { diff --git a/passes/S038/testdata/src/a/main.go b/passes/S038/testdata/src/a/main.go index 13295956..03fb4cad 100644 --- a/passes/S038/testdata/src/a/main.go +++ b/passes/S038/testdata/src/a/main.go @@ -24,8 +24,10 @@ func f() { }, }, } + // Failing + // Configuration block has not specified `MaxItems: 1` _ = &schema.Resource{ Read: func(*schema.ResourceData, interface{}) error { return nil }, Schema: map[string]*schema.Schema{ @@ -45,6 +47,28 @@ func f() { }, } + // Configuration block' type is not `TypeList` + _ = &schema.Resource{ + Read: func(*schema.ResourceData, interface{}) error { return nil }, + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeSet, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + AtLeastOneOf: []string{"x.0.bar"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "x.0" configuration block attribute references are only valid for TypeList and MaxItems: 1 attributes` + }, + "bar": { + AtLeastOneOf: []string{"x.0.foo"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "x.0" configuration block attribute references are only valid for TypeList and MaxItems: 1 attributes` + }, + }, + }, + }, + }, + } + + // Invalid reference: reference inside configuration block (not 0th block) _ = &schema.Resource{ Read: func(*schema.ResourceData, interface{}) error { return nil }, Schema: map[string]*schema.Schema{ @@ -56,17 +80,133 @@ func f() { "foo": { AtLeastOneOf: []string{"x.1.bar"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "x.1" configuration block attribute references must be separated by .0` }, - "bar": { - AtLeastOneOf: []string{"x.1.foo"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "x.1" configuration block attribute references must be separated by .0` + }, + }, + }, + }, + } + + // Invalid reference: reference inside configuration block (non-existed attribute) + _ = &schema.Resource{ + Read: func(*schema.ResourceData, interface{}) error { return nil }, + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + AtLeastOneOf: []string{"x.0.bar"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "x.0.bar" references to unknown attribute` + }, + }, + }, + }, + }, + } + + // Invalid reference: reference inside configuration block (deep) + _ = &schema.Resource{ + Read: func(*schema.ResourceData, interface{}) error { return nil }, + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "y": { + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + AtLeastOneOf: []string{"x.0.y.0.bar"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "x.0.y.0.bar" references to unknown attribute` + }, + }, + }, + }, + }, + }, + }, + }, + } + + // Invalid reference: root attribute reference non-existing root reference + _ = &schema.Resource{ + Read: func(*schema.ResourceData, interface{}) error { return nil }, + Schema: map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + AtLeastOneOf: []string{"bar"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "bar" references to unknown attribute` + }, + }, + } + + // Invalid reference: root attribute referencing existing configuration block attribute with non-existing nested attribute + _ = &schema.Resource{ + Read: func(*schema.ResourceData, interface{}) error { return nil }, + Schema: map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + AtLeastOneOf: []string{"x.0.bar"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "x.0.bar" references to unknown attribute` + }, + "x": { + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{}, + }, + }, + }, + } + + // Invalid reference: configuration block nested attribute referencing non-existing root attribute + _ = &schema.Resource{ + Read: func(*schema.ResourceData, interface{}) error { return nil }, + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + AtLeastOneOf: []string{"bar"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "bar" references to unknown attribute` + }, + }, + }, + }, + }, + } + + // Invalid reference: configuration block nested attribute referencing existing configuration block attribute with non-existing nested attribute + _ = &schema.Resource{ + Read: func(*schema.ResourceData, interface{}) error { return nil }, + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + AtLeastOneOf: []string{"y.0.bar"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "y.0.bar" references to unknown attribute` }, }, }, }, + "y": { + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{}, + }, + }, }, } // Passing + // Valid reference: reference inside configuration block _ = &schema.Resource{ Read: func(*schema.ResourceData, interface{}) error { return nil }, Schema: map[string]*schema.Schema{ @@ -87,4 +227,121 @@ func f() { }, } + // Valid reference: reference inside configuration block (deep) + _ = &schema.Resource{ + Read: func(*schema.ResourceData, interface{}) error { return nil }, + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "y": { + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + AtLeastOneOf: []string{"x.0.y.0.bar"}, + }, + "bar": { + AtLeastOneOf: []string{"x.0.y.0.foo"}, + }, + }, + }, + }, + }, + }, + }, + }, + } + + // Valid reference: root attribute reference root reference + _ = &schema.Resource{ + Read: func(*schema.ResourceData, interface{}) error { return nil }, + Schema: map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + AtLeastOneOf: []string{"bar"}, + }, + "bar": { + Type: schema.TypeString, + }, + }, + } + + // Valid reference: root attribute referencing existing configuration block attribute with existing nested attribute + _ = &schema.Resource{ + Read: func(*schema.ResourceData, interface{}) error { return nil }, + Schema: map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + AtLeastOneOf: []string{"x.0.bar"}, + }, + "x": { + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "bar": { + Type: schema.TypeString, + }, + }, + }, + }, + }, + } + + // Valid reference: configuration block nested attribute referencing existing root attribute + _ = &schema.Resource{ + Read: func(*schema.ResourceData, interface{}) error { return nil }, + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + AtLeastOneOf: []string{"bar"}, + }, + }, + }, + }, + "bar": { + Type: schema.TypeString, + }, + }, + } + + // Valid reference: configuration block nested attribute referencing existing configuration block attribute with existing nested attribute + _ = &schema.Resource{ + Read: func(*schema.ResourceData, interface{}) error { return nil }, + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + AtLeastOneOf: []string{"y.0.bar"}, + }, + }, + }, + }, + "y": { + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "bar": { + Type: schema.TypeString, + }, + }, + }, + }, + }, + } + }