Skip to content

Commit 95054a5

Browse files
authored
feat: detect unused provider aliases (#304)
1 parent 081802d commit 95054a5

File tree

3 files changed

+242
-9
lines changed

3 files changed

+242
-9
lines changed

docs/rules/terraform_unused_declarations.md

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# terraform_unused_declarations
22

3-
Disallow variables, data sources, and locals that are declared but never used.
3+
Disallow variables, data sources, locals, and provider aliases that are declared but never used.
44

55
> This rule is enabled by "recommended" preset.
66
@@ -28,13 +28,54 @@ Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0
2828
2929
```
3030

31+
Provider aliases example:
32+
33+
```hcl
34+
provider "azurerm" {
35+
features {}
36+
alias = "test_123"
37+
subscription_id = ""
38+
}
39+
40+
resource "azurerm_resource_group" "example" {
41+
name = "example-resources"
42+
location = "West Europe"
43+
provider = azurerm.test_123
44+
}
45+
```
46+
47+
```
48+
$ tflint
49+
0 issue(s) found
50+
```
51+
52+
Without the resource using the aliased provider:
53+
54+
```hcl
55+
provider "azurerm" {
56+
features {}
57+
alias = "test_123"
58+
subscription_id = ""
59+
}
60+
```
61+
62+
```
63+
$ tflint
64+
1 issue(s) found:
65+
66+
Warning: provider "azurerm" with alias "test_123" is declared but not used (terraform_unused_declarations)
67+
68+
on config.tf line 1:
69+
1: provider "azurerm" {
70+
```
71+
3172
## Why
3273

33-
Terraform will ignore variables and locals that are not used. It will refresh declared data sources regardless of usage. However, unreferenced variables likely indicate either a bug (and should be referenced) or removed code (and should be removed).
74+
Terraform will ignore variables and locals that are not used. It will refresh declared data sources regardless of usage. However, unreferenced variables and provider aliases likely indicate either a bug (and should be referenced) or removed code (and should be removed).
3475

3576
## How To Fix
3677

37-
Remove the declaration. For `variable` and `data`, remove the entire block. For a `local` value, remove the attribute from the `locals` block.
78+
Remove the declaration. For `variable`, `data`, and `provider` (with alias), remove the entire block. For a `local` value, remove the attribute from the `locals` block.
3879

3980
While data sources should generally not have side effects, take greater care when removing them. For example, removing `data "http"` will cause Terraform to no longer perform an HTTP `GET` request during each plan. If a data source is being used for side effects, add an annotation to ignore it:
4081

rules/terraform_unused_declarations.go

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55

66
"github.com/hashicorp/hcl/v2"
7+
"github.com/hashicorp/hcl/v2/gohcl"
78
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
89
"github.com/terraform-linters/tflint-plugin-sdk/terraform/addrs"
910
"github.com/terraform-linters/tflint-plugin-sdk/terraform/lang"
@@ -18,9 +19,10 @@ type TerraformUnusedDeclarationsRule struct {
1819
}
1920

2021
type declarations struct {
21-
Variables map[string]*hclext.Block
22-
DataResources map[string]*hclext.Block
23-
Locals map[string]*terraform.Local
22+
Variables map[string]*hclext.Block
23+
DataResources map[string]*hclext.Block
24+
Locals map[string]*terraform.Local
25+
ProviderAliases map[string]*hclext.Block
2426
}
2527

2628
// NewTerraformUnusedDeclarationsRule returns a new rule
@@ -103,15 +105,31 @@ func (r *TerraformUnusedDeclarationsRule) Check(rr tflint.Runner) error {
103105
return err
104106
}
105107
}
108+
for _, provider := range decl.ProviderAliases {
109+
aliasAttr := provider.Body.Attributes["alias"]
110+
var aliasName string
111+
if diags := gohcl.DecodeExpression(aliasAttr.Expr, nil, &aliasName); diags.HasErrors() {
112+
continue
113+
}
114+
if err := runner.EmitIssueWithFix(
115+
r,
116+
fmt.Sprintf(`provider "%s" with alias "%s" is declared but not used`, provider.Labels[0], aliasName),
117+
provider.DefRange,
118+
func(f tflint.Fixer) error { return f.RemoveExtBlock(provider) },
119+
); err != nil {
120+
return err
121+
}
122+
}
106123

107124
return nil
108125
}
109126

110127
func (r *TerraformUnusedDeclarationsRule) declarations(runner *terraform.Runner) (*declarations, error) {
111128
decl := &declarations{
112-
Variables: map[string]*hclext.Block{},
113-
DataResources: map[string]*hclext.Block{},
114-
Locals: map[string]*terraform.Local{},
129+
Variables: map[string]*hclext.Block{},
130+
DataResources: map[string]*hclext.Block{},
131+
Locals: map[string]*terraform.Local{},
132+
ProviderAliases: map[string]*hclext.Block{},
115133
}
116134

117135
body, err := runner.GetModuleContent(&hclext.BodySchema{
@@ -151,6 +169,15 @@ func (r *TerraformUnusedDeclarationsRule) declarations(runner *terraform.Runner)
151169
},
152170
},
153171
},
172+
{
173+
Type: "provider",
174+
LabelNames: []string{"name"},
175+
Body: &hclext.BodySchema{
176+
Attributes: []hclext.AttributeSchema{
177+
{Name: "alias"},
178+
},
179+
},
180+
},
154181
},
155182
}, &tflint.GetModuleContentOption{ExpandMode: tflint.ExpandModeNone})
156183
if err != nil {
@@ -168,6 +195,14 @@ func (r *TerraformUnusedDeclarationsRule) declarations(runner *terraform.Runner)
168195
// Scoped data source addresses are unique in the module
169196
decl.DataResources[fmt.Sprintf("data.%s.%s", data.Labels[0], data.Labels[1])] = data
170197
}
198+
case "provider":
199+
// Only track providers with aliases
200+
if aliasAttr, exists := block.Body.Attributes["alias"]; exists {
201+
var aliasName string
202+
if diags := gohcl.DecodeExpression(aliasAttr.Expr, nil, &aliasName); !diags.HasErrors() {
203+
decl.ProviderAliases[fmt.Sprintf("%s.%s", block.Labels[0], aliasName)] = block
204+
}
205+
}
171206
default:
172207
panic("unreachable")
173208
}
@@ -183,6 +218,13 @@ func (r *TerraformUnusedDeclarationsRule) declarations(runner *terraform.Runner)
183218
}
184219

185220
func (r *TerraformUnusedDeclarationsRule) checkForRefsInExpr(expr hcl.Expression, decl *declarations) {
221+
// Check for provider alias references (e.g., aws.west in provider = aws.west)
222+
if traversal, diags := hcl.AbsTraversalForExpr(expr); diags == nil && len(traversal) == 2 {
223+
// Provider aliases are referenced as <provider>.<alias> (2 parts)
224+
providerRef := fmt.Sprintf("%s.%s", traversal.RootName(), traversal[1].(hcl.TraverseAttr).Name)
225+
delete(decl.ProviderAliases, providerRef)
226+
}
227+
186228
ReferenceLoop:
187229
for _, ref := range lang.ReferencesInExpr(expr) {
188230
switch sub := ref.Subject.(type) {

rules/terraform_unused_declarations_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,156 @@ check "unused" {
267267
},
268268
},
269269
},
270+
{
271+
Name: "unused provider alias",
272+
Content: `
273+
provider "azurerm" {
274+
features {}
275+
alias = "test_123"
276+
subscription_id = ""
277+
}
278+
`,
279+
Expected: helper.Issues{
280+
{
281+
Rule: NewTerraformUnusedDeclarationsRule(),
282+
Message: `provider "azurerm" with alias "test_123" is declared but not used`,
283+
Range: hcl.Range{
284+
Filename: "config.tf",
285+
Start: hcl.Pos{Line: 2, Column: 1},
286+
End: hcl.Pos{Line: 2, Column: 19},
287+
},
288+
},
289+
},
290+
Fixed: `
291+
`,
292+
},
293+
{
294+
Name: "used provider alias in resource",
295+
Content: `
296+
provider "azurerm" {
297+
features {}
298+
alias = "test_123"
299+
subscription_id = ""
300+
}
301+
302+
resource "azurerm_resource_group" "example" {
303+
name = "example-resources"
304+
location = "West Europe"
305+
provider = azurerm.test_123
306+
}
307+
`,
308+
Expected: helper.Issues{},
309+
},
310+
{
311+
Name: "used provider alias in data source",
312+
Content: `
313+
provider "aws" {
314+
alias = "west"
315+
region = "us-west-2"
316+
}
317+
318+
data "aws_ami" "example" {
319+
provider = aws.west
320+
most_recent = true
321+
}
322+
323+
output "ami" {
324+
value = data.aws_ami.example.id
325+
}
326+
`,
327+
Expected: helper.Issues{},
328+
},
329+
{
330+
Name: "used provider alias in module",
331+
Content: `
332+
provider "aws" {
333+
alias = "west"
334+
region = "us-west-2"
335+
}
336+
337+
module "example" {
338+
source = "./module"
339+
providers = {
340+
aws = aws.west
341+
}
342+
}
343+
`,
344+
Expected: helper.Issues{},
345+
},
346+
{
347+
Name: "provider without alias is not checked",
348+
Content: `
349+
provider "aws" {
350+
region = "us-east-1"
351+
}
352+
`,
353+
Expected: helper.Issues{},
354+
},
355+
{
356+
Name: "multiple provider aliases used in module providers map",
357+
Content: `
358+
provider "aws" {
359+
alias = "east"
360+
region = "us-east-1"
361+
}
362+
363+
provider "aws" {
364+
alias = "west"
365+
region = "us-west-2"
366+
}
367+
368+
module "example" {
369+
source = "./module"
370+
providers = {
371+
aws.primary = aws.east
372+
aws.secondary = aws.west
373+
}
374+
}
375+
`,
376+
Expected: helper.Issues{},
377+
},
378+
{
379+
Name: "multiple provider aliases, one unused",
380+
Content: `
381+
provider "aws" {
382+
alias = "east"
383+
region = "us-east-1"
384+
}
385+
386+
provider "aws" {
387+
alias = "west"
388+
region = "us-west-2"
389+
}
390+
391+
resource "aws_instance" "example" {
392+
provider = aws.west
393+
ami = "ami-12345"
394+
}
395+
`,
396+
Expected: helper.Issues{
397+
{
398+
Rule: NewTerraformUnusedDeclarationsRule(),
399+
Message: `provider "aws" with alias "east" is declared but not used`,
400+
Range: hcl.Range{
401+
Filename: "config.tf",
402+
Start: hcl.Pos{Line: 2, Column: 1},
403+
End: hcl.Pos{Line: 2, Column: 15},
404+
},
405+
},
406+
},
407+
Fixed: `
408+
409+
provider "aws" {
410+
alias = "west"
411+
region = "us-west-2"
412+
}
413+
414+
resource "aws_instance" "example" {
415+
provider = aws.west
416+
ami = "ami-12345"
417+
}
418+
`,
419+
},
270420
}
271421

272422
rule := NewTerraformUnusedDeclarationsRule()

0 commit comments

Comments
 (0)