Skip to content

Commit 382e6ae

Browse files
tanelso2bendrucker
andauthored
Add rule for finding duplicate keys in map expressions (#194)
Co-authored-by: Ben Drucker <bvdrucker@gmail.com>
1 parent 77aed47 commit 382e6ae

File tree

2 files changed

+287
-0
lines changed

2 files changed

+287
-0
lines changed

rules/terraform_map_duplicate_keys.go

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

0 commit comments

Comments
 (0)