diff --git a/docs/rules/README.md b/docs/rules/README.md index 1e728be1c..bbf6a7191 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -76,4 +76,5 @@ These rules suggest to better ways. |[terraform_documented_variables](terraform_documented_variables.md)|| |[terraform_typed_variables](terraform_typed_variables.md)|| |[terraform_module_pinned_source](terraform_module_pinned_source.md)|✔| +|[terraform_naming_convention](terraform_naming_convention.md)|| |[terraform_required_version](terraform_required_version.md)|| diff --git a/docs/rules/terraform_naming_convention.md b/docs/rules/terraform_naming_convention.md new file mode 100644 index 000000000..87be6c2f8 --- /dev/null +++ b/docs/rules/terraform_naming_convention.md @@ -0,0 +1,237 @@ +# terraform_naming_convention + +Enforces naming conventions for the following blocks: + +* Resources +* Input variables +* Output values +* Local values +* Modules +* Data sources + +## Configuration + +Name | Default | Value +--- | --- | --- +enabled | `false` | Boolean +format | `snake_case` | `snake_case`, `mixed_snake_case`, `""` +custom | `""` | String representation of a golang regular expression that the block name must match +data | | Block settings to override naming convention for data sources +locals | | Block settings to override naming convention for local values +module | | Block settings to override naming convention for modules +output | | Block settings to override naming convention for output values +resource | | Block settings to override naming convention for resources +variable | | Block settings to override naming convention for input variables + + +#### `format` + +The `format` option defines the allowed formats for the block label. +This option accepts one of the following values: + +* `snake_case` - standard snake_case format - all characters must be lower-case, and underscores are allowed. +* `mixed_snake_case` - modified snake_case format - characters may be upper or lower case, and underscores are allowed. +* `none` - signifies "this block shall not have its format checked". This can be useful if you want to enforce no particular format for a block. + +#### `custom` + +The `custom` option defines a custom regex that the identifier must match. This option allows you to have a +bit more finer-grained control over identifiers, letting you force certain patterns and substrings. + +## Examples + +### Default - enforce snake_case for all blocks + +#### Rule configuration + +``` +rule "terraform_naming_convention" { + enabled = true +} +``` + +#### Sample terraform source file + +```hcl +data "aws_eip" "camelCase" { +} + +data "aws_eip" "valid_name" { +} +``` + +``` +$ tflint +1 issue(s) found: + +Notice: data name `camelCase` must match the following format: snake_case (terraform_naming_convention) + + on template.tf line 1: + 1: data "aws_eip" "camelCase" { + +Reference: https://github.com/terraform-linters/tflint/blob/v0.15.3/docs/rules/terraform_naming_convention.md + +``` + + +### Custom naming expression for all blocks + +#### Rule configuration + +``` +rule "terraform_naming_convention" { + enabled = true + + custom = "^[a-zA-Z]+([_-][a-zA-Z]+)*$" +} +``` + +#### Sample terraform source file + +```hcl +resource "aws_eip" "Invalid_Name_With_Number123" { +} + +resource "aws_eip" "Name-With_Dash" { +} +``` + +``` +$ tflint +1 issue(s) found: + +Notice: resource name `Invalid_Name_With_Number123` must match the following RegExp: ^[a-zA-Z]+([_-][a-zA-Z]+)*$ (terraform_naming_convention) + + on template.tf line 1: + 1: resource "aws_eip" "Invalid_Name_With_Number123" { + +Reference: https://github.com/terraform-linters/tflint/blob/v0.15.3/docs/rules/terraform_naming_convention.md + +``` + + +### Override default setting for specific block type + +#### Rule configuration + +``` +rule "terraform_naming_convention" { + enabled = true + + module { + custom = "^[a-zA-Z]+(_[a-zA-Z]+)*$" + } +} +``` + +#### Sample terraform source file + +```hcl +// data name enforced with default snake_case +data "aws_eip" "eip_1a" { +} + +module "valid_module" { + source = "" +} + +module "invalid_module_with_number_1a" { + source = "" +} +``` + +``` +$ tflint +1 issue(s) found: + +Notice: module name `invalid_module_with_number_1a` must match the following RegExp: ^[a-zA-Z]+(_[a-zA-Z]+)*$ (terraform_naming_convention) + + on template.tf line 9: + 9: module "invalid_module_with_number_1a" { + +Reference: https://github.com/terraform-linters/tflint/blob/v0.15.3/docs/rules/terraform_naming_convention.md + +``` + +### Disable for specific block type + +#### Rule configuration + +``` +rule "terraform_naming_convention" { + enabled = true + + module { + format = "none" + } +} +``` + +#### Sample terraform source file + +```hcl +// data name enforced with default snake_case +data "aws_eip" "eip_1a" { +} + +// module names will not be enforced +module "Valid_Name-Not-Enforced" { + source = "" +} +``` + + +### Disable for all blocks but enforce a specific block type + +#### Rule configuration + +``` +rule "terraform_naming_convention" { + enabled = true + format = "none" + + local { + format = "snake_case" + } +} +``` + +#### Sample terraform source file + +```hcl +// Data block name not enforced +data "aws_eip" "EIP_1a" { +} + +// Resource block name not enforced +resource "aws_eip" "EIP_1b" { +} + +// local variable names enforced +locals { + valid_name = "valid" + invalid-name = "dashes are not allowed with snake_case" +} +``` + +``` +$ tflint +1 issue(s) found: + +Notice: local value name `invalid-name` must match the following format: snake_case (terraform_naming_convention) + + on template.tf line 12: + 12: invalid-name = "dashes are not allowed with snake_case" + +Reference: https://github.com/terraform-linters/tflint/blob/v0.15.3/docs/rules/terraform_naming_convention.md + +``` + +## Why + +Naming conventions are optional, so it is not necessary to follow this. +But this rule is useful if you want to force the following naming conventions in line with the [Terraform Plugin Naming Best Practices](https://www.terraform.io/docs/extend/best-practices/naming.html). + +## How To Fix + +Update the block label according to the format or custom regular expression. diff --git a/rules/provider.go b/rules/provider.go index 1c57aeb10..323cdf489 100644 --- a/rules/provider.go +++ b/rules/provider.go @@ -45,6 +45,7 @@ var manualDefaultRules = []Rule{ terraformrules.NewTerraformDocumentedOutputsRule(), terraformrules.NewTerraformDocumentedVariablesRule(), terraformrules.NewTerraformModulePinnedSourceRule(), + terraformrules.NewTerraformNamingConventionRule(), terraformrules.NewTerraformTypedVariablesRule(), terraformrules.NewTerraformRequiredVersionRule(), } diff --git a/rules/terraformrules/terraform_naming_convention.go b/rules/terraformrules/terraform_naming_convention.go new file mode 100644 index 000000000..ebf0d7bc5 --- /dev/null +++ b/rules/terraformrules/terraform_naming_convention.go @@ -0,0 +1,351 @@ +package terraformrules + +import ( + "fmt" + "log" + "regexp" + "strings" + + "github.com/terraform-linters/tflint/tflint" +) + +// TerraformNamingConventionRule checks whether blocks follow naming convention +type TerraformNamingConventionRule struct{} + +type terraformNamingConventionRuleConfig struct { + Format string `hcl:"format,optional"` + Custom string `hcl:"custom,optional"` + + Data *BlockFormatConfig `hcl:"data,block"` + Locals *BlockFormatConfig `hcl:"locals,block"` + Module *BlockFormatConfig `hcl:"module,block"` + Output *BlockFormatConfig `hcl:"output,block"` + Resource *BlockFormatConfig `hcl:"resource,block"` + Variable *BlockFormatConfig `hcl:"variable,block"` +} + +// BlockFormatConfig defines the pre-defined format or custom regular expression to use +type BlockFormatConfig struct { + Format string `hcl:"format,optional"` + Custom string `hcl:"custom,optional"` +} + +// NameValidator contains the regular expression to validate block name, if it was a named format, and the format name/regular expression string +type NameValidator struct { + Format string + IsNamedFormat bool + Regexp *regexp.Regexp +} + +// NewTerraformNamingConventionRule returns new rule with default attributes +func NewTerraformNamingConventionRule() *TerraformNamingConventionRule { + return &TerraformNamingConventionRule{} +} + +// Name returns the rule name +func (r *TerraformNamingConventionRule) Name() string { + return "terraform_naming_convention" +} + +// Enabled returns whether the rule is enabled by default +func (r *TerraformNamingConventionRule) Enabled() bool { + return false +} + +// Severity returns the rule severity +func (r *TerraformNamingConventionRule) Severity() string { + return tflint.NOTICE +} + +// Link returns the rule reference link +func (r *TerraformNamingConventionRule) Link() string { + return tflint.ReferenceLink(r.Name()) +} + +// Check checks whether blocks follow naming convention +func (r *TerraformNamingConventionRule) Check(runner *tflint.Runner) error { + log.Printf("[TRACE] Check `%s` rule for `%s` runner", r.Name(), runner.TFConfigPath()) + + config := terraformNamingConventionRuleConfig{} + config.Format = "snake_case" + if err := runner.DecodeRuleConfig(r.Name(), &config); err != nil { + return err + } + + defaultNameValidator, err := config.getNameValidator() + if err != nil { + return fmt.Errorf("Invalid default configuration: %v", err) + } + + if err := r.checkDataBlocks(runner, &config, defaultNameValidator); err != nil { + return err + } + + if err := r.checkLocalValues(runner, &config, defaultNameValidator); err != nil { + return err + } + + if err := r.checkModuleBlocks(runner, &config, defaultNameValidator); err != nil { + return err + } + + if err := r.checkOutputBlocks(runner, &config, defaultNameValidator); err != nil { + return err + } + + if err := r.checkResourceBlocks(runner, &config, defaultNameValidator); err != nil { + return err + } + + if err := r.checkVariableBlocks(runner, &config, defaultNameValidator); err != nil { + return err + } + + return nil +} + +func (r *TerraformNamingConventionRule) checkDataBlocks(runner *tflint.Runner, config *terraformNamingConventionRuleConfig, defaultValidator *NameValidator) error { + validator := defaultValidator + if config.Data != nil { + nameValidator, err := config.Data.getNameValidator() + if err != nil { + return fmt.Errorf("Invalid data configuration: %v", err) + } + + validator = nameValidator + } + + if validator != nil { + for _, data := range runner.TFConfig.Module.DataResources { + if !validator.Regexp.MatchString(data.Name) { + var message string + if validator.IsNamedFormat { + message = "data name `%s` must match the following format: %s" + } else { + message = "data name `%s` must match the following RegExp: %s" + } + + runner.EmitIssue( + r, + fmt.Sprintf(message, data.Name, validator.Format), + data.DeclRange, + ) + } + } + } + + return nil +} + +func (r *TerraformNamingConventionRule) checkLocalValues(runner *tflint.Runner, config *terraformNamingConventionRuleConfig, defaultValidator *NameValidator) error { + validator := defaultValidator + if config.Locals != nil { + nameValidator, err := config.Locals.getNameValidator() + if err != nil { + return fmt.Errorf("Invalid locals configuration: %v", err) + } + + validator = nameValidator + } + + if validator != nil { + for _, local := range runner.TFConfig.Module.Locals { + if !validator.Regexp.MatchString(local.Name) { + var message string + if validator.IsNamedFormat { + message = "local value name `%s` must match the following format: %s" + } else { + message = "local value name `%s` must match the following RegExp: %s" + } + + runner.EmitIssue( + r, + fmt.Sprintf(message, local.Name, validator.Format), + local.DeclRange, + ) + } + } + } + + return nil +} + +func (r *TerraformNamingConventionRule) checkModuleBlocks(runner *tflint.Runner, config *terraformNamingConventionRuleConfig, defaultValidator *NameValidator) error { + validator := defaultValidator + if config.Module != nil { + nameValidator, err := config.Module.getNameValidator() + if err != nil { + return fmt.Errorf("Invalid module configuration: %v", err) + } + + validator = nameValidator + } + + if validator != nil { + for _, module := range runner.TFConfig.Module.ModuleCalls { + if !validator.Regexp.MatchString(module.Name) { + var message string + if validator.IsNamedFormat { + message = "module name `%s` must match the following format: %s" + } else { + message = "module name `%s` must match the following RegExp: %s" + } + + runner.EmitIssue( + r, + fmt.Sprintf(message, module.Name, validator.Format), + module.DeclRange, + ) + } + } + } + + return nil +} + +func (r *TerraformNamingConventionRule) checkOutputBlocks(runner *tflint.Runner, config *terraformNamingConventionRuleConfig, defaultValidator *NameValidator) error { + validator := defaultValidator + if config.Output != nil { + nameValidator, err := config.Output.getNameValidator() + if err != nil { + return fmt.Errorf("Invalid output configuration: %v", err) + } + + validator = nameValidator + } + + if validator != nil { + for _, output := range runner.TFConfig.Module.Outputs { + if !validator.Regexp.MatchString(output.Name) { + var message string + if validator.IsNamedFormat { + message = "output name `%s` must match the following format: %s" + } else { + message = "output name `%s` must match the following RegExp: %s" + } + + runner.EmitIssue( + r, + fmt.Sprintf(message, output.Name, validator.Format), + output.DeclRange, + ) + } + } + } + + return nil +} + +func (r *TerraformNamingConventionRule) checkResourceBlocks(runner *tflint.Runner, config *terraformNamingConventionRuleConfig, defaultValidator *NameValidator) error { + validator := defaultValidator + if config.Resource != nil { + nameValidator, err := config.Resource.getNameValidator() + if err != nil { + return fmt.Errorf("Invalid resource configuration: %v", err) + } + + validator = nameValidator + } + + if validator != nil { + for _, resource := range runner.TFConfig.Module.ManagedResources { + if !validator.Regexp.MatchString(resource.Name) { + var message string + if validator.IsNamedFormat { + message = "resource name `%s` must match the following format: %s" + } else { + message = "resource name `%s` must match the following RegExp: %s" + } + + runner.EmitIssue( + r, + fmt.Sprintf(message, resource.Name, validator.Format), + resource.DeclRange, + ) + } + } + } + + return nil +} + +func (r *TerraformNamingConventionRule) checkVariableBlocks(runner *tflint.Runner, config *terraformNamingConventionRuleConfig, defaultValidator *NameValidator) error { + validator := defaultValidator + if config.Variable != nil { + nameValidator, err := config.Variable.getNameValidator() + if err != nil { + return fmt.Errorf("Invalid variable configuration: %v", err) + } + + validator = nameValidator + } + + if validator != nil { + for _, variable := range runner.TFConfig.Module.Variables { + if !validator.Regexp.MatchString(variable.Name) { + var message string + if validator.IsNamedFormat { + message = "variable name `%s` must match the following format: %s" + } else { + message = "variable name `%s` must match the following RegExp: %s" + } + + runner.EmitIssue( + r, + fmt.Sprintf(message, variable.Name, validator.Format), + variable.DeclRange, + ) + } + } + } + + return nil +} + +func (config *BlockFormatConfig) getNameValidator() (*NameValidator, error) { + return getNameValidator(config.Custom, config.Format) +} + +func (config *terraformNamingConventionRuleConfig) getNameValidator() (*NameValidator, error) { + return getNameValidator(config.Custom, config.Format) +} + +var snakeCaseRegex = regexp.MustCompile("^[a-z][a-z0-9]*(_[a-z0-9]+)*$") +var mixedSnakeCaseRegex = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9]*(_[a-zA-Z0-9]+)*$") + +func getNameValidator(custom string, format string) (*NameValidator, error) { + // Prefer custom format if specified + if custom != "" { + customRegex, err := regexp.Compile(custom) + nameValidator := &NameValidator{ + IsNamedFormat: false, + Format: custom, + Regexp: customRegex, + } + + return nameValidator, err + } else if format != "none" { + switch strings.ToLower(format) { + case "snake_case": + nameValidator := &NameValidator{ + IsNamedFormat: true, + Format: format, + Regexp: snakeCaseRegex, + } + + return nameValidator, nil + case "mixed_snake_case": + nameValidator := &NameValidator{ + IsNamedFormat: true, + Format: format, + Regexp: mixedSnakeCaseRegex, + } + + return nameValidator, nil + default: + return nil, fmt.Errorf("`%s` is unsupported format", format) + } + } + + return nil, nil +} diff --git a/rules/terraformrules/terraform_naming_convention_test.go b/rules/terraformrules/terraform_naming_convention_test.go new file mode 100644 index 000000000..df618c4ed --- /dev/null +++ b/rules/terraformrules/terraform_naming_convention_test.go @@ -0,0 +1,2867 @@ +package terraformrules + +import ( + "fmt" + "io/ioutil" + "os" + "testing" + + hcl "github.com/hashicorp/hcl/v2" + "github.com/terraform-linters/tflint/tflint" +) + +// Data blocks +func Test_TerraformNamingConventionRule_Data_DefaultEmpty(t *testing.T) { + testDataSnakeCase(t, "default config", "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true +}`) +} + +func Test_TerraformNamingConventionRule_Data_DefaultFormat(t *testing.T) { + testDataMixedSnakeCase(t, `default config (format="mixed_snake_case")`, ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" +}`) +} + +func Test_TerraformNamingConventionRule_Data_DefaultCustom(t *testing.T) { + testDataSnakeCase(t, `default config (custom="^[a-z_]+$")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^[a-z][a-z]*(_[a-z]+)*$" +}`) +} + +func Test_TerraformNamingConventionRule_Data_DefaultDisabled(t *testing.T) { + testDataDisabled(t, `default config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + format = "none" +}`) +} + +func Test_TerraformNamingConventionRule_Data_DefaultFormat_OverrideFormat(t *testing.T) { + testDataSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" + + data { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Data_DefaultFormat_OverrideCustom(t *testing.T) { + testDataSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" + + data { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Data_DefaultCustom_OverrideFormat(t *testing.T) { + testDataSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^ignored$" + + data { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Data_DefaultCustom_OverrideCustom(t *testing.T) { + testDataSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^ignored$" + + data { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Data_DefaultDisabled_OverrideFormat(t *testing.T) { + testDataSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + format = "none" + + data { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Data_DefaultDisabled_OverrideCustom(t *testing.T) { + testDataSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + format = "none" + + data { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Data_DefaultEmpty_OverrideDisabled(t *testing.T) { + testDataDisabled(t, `overridden config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + + data { + format = "none" + } +}`) +} + +func Test_TerraformNamingConventionRule_Data_DefaultFormat_OverrideDisabled(t *testing.T) { + testDataDisabled(t, `overridden config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + format = "snake_case" + + data { + format = "none" + } +}`) +} + +func testDataSnakeCase(t *testing.T, testType string, formatName string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("data: %s - Invalid snake_case with dash", testType), + Content: ` +data "aws_eip" "dash-name" { +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("data name `dash-name` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 27}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("data: %s - Invalid snake_case with camelCase", testType), + Content: ` +data "aws_eip" "camelCased" { +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("data name `camelCased` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 28}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("data: %s - Invalid snake_case with double underscore", testType), + Content: ` +data "aws_eip" "foo__bar" { +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("data name `foo__bar` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 26}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("data: %s - Invalid snake_case with underscore tail", testType), + Content: ` +data "aws_eip" "foo_bar_" { +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("data name `foo_bar_` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 26}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("data: %s - Invalid snake_case with Mixed_Snake_Case", testType), + Content: ` +data "aws_eip" "Foo_Bar" { +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("data name `Foo_Bar` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 25}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("data: %s - Valid snake_case", testType), + Content: ` +data "aws_eip" "foo_bar" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("data: %s - Valid single word", testType), + Content: ` +data "aws_eip" "foo" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +func testDataMixedSnakeCase(t *testing.T, testType string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("data: %s - Invalid mixed_snake_case with dash", testType), + Content: ` +data "aws_eip" "dash-name" { +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "data name `dash-name` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 27}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("data: %s - Invalid mixed_snake_case with double underscore", testType), + Content: ` +data "aws_eip" "Foo__Bar" { +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "data name `Foo__Bar` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 26}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("data: %s - Invalid mixed_snake_case with underscore tail", testType), + Content: ` +data "aws_eip" "Foo_Bar_" { +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "data name `Foo_Bar_` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 26}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("data: %s - Valid snake_case", testType), + Content: ` +data "aws_eip" "foo_bar" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("data: %s - Valid single word", testType), + Content: ` +data "aws_eip" "foo" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("data: %s - Valid Mixed_Snake_Case", testType), + Content: ` +data "aws_eip" "Foo_Bar" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("data: %s - Valid single word with upper characters", testType), + Content: ` +data "aws_eip" "foo" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("data: %s - Valid PascalCase", testType), + Content: ` +data "aws_eip" "PascalCase" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("data: %s - Valid camelCase", testType), + Content: ` +data "aws_eip" "camelCase" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +func testDataDisabled(t *testing.T, testType string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("data: %s - Valid mixed_snake_case with dash", testType), + Content: ` +data "aws_eip" "dash-name" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("data: %s - Valid snake_case", testType), + Content: ` +data "aws_eip" "foo_bar" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("data: %s - Valid single word", testType), + Content: ` +data "aws_eip" "foo" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("data: %s - Valid Mixed_Snake_Case", testType), + Content: ` +data "aws_eip" "Foo_Bar" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("data: %s - Valid single word upper characters", testType), + Content: ` +data "aws_eip" "Foo" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("data: %s - Valid PascalCase", testType), + Content: ` +data "aws_eip" "PascalCase" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("data: %s - Valid camelCase", testType), + Content: ` +data "aws_eip" "camelCase" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +// Local values +func Test_TerraformNamingConventionRule_Locals_DefaultEmpty(t *testing.T) { + testLocalsSnakeCase(t, "default config", "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true +}`) +} + +func Test_TerraformNamingConventionRule_Locals_DefaultFormat(t *testing.T) { + testLocalsMixedSnakeCase(t, `default config (format="mixed_snake_case")`, ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" +}`) +} + +func Test_TerraformNamingConventionRule_Locals_DefaultCustom(t *testing.T) { + testLocalsSnakeCase(t, `default config (custom="^[a-z_]+$")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^[a-z][a-z]*(_[a-z]+)*$" +}`) +} + +func Test_TerraformNamingConventionRule_Locals_DefaultDisabled(t *testing.T) { + testLocalsDisabled(t, `default config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + format = "none" +}`) +} + +func Test_TerraformNamingConventionRule_Locals_DefaultFormat_OverrideFormat(t *testing.T) { + testLocalsSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" + + locals { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Locals_DefaultFormat_OverrideCustom(t *testing.T) { + testLocalsSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" + + locals { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Locals_DefaultCustom_OverrideFormat(t *testing.T) { + testLocalsSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^ignored$" + + locals { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Locals_DefaultCustom_OverrideCustom(t *testing.T) { + testLocalsSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^ignored$" + + locals { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Locals_DefaultDisabled_OverrideFormat(t *testing.T) { + testLocalsSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + format = "none" + + locals { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Locals_DefaultDisabled_OverrideCustom(t *testing.T) { + testLocalsSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + format = "none" + + locals { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Locals_DefaultEmpty_OverrideDisabled(t *testing.T) { + testLocalsDisabled(t, `overridden config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + + locals { + format = "none" + } +}`) +} + +func Test_TerraformNamingConventionRule_Locals_DefaultFormat_OverrideDisabled(t *testing.T) { + testLocalsDisabled(t, `overridden config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + format = "snake_case" + + locals { + format = "none" + } +}`) +} + +func testLocalsSnakeCase(t *testing.T, testType string, formatName string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("locals: %s - Invalid snake_case with dash", testType), + Content: ` +locals { + dash-name = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("local value name `dash-name` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 3, Column: 3}, + End: hcl.Pos{Line: 3, Column: 24}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("locals: %s - Invalid snake_case with camelCase", testType), + Content: ` +locals { + camelCased = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("local value name `camelCased` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 3, Column: 3}, + End: hcl.Pos{Line: 3, Column: 25}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("locals: %s - Invalid snake_case with double underscore", testType), + Content: ` +locals { + foo__bar = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("local value name `foo__bar` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 3, Column: 3}, + End: hcl.Pos{Line: 3, Column: 23}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("locals: %s - Invalid snake_case with underscore tail", testType), + Content: ` +locals { + foo_bar_ = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("local value name `foo_bar_` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 3, Column: 3}, + End: hcl.Pos{Line: 3, Column: 23}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("locals: %s - Invalid snake_case with Mixed_Snake_Case", testType), + Content: ` +locals { + Foo_Bar = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("local value name `Foo_Bar` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 3, Column: 3}, + End: hcl.Pos{Line: 3, Column: 22}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("locals: %s - Valid snake_case", testType), + Content: ` +locals { + foo_bar = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("locals: %s - Valid single word", testType), + Content: ` +locals { + foo = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +func testLocalsMixedSnakeCase(t *testing.T, testType string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("locals: %s - Invalid mixed_snake_case with dash", testType), + Content: ` +locals { + dash-name = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "local value name `dash-name` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 3, Column: 3}, + End: hcl.Pos{Line: 3, Column: 24}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("locals: %s - Invalid mixed_snake_case with double underscore", testType), + Content: ` +locals { + Foo__Bar = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "local value name `Foo__Bar` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 3, Column: 3}, + End: hcl.Pos{Line: 3, Column: 23}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("locals: %s - Invalid mixed_snake_case with underscore tail", testType), + Content: ` +locals { + Foo_Bar_ = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "local value name `Foo_Bar_` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 3, Column: 3}, + End: hcl.Pos{Line: 3, Column: 23}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("locals: %s - Valid snake_case", testType), + Content: ` +locals { + foo_bar = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("locals: %s - Valid single word", testType), + Content: ` +locals { + foo = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("locals: %s - Valid Mixed_Snake_Case", testType), + Content: ` +locals { + Foo_Bar = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("locals: %s - Valid single word with upper characters", testType), + Content: ` +locals { + Foo = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("locals: %s - Valid PascalCase", testType), + Content: ` +locals { + PascalCase = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("locals: %s - Valid camelCase", testType), + Content: ` +locals { + camelCase = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +func testLocalsDisabled(t *testing.T, testType string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("locals: %s - Valid mixed_snake_case with dash", testType), + Content: ` +locals { + dash-name = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("locals: %s - Valid snake_case", testType), + Content: ` +locals { + foo_bar = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("locals: %s - Valid single word", testType), + Content: ` +locals { + foo = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("locals: %s - Valid Mixed_Snake_Case", testType), + Content: ` +locals { + Foo_Bar = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("locals: %s - Valid single word with upper characters", testType), + Content: ` +locals { + Foo = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("locals: %s - Valid PascalCase", testType), + Content: ` +locals { + PascalCase = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("locals: %s - Valid camelCase", testType), + Content: ` +locals { + camelCase = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +// Module blocks +func Test_TerraformNamingConventionRule_Module_DefaultEmpty(t *testing.T) { + testModuleSnakeCase(t, "default config", "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true +}`) +} + +func Test_TerraformNamingConventionRule_Module_DefaultFormat(t *testing.T) { + testModuleMixedSnakeCase(t, `default config (format="mixed_snake_case")`, ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" +}`) +} + +func Test_TerraformNamingConventionRule_Module_DefaultCustom(t *testing.T) { + testModuleSnakeCase(t, `default config (custom="^[a-z_]+$")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^[a-z][a-z]*(_[a-z]+)*$" +}`) +} + +func Test_TerraformNamingConventionRule_Module_DefaultDisabled(t *testing.T) { + testModuleDisabled(t, `default config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + format = "none" +}`) +} + +func Test_TerraformNamingConventionRule_Module_DefaultFormat_OverrideFormat(t *testing.T) { + testModuleSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" + + module { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Module_DefaultFormat_OverrideCustom(t *testing.T) { + testModuleSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" + + module { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Module_DefaultCustom_OverrideFormat(t *testing.T) { + testModuleSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^ignored$" + + module { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Module_DefaultCustom_OverrideCustom(t *testing.T) { + testModuleSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^ignored$" + + module { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Module_DefaultDisabled_OverrideFormat(t *testing.T) { + testModuleSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + format = "none" + + module { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Module_DefaultDisabled_OverrideCustom(t *testing.T) { + testModuleSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + format = "none" + + module { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Module_DefaultEmpty_OverrideDisabled(t *testing.T) { + testModuleDisabled(t, `overridden config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + + module { + format = "none" + } +}`) +} + +func Test_TerraformNamingConventionRule_Module_DefaultFormat_OverrideDisabled(t *testing.T) { + testModuleDisabled(t, `overridden config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + format = "snake_case" + + module { + format = "none" + } +}`) +} + +func testModuleSnakeCase(t *testing.T, testType string, formatName string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("module: %s - Invalid snake_case with dash", testType), + Content: ` +module "dash-name" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("module name `dash-name` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 19}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("module: %s - Invalid snake_case with camelCase", testType), + Content: ` +module "camelCased" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("module name `camelCased` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 20}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("module: %s - Invalid snake_case with double underscore", testType), + Content: ` +module "foo__bar" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("module name `foo__bar` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 18}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("module: %s - Invalid snake_case with underscore tail", testType), + Content: ` +module "foo_bar_" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("module name `foo_bar_` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 18}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("module: %s - Invalid snake_case with Mixed_Snake_Case", testType), + Content: ` +module "Foo_Bar" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("module name `Foo_Bar` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 17}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("module: %s - Valid snake_case", testType), + Content: ` +module "foo_bar" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("module: %s - Valid single word", testType), + Content: ` +module "foo" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +func testModuleMixedSnakeCase(t *testing.T, testType string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("module: %s - Invalid mixed_snake_case with dash", testType), + Content: ` +module "dash-name" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "module name `dash-name` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 19}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("module: %s - Invalid mixed_snake_case with double underscore", testType), + Content: ` +module "Foo__Bar" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "module name `Foo__Bar` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 18}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("module: %s - Invalid mixed_snake_case with underscore tail", testType), + Content: ` +module "Foo_Bar_" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "module name `Foo_Bar_` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 18}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("module: %s - Valid snake_case", testType), + Content: ` +module "foo_bar" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("module: %s - Valid single word", testType), + Content: ` +module "foo" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("module: %s - Valid Mixed_Snake_Case", testType), + Content: ` +module "Foo_Bar" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("module: %s - Valid single word with upper characters", testType), + Content: ` +module "foo" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("module: %s - Valid PascalCase", testType), + Content: ` +module "PascalCase" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("module: %s - Valid camelCase", testType), + Content: ` +module "camelCase" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +func testModuleDisabled(t *testing.T, testType string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("module: %s - Valid mixed_snake_case with dash", testType), + Content: ` +module "dash-name" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("module: %s - Valid snake_case", testType), + Content: ` +module "foo_bar" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("module: %s - Valid single word", testType), + Content: ` +module "foo" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("module: %s - Valid Mixed_Snake_Case", testType), + Content: ` +module "Foo_Bar" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("module: %s - Valid single word upper characters", testType), + Content: ` +module "Foo" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("module: %s - Valid PascalCase", testType), + Content: ` +module "PascalCase" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("module: %s - Valid camelCase", testType), + Content: ` +module "camelCase" { + source = "" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +// Output blocks +func Test_TerraformNamingConventionRule_Output_DefaultEmpty(t *testing.T) { + testOutputSnakeCase(t, "default config", "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true +}`) +} + +func Test_TerraformNamingConventionRule_Output_DefaultFormat(t *testing.T) { + testOutputMixedSnakeCase(t, `default config (format="mixed_snake_case")`, ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" +}`) +} + +func Test_TerraformNamingConventionRule_Output_DefaultCustom(t *testing.T) { + testOutputSnakeCase(t, `default config (custom="^[a-z_]+$")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^[a-z][a-z]*(_[a-z]+)*$" +}`) +} + +func Test_TerraformNamingConventionRule_Output_DefaultDisabled(t *testing.T) { + testOutputDisabled(t, `default config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + format = "none" +}`) +} + +func Test_TerraformNamingConventionRule_Output_DefaultFormat_OverrideFormat(t *testing.T) { + testOutputSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" + + output { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Output_DefaultFormat_OverrideCustom(t *testing.T) { + testOutputSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" + + output { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Output_DefaultCustom_OverrideFormat(t *testing.T) { + testOutputSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^ignored$" + + output { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Output_DefaultCustom_OverrideCustom(t *testing.T) { + testOutputSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^ignored$" + + output { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Output_DefaultDisabled_OverrideFormat(t *testing.T) { + testOutputSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + format = "none" + + output { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Output_DefaultDisabled_OverrideCustom(t *testing.T) { + testOutputSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + format = "none" + + output { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Output_DefaultEmpty_OverrideDisabled(t *testing.T) { + testOutputDisabled(t, `overridden config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + + output { + format = "none" + } +}`) +} + +func Test_TerraformNamingConventionRule_Output_DefaultFormat_OverrideDisabled(t *testing.T) { + testOutputDisabled(t, `overridden config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + format = "snake_case" + + output { + format = "none" + } +}`) +} + +func testOutputSnakeCase(t *testing.T, testType string, formatName string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("output: %s - Invalid snake_case with dash", testType), + Content: ` +output "dash-name" { + value = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("output name `dash-name` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 19}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("output: %s - Invalid snake_case with camelCase", testType), + Content: ` +output "camelCased" { + value = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("output name `camelCased` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 20}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("output: %s - Invalid snake_case with double underscore", testType), + Content: ` +output "foo__bar" { + value = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("output name `foo__bar` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 18}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("output: %s - Invalid snake_case with underscore tail", testType), + Content: ` +output "foo_bar_" { + value = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("output name `foo_bar_` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 18}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("output: %s - Invalid snake_case with Mixed_Snake_Case", testType), + Content: ` +output "Foo_Bar" { + value = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("output name `Foo_Bar` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 17}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("output: %s - Valid snake_case", testType), + Content: ` +output "foo_bar" { + value = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("output: %s - Valid single word", testType), + Content: ` +output "foo" { + value = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +func testOutputMixedSnakeCase(t *testing.T, testType string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("output: %s - Invalid mixed_snake_case with dash", testType), + Content: ` +output "dash-name" { + value = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "output name `dash-name` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 19}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("output: %s - Invalid mixed_snake_case with double underscore", testType), + Content: ` +output "Foo__Bar" { + value = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "output name `Foo__Bar` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 18}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("output: %s - Invalid mixed_snake_case with underscore tail", testType), + Content: ` +output "Foo_Bar_" { + value = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "output name `Foo_Bar_` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 18}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("output: %s - Valid snake_case", testType), + Content: ` +output "foo_bar" { + value = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("output: %s - Valid single word", testType), + Content: ` +output "foo" { + value = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("output: %s - Valid Mixed_Snake_Case", testType), + Content: ` +output "Foo_Bar" { + value = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("output: %s - Valid single word with upper characters", testType), + Content: ` +output "foo" { + value = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("output: %s - Valid PascalCase", testType), + Content: ` +output "PascalCase" { + value = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("output: %s - Valid camelCase", testType), + Content: ` +output "camelCase" { + value = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +func testOutputDisabled(t *testing.T, testType string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("output: %s - Valid mixed_snake_case with dash", testType), + Content: ` +output "dash-name" { + value = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("output: %s - Valid snake_case", testType), + Content: ` +output "foo_bar" { + value = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("output: %s - Valid single word", testType), + Content: ` +output "foo" { + value = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("output: %s - Valid Mixed_Snake_Case", testType), + Content: ` +output "Foo_Bar" { + value = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("output: %s - Valid single word upper characters", testType), + Content: ` +output "Foo" { + value = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("output: %s - Valid PascalCase", testType), + Content: ` +output "PascalCase" { + value = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("output: %s - Valid camelCase", testType), + Content: ` +output "camelCase" { + value = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +// Resource blocks +func Test_TerraformNamingConventionRule_Resource_DefaultEmpty(t *testing.T) { + testResourceSnakeCase(t, "default config", "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true +}`) +} + +func Test_TerraformNamingConventionRule_Resource_DefaultFormat(t *testing.T) { + testResourceMixedSnakeCase(t, `default config (format="mixed_snake_case")`, ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" +}`) +} + +func Test_TerraformNamingConventionRule_Resource_DefaultCustom(t *testing.T) { + testResourceSnakeCase(t, `default config (custom="^[a-z_]+$")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^[a-z][a-z]*(_[a-z]+)*$" +}`) +} + +func Test_TerraformNamingConventionRule_Resource_DefaultDisabled(t *testing.T) { + testResourceDisabled(t, `default config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + format = "none" +}`) +} + +func Test_TerraformNamingConventionRule_Resource_DefaultFormat_OverrideFormat(t *testing.T) { + testResourceSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" + + resource { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Resource_DefaultFormat_OverrideCustom(t *testing.T) { + testResourceSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" + + resource { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Resource_DefaultCustom_OverrideFormat(t *testing.T) { + testResourceSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^ignored$" + + resource { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Resource_DefaultCustom_OverrideCustom(t *testing.T) { + testResourceSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^ignored$" + + resource { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Resource_DefaultDisabled_OverrideFormat(t *testing.T) { + testResourceSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + format = "none" + + resource { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Resource_DefaultDisabled_OverrideCustom(t *testing.T) { + testResourceSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + format = "none" + + resource { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Resource_DefaultEmpty_OverrideDisabled(t *testing.T) { + testResourceDisabled(t, `overridden config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + + resource { + format = "none" + } +}`) +} + +func Test_TerraformNamingConventionRule_Resource_DefaultFormat_OverrideDisabled(t *testing.T) { + testResourceDisabled(t, `overridden config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + format = "snake_case" + + resource { + format = "none" + } +}`) +} + +func testResourceSnakeCase(t *testing.T, testType string, formatName string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("resource: %s - Invalid snake_case with dash", testType), + Content: ` +resource "aws_eip" "dash-name" { +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("resource name `dash-name` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 31}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("resource: %s - Invalid snake_case with camelCase", testType), + Content: ` +resource "aws_eip" "camelCased" { +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("resource name `camelCased` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 32}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("resource: %s - Invalid snake_case with double underscore", testType), + Content: ` +resource "aws_eip" "foo__bar" { +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("resource name `foo__bar` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 30}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("resource: %s - Invalid snake_case with underscore tail", testType), + Content: ` +resource "aws_eip" "foo_bar_" { +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("resource name `foo_bar_` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 30}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("resource: %s - Invalid snake_case with Mixed_Snake_Case", testType), + Content: ` +resource "aws_eip" "Foo_Bar" { +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("resource name `Foo_Bar` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 29}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("resource: %s - Valid snake_case", testType), + Content: ` +resource "aws_eip" "foo_bar" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("resource: %s - Valid single word", testType), + Content: ` +resource "aws_eip" "foo" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +func testResourceMixedSnakeCase(t *testing.T, testType string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("resource: %s - Invalid mixed_snake_case with dash", testType), + Content: ` +resource "aws_eip" "dash-name" { +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "resource name `dash-name` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 31}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("resource: %s - Invalid mixed_snake_case with double underscore", testType), + Content: ` +resource "aws_eip" "Foo__Bar" { +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "resource name `Foo__Bar` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 30}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("resource: %s - Invalid mixed_snake_case with underscore tail", testType), + Content: ` +resource "aws_eip" "Foo_Bar_" { +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "resource name `Foo_Bar_` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 30}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("resource: %s - Valid snake_case", testType), + Content: ` +resource "aws_eip" "foo_bar" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("resource: %s - Valid single word", testType), + Content: ` +resource "aws_eip" "foo" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("resource: %s - Valid Mixed_Snake_Case", testType), + Content: ` +resource "aws_eip" "Foo_Bar" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("resource: %s - Valid single word with upper characters", testType), + Content: ` +resource "aws_eip" "foo" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("resource: %s - Valid PascalCase", testType), + Content: ` +resource "aws_eip" "PascalCase" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("resource: %s - Valid camelCase", testType), + Content: ` +resource "aws_eip" "camelCase" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +func testResourceDisabled(t *testing.T, testType string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("resource: %s - Valid mixed_snake_case with dash", testType), + Content: ` +resource "aws_eip" "dash-name" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("resource: %s - Valid snake_case", testType), + Content: ` +resource "aws_eip" "foo_bar" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("resource: %s - Valid single word", testType), + Content: ` +resource "aws_eip" "foo" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("resource: %s - Valid Mixed_Snake_Case", testType), + Content: ` +resource "aws_eip" "Foo_Bar" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("resource: %s - Valid single word upper characters", testType), + Content: ` +resource "aws_eip" "Foo" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("resource: %s - Valid PascalCase", testType), + Content: ` +resource "aws_eip" "PascalCase" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("resource: %s - Valid camelCase", testType), + Content: ` +resource "aws_eip" "camelCase" { +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +// Variable blocks +func Test_TerraformNamingConventionRule_Variable_DefaultEmpty(t *testing.T) { + testVariableSnakeCase(t, "default config", "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true +}`) +} + +func Test_TerraformNamingConventionRule_Variable_DefaultFormat(t *testing.T) { + testVariableMixedSnakeCase(t, `default config (format="mixed_snake_case")`, ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" +}`) +} + +func Test_TerraformNamingConventionRule_Variable_DefaultCustom(t *testing.T) { + testVariableSnakeCase(t, `default config (custom="^[a-z_]+$")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^[a-z][a-z]*(_[a-z]+)*$" +}`) +} + +func Test_TerraformNamingConventionRule_Variable_DefaultDisabled(t *testing.T) { + testVariableDisabled(t, `default config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + format = "none" +}`) +} + +func Test_TerraformNamingConventionRule_Variable_DefaultFormat_OverrideFormat(t *testing.T) { + testVariableSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" + + variable { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Variable_DefaultFormat_OverrideCustom(t *testing.T) { + testVariableSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + format = "mixed_snake_case" + + variable { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Variable_DefaultCustom_OverrideFormat(t *testing.T) { + testVariableSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^ignored$" + + variable { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Variable_DefaultCustom_OverrideCustom(t *testing.T) { + testVariableSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + custom = "^ignored$" + + variable { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Variable_DefaultDisabled_OverrideFormat(t *testing.T) { + testVariableSnakeCase(t, `overridden config (format="snake_case")`, "format: snake_case", ` +rule "terraform_naming_convention" { + enabled = true + format = "none" + + variable { + format = "snake_case" + } +}`) +} + +func Test_TerraformNamingConventionRule_Variable_DefaultDisabled_OverrideCustom(t *testing.T) { + testVariableSnakeCase(t, `overridden config (format="snake_case")`, "RegExp: ^[a-z][a-z]*(_[a-z]+)*$", ` +rule "terraform_naming_convention" { + enabled = true + format = "none" + + variable { + custom = "^[a-z][a-z]*(_[a-z]+)*$" + } +}`) +} + +func Test_TerraformNamingConventionRule_Variable_DefaultEmpty_OverrideDisabled(t *testing.T) { + testVariableDisabled(t, `overridden config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + + variable { + format = "none" + } +}`) +} + +func Test_TerraformNamingConventionRule_Variable_DefaultFormat_OverrideDisabled(t *testing.T) { + testVariableDisabled(t, `overridden config (format=null)`, ` +rule "terraform_naming_convention" { + enabled = true + format = "snake_case" + + variable { + format = "none" + } +}`) +} + +func testVariableSnakeCase(t *testing.T, testType string, formatName string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("variable: %s - Invalid snake_case with dash", testType), + Content: ` +variable "dash-name" { + description = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("variable name `dash-name` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 21}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("variable: %s - Invalid snake_case with camelCase", testType), + Content: ` +variable "camelCased" { + description = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("variable name `camelCased` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 22}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("variable: %s - Invalid snake_case with double underscore", testType), + Content: ` +variable "foo__bar" { + description = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("variable name `foo__bar` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 20}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("variable: %s - Invalid snake_case with underscore tail", testType), + Content: ` +variable "foo_bar_" { + description = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("variable name `foo_bar_` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 20}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("variable: %s - Invalid snake_case with Mixed_Snake_Case", testType), + Content: ` +variable "Foo_Bar" { + description = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: fmt.Sprintf("variable name `Foo_Bar` must match the following %s", formatName), + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 19}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("variable: %s - Valid snake_case", testType), + Content: ` +variable "foo_bar" { + description = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("variable: %s - Valid single word", testType), + Content: ` +variable "foo" { + description = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +func testVariableMixedSnakeCase(t *testing.T, testType string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("variable: %s - Invalid mixed_snake_case with dash", testType), + Content: ` +variable "dash-name" { + description = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "variable name `dash-name` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 21}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("variable: %s - Invalid mixed_snake_case with double underscore", testType), + Content: ` +variable "Foo__Bar" { + description = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "variable name `Foo__Bar` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 20}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("variable: %s - Invalid mixed_snake_case with underscore tail", testType), + Content: ` +variable "Foo_Bar_" { + description = "invalid" +}`, + Config: config, + Expected: tflint.Issues{ + { + Rule: rule, + Message: "variable name `Foo_Bar_` must match the following format: mixed_snake_case", + Range: hcl.Range{ + Filename: "tests.tf", + Start: hcl.Pos{Line: 2, Column: 1}, + End: hcl.Pos{Line: 2, Column: 20}, + }, + }, + }, + }, + { + Name: fmt.Sprintf("variable: %s - Valid snake_case", testType), + Content: ` +variable "foo_bar" { + description = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("variable: %s - Valid single word", testType), + Content: ` +variable "foo" { + description = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("variable: %s - Valid Mixed_Snake_Case", testType), + Content: ` +variable "Foo_Bar" { + description = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("variable: %s - Valid single word with upper characters", testType), + Content: ` +variable "foo" { + description = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("variable: %s - Valid PascalCase", testType), + Content: ` +variable "PascalCase" { + description = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("variable: %s - Valid camelCase", testType), + Content: ` +variable "camelCase" { + description = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +func testVariableDisabled(t *testing.T, testType string, config string) { + rule := NewTerraformNamingConventionRule() + + cases := []struct { + Name string + Content string + Config string + Expected tflint.Issues + }{ + { + Name: fmt.Sprintf("variable: %s - Valid mixed_snake_case with dash", testType), + Content: ` +variable "dash-name" { + description = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("variable: %s - Valid snake_case", testType), + Content: ` +variable "foo_bar" { + description = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("variable: %s - Valid single word", testType), + Content: ` +variable "foo" { + description = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("variable: %s - Valid Mixed_Snake_Case", testType), + Content: ` +variable "Foo_Bar" { + description = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("variable: %s - Valid single word upper characters", testType), + Content: ` +variable "Foo" { + description = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("variable: %s - Valid PascalCase", testType), + Content: ` +variable "PascalCase" { + description = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + { + Name: fmt.Sprintf("variable: %s - Valid camelCase", testType), + Content: ` +variable "camelCase" { + description = "valid" +}`, + Config: config, + Expected: tflint.Issues{}, + }, + } + + for _, tc := range cases { + runner := tflint.TestRunnerWithConfig(t, map[string]string{"tests.tf": tc.Content}, loadConfigFromNamingConventionTempFile(t, tc.Config)) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + tflint.AssertIssues(t, tc.Expected, runner.Issues) + } +} + +// TODO: Replace with TestRunner +func loadConfigFromNamingConventionTempFile(t *testing.T, content string) *tflint.Config { + if content == "" { + return tflint.EmptyConfig() + } + + tmpfile, err := ioutil.TempFile("", "terraform_naming_convention") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile.Name()) + + if _, err := tmpfile.Write([]byte(content)); err != nil { + t.Fatal(err) + } + config, err := tflint.LoadConfig(tmpfile.Name()) + if err != nil { + t.Fatal(err) + } + return config +}