Skip to content

Commit e766385

Browse files
committed
Add rule for finding duplicate keys in map expressions
1 parent 0a16f84 commit e766385

File tree

2 files changed

+257
-0
lines changed

2 files changed

+257
-0
lines changed

rules/terraform_map_duplicate_keys.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package rules
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hashicorp/hcl/v2"
7+
"github.com/hashicorp/hcl/v2/hclsyntax"
8+
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
9+
"github.com/terraform-linters/tflint-ruleset-terraform/project"
10+
"github.com/zclconf/go-cty/cty"
11+
)
12+
13+
// This rule checks for map literals with duplicate keys
14+
type TerraformMapDuplicateKeysRule struct {
15+
tflint.DefaultRule
16+
}
17+
18+
func NewTerraformMapDuplicateKeysRule() *TerraformMapDuplicateKeysRule {
19+
return &TerraformMapDuplicateKeysRule{}
20+
}
21+
22+
func (r *TerraformMapDuplicateKeysRule) Name() string {
23+
return "terraform_map_duplicate_keys"
24+
}
25+
26+
func (r *TerraformMapDuplicateKeysRule) Enabled() bool {
27+
return true
28+
}
29+
30+
func (r *TerraformMapDuplicateKeysRule) Severity() tflint.Severity {
31+
return tflint.WARNING
32+
}
33+
34+
func (r *TerraformMapDuplicateKeysRule) Link() string {
35+
return project.ReferenceLink(r.Name())
36+
}
37+
38+
func (r *TerraformMapDuplicateKeysRule) Check(runner tflint.Runner) error {
39+
path, err := runner.GetModulePath()
40+
if err != nil {
41+
return err
42+
}
43+
if !path.IsRoot() {
44+
// This rule does not evaluate child modules
45+
return nil
46+
}
47+
48+
diags := runner.WalkExpressions(tflint.ExprWalkFunc(func(e hcl.Expression) hcl.Diagnostics {
49+
return r.checkObjectConsExpr(e, runner)
50+
}))
51+
if diags.HasErrors() {
52+
return diags
53+
}
54+
55+
return nil
56+
}
57+
58+
func (r *TerraformMapDuplicateKeysRule) checkObjectConsExpr(e hcl.Expression, runner tflint.Runner) hcl.Diagnostics {
59+
exprMap, ok := e.(*hclsyntax.ObjectConsExpr)
60+
if !ok {
61+
// Ignore everything that isn't an ObjectConsExpr
62+
return nil
63+
}
64+
diags := hcl.Diagnostics{}
65+
foundKeys := make(map[string]hcl.Range)
66+
for _, item := range exprMap.Items {
67+
expr := item.KeyExpr.(*hclsyntax.ObjectConsKeyExpr)
68+
val := cty.Value{}
69+
err := runner.EvaluateExpr(expr, &val, &tflint.EvaluateExprOption{})
70+
if err != nil {
71+
fmt.Printf("Failed to evaluate an expression, continuing\n")
72+
diags.Append(
73+
&hcl.Diagnostic{
74+
Severity: hcl.DiagError,
75+
Summary: "failed to evaluate expression",
76+
Detail: err.Error(),
77+
},
78+
)
79+
continue
80+
}
81+
if !val.IsKnown() || val.IsNull() {
82+
// When trying to evaluate an expression
83+
// with a variable without a default,
84+
// runner.evaluateExpr() returns a null value.
85+
// Ignore this case since there's nothing we can do.
86+
fmt.Printf("Unknown key, continuing\n")
87+
continue
88+
}
89+
90+
if previousRange, exists := foundKeys[val.AsString()]; exists {
91+
msg := fmt.Sprintf("Duplicate key: '%s'\nThe previous definition was at %s", val.AsString(), previousRange)
92+
if err := runner.EmitIssue(r, msg, expr.Range()); err != nil {
93+
diags.Append(
94+
&hcl.Diagnostic{
95+
Severity: hcl.DiagError,
96+
Summary: "failed to call EmitIssue()",
97+
Detail: err.Error(),
98+
},
99+
)
100+
}
101+
} else {
102+
foundKeys[val.AsString()] = expr.Range()
103+
}
104+
}
105+
return diags
106+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package rules
2+
3+
import (
4+
"testing"
5+
6+
"github.com/hashicorp/hcl/v2"
7+
"github.com/terraform-linters/tflint-plugin-sdk/helper"
8+
)
9+
10+
func Test_TerraformMapLiteralChangeNameLater(t *testing.T) {
11+
cases := []struct {
12+
Name string
13+
Content string
14+
Expected helper.Issues
15+
Fixed string
16+
}{
17+
{
18+
Name: "No duplicates",
19+
Content: `
20+
resource "null_resource" "test" {
21+
test = {
22+
a = 1
23+
b = 2
24+
c = 3
25+
}
26+
}`,
27+
Expected: helper.Issues{},
28+
},
29+
{
30+
Name: "duplicate keys in map literal",
31+
Content: `
32+
resource "null_resource" "test" {
33+
triggers = {
34+
a = "b"
35+
a = "c"
36+
}
37+
}`,
38+
Expected: helper.Issues{
39+
{
40+
Rule: NewTerraformMapDuplicateKeysRule(),
41+
Message: "Duplicate key: 'a'\nThe previous definition was at module.tf:4,9-10",
42+
Range: hcl.Range{
43+
Filename: "module.tf",
44+
Start: hcl.Pos{Line: 5, Column: 9},
45+
End: hcl.Pos{Line: 5, Column: 10},
46+
},
47+
},
48+
},
49+
},
50+
{
51+
Name: "Using variables as keys",
52+
Content: `
53+
variable "a" {
54+
type = string
55+
default = "b"
56+
}
57+
58+
resource "null_resource" "test" {
59+
map = {
60+
(var.a) = 5
61+
b = 8
62+
}
63+
}`,
64+
Expected: helper.Issues{
65+
{
66+
Rule: NewTerraformMapDuplicateKeysRule(),
67+
Message: "Duplicate key: 'b'\nThe previous definition was at module.tf:9,4-11",
68+
Range: hcl.Range{
69+
Filename: "module.tf",
70+
Start: hcl.Pos{Line: 10, Column: 4},
71+
End: hcl.Pos{Line: 10, Column: 5},
72+
},
73+
},
74+
},
75+
},
76+
{
77+
Name: "Using a variable as a key without a default",
78+
Content: `
79+
variable "unknown" {
80+
type = string
81+
}
82+
83+
resource "null_resource" "test" {
84+
map = {
85+
x = 8
86+
(var.unknown) = 5
87+
}
88+
}`,
89+
Expected: helper.Issues{},
90+
},
91+
{
92+
Name: "Multiple duplicates in same map",
93+
Content: `
94+
resource "null_resource" "test" {
95+
map = {
96+
a = 7
97+
a = 8
98+
a = 9
99+
}
100+
}`,
101+
Expected: helper.Issues{
102+
{
103+
Rule: NewTerraformMapDuplicateKeysRule(),
104+
Message: "Duplicate key: 'a'\nThe previous definition was at module.tf:4,4-5",
105+
Range: hcl.Range{
106+
Filename: "module.tf",
107+
Start: hcl.Pos{Line: 5, Column: 4},
108+
End: hcl.Pos{Line: 5, Column: 5},
109+
},
110+
},
111+
{
112+
Rule: NewTerraformMapDuplicateKeysRule(),
113+
Message: "Duplicate key: 'a'\nThe previous definition was at module.tf:4,4-5",
114+
Range: hcl.Range{
115+
Filename: "module.tf",
116+
Start: hcl.Pos{Line: 6, Column: 4},
117+
End: hcl.Pos{Line: 6, Column: 5},
118+
},
119+
},
120+
},
121+
},
122+
{
123+
Name: "Using same key in different maps is okay",
124+
Content: `
125+
126+
resource "null_resource" "test" {
127+
map = {
128+
x = 1
129+
}
130+
map2 = {
131+
x = 2
132+
}
133+
}`,
134+
Expected: helper.Issues{},
135+
},
136+
}
137+
138+
rule := NewTerraformMapDuplicateKeysRule()
139+
140+
for _, tc := range cases {
141+
t.Run(tc.Name, func(t *testing.T) {
142+
runner := testRunner(t, map[string]string{"module.tf": tc.Content})
143+
144+
if err := rule.Check(runner); err != nil {
145+
t.Fatalf("Unexpected error occurred: %s", err)
146+
}
147+
148+
helper.AssertIssues(t, tc.Expected, runner.Runner.(*helper.Runner).Issues)
149+
})
150+
}
151+
}

0 commit comments

Comments
 (0)