Skip to content

Commit fa56094

Browse files
authored
Add rule generator (#64)
1 parent b71dcb6 commit fa56094

File tree

8 files changed

+245
-1
lines changed

8 files changed

+245
-1
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,13 @@ You can easily install the built plugin with the following:
6262
```
6363
$ make install
6464
```
65+
66+
## Add a new rule
67+
68+
If you are interested in adding a new rule to this ruleset, you can use the generator. Run the following command:
69+
70+
```
71+
$ go run ./rules/generator
72+
```
73+
74+
Follow the instructions to edit the generated files and open a new pull request.

docs/rules/rule.md.tmpl

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# {{ .RuleName }}
2+
3+
// TODO: Write the rule's description here
4+
5+
## Example
6+
7+
```hcl
8+
resource "null_resource" "foo" {
9+
// TODO: Write the example Terraform code which violates the rule
10+
}
11+
```
12+
13+
```
14+
$ tflint
15+
16+
// TODO: Write the output when inspects the above code
17+
18+
```
19+
20+
## Why
21+
22+
// TODO: Write why you should follow the rule. This section is also a place to explain the value of the rule
23+
24+
## How To Fix
25+
26+
// TODO: Write how to fix it to avoid the problem

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.15
44

55
require (
66
github.com/aws/aws-sdk-go v1.37.1
7+
github.com/dave/dst v0.26.2
78
github.com/golang/mock v1.4.4
89
github.com/google/go-cmp v0.5.4
910
github.com/hashicorp/aws-sdk-go-base v0.7.0

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
7777
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
7878
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
7979
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
80+
github.com/dave/dst v0.26.2 h1:lnxLAKI3tx7MgLNVDirFCsDTlTG9nKTk7GcptKcWSwY=
81+
github.com/dave/dst v0.26.2/go.mod h1:UMDJuIRPfyUCC78eFuB+SV/WI8oDeyFDvM/JR6NI3IU=
82+
github.com/dave/gopackages v0.0.0-20170318123100-46e7023ec56e/go.mod h1:i00+b/gKdIDIxuLDFob7ustLAVqhsZRk2qVZrArELGQ=
83+
github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg=
84+
github.com/dave/kerr v0.0.0-20170318121727-bc25dd6abe8e/go.mod h1:qZqlPyPvfsDJt+3wHJ1EvSXDuVjFTK0j2p/ca+gtsb8=
85+
github.com/dave/rebecca v0.9.1/go.mod h1:N6XYdMD/OKw3lkF3ywh8Z6wPGuwNFDNtWYEMFWEmXBA=
8086
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8187
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
8288
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -156,6 +162,7 @@ github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPg
156162
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
157163
github.com/google/martian/v3 v3.0.0 h1:pMen7vLs8nvgEYhywH3KDWJIJTeEr2ULsVWHWYHQyBs=
158164
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
165+
github.com/google/pprof v0.0.0-20181127221834-b4f47329b966/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
159166
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
160167
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
161168
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@@ -362,6 +369,7 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
362369
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
363370
go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto=
364371
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
372+
golang.org/x/arch v0.0.0-20180920145803-b19384d3c130/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8=
365373
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
366374
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
367375
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -454,6 +462,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
454462
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
455463
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
456464
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
465+
golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
457466
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
458467
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
459468
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -534,6 +543,7 @@ golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjs
534543
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
535544
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
536545
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
546+
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
537547
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
538548
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
539549
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -628,6 +638,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8
628638
gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
629639
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
630640
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
641+
gopkg.in/src-d/go-billy.v4 v4.3.0 h1:KtlZ4c1OWbIs4jCv5ZXrTqG8EQocr0g/d4DjNg70aek=
642+
gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk=
631643
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
632644
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
633645
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=

rules/generator-utils/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,5 @@ func GenerateFile(fileName string, tmplName string, meta interface{}) {
4545
// The difference from GenerateFile function is to output logs
4646
func GenerateFileWithLogs(fileName string, tmplName string, meta interface{}) {
4747
GenerateFile(fileName, tmplName, meta)
48-
fmt.Printf("Create: %s\n", fileName)
48+
fmt.Printf("Created: %s\n", fileName)
4949
}

rules/generator/main.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"go/format"
7+
"io/ioutil"
8+
"os"
9+
"strings"
10+
11+
"github.com/dave/dst"
12+
"github.com/dave/dst/decorator"
13+
utils "github.com/terraform-linters/tflint-ruleset-aws/rules/generator-utils"
14+
)
15+
16+
type metadata struct {
17+
RuleName string
18+
RuleNameCC string
19+
}
20+
21+
func main() {
22+
buf := bufio.NewReader(os.Stdin)
23+
fmt.Print("Rule name? (e.g. aws_instance_invalid_type): ")
24+
ruleName, err := buf.ReadString('\n')
25+
if err != nil {
26+
panic(err)
27+
}
28+
ruleName = strings.Trim(ruleName, "\n")
29+
30+
meta := &metadata{RuleNameCC: utils.ToCamel(ruleName), RuleName: ruleName}
31+
32+
utils.GenerateFileWithLogs(fmt.Sprintf("rules/%s.go", ruleName), "rules/rule.go.tmpl", meta)
33+
utils.GenerateFileWithLogs(fmt.Sprintf("rules/%s_test.go", ruleName), "rules/rule_test.go.tmpl", meta)
34+
utils.GenerateFileWithLogs(fmt.Sprintf("docs/rules/%s.md", ruleName), "docs/rules/rule.md.tmpl", meta)
35+
36+
src, err := ioutil.ReadFile("rules/provider.go")
37+
if err != nil {
38+
panic(err)
39+
}
40+
dstf, err := decorator.Parse(src)
41+
if err != nil {
42+
panic(err)
43+
}
44+
45+
dst.Inspect(dstf, func(n dst.Node) bool {
46+
switch node := n.(type) {
47+
case *dst.CompositeLit:
48+
expr := &dst.CallExpr{
49+
Fun: &dst.Ident{
50+
Name: fmt.Sprintf("New%sRule", meta.RuleNameCC),
51+
},
52+
}
53+
expr.Decs.Before = dst.NewLine
54+
expr.Decs.After = dst.NewLine
55+
node.Elts = append(node.Elts, expr)
56+
}
57+
return true
58+
})
59+
60+
fset, astf, err := decorator.RestoreFile(dstf)
61+
if err != nil {
62+
panic(err)
63+
}
64+
65+
fp, err := os.OpenFile("rules/provider.go", os.O_RDWR, 0755)
66+
if err != nil {
67+
panic(err)
68+
}
69+
if err := format.Node(fp, fset, astf); err != nil {
70+
panic(err)
71+
}
72+
fmt.Println("Modified: rules/provider.go")
73+
74+
fmt.Println(`
75+
TODO:
76+
1. Remove all "TODO" comments from generated files.
77+
2. Write implementation of the rule.
78+
3. Add a link to the generated documentation into docs/rules/README.md`)
79+
}

rules/rule.go.tmpl

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package rules
2+
3+
import (
4+
hcl "github.com/hashicorp/hcl/v2"
5+
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
6+
"github.com/terraform-linters/tflint-ruleset-aws/project"
7+
)
8+
9+
// TODO: Write the rule's description here
10+
// {{ .RuleNameCC }}Rule checks ...
11+
type {{ .RuleNameCC }}Rule struct {
12+
resourceType string
13+
attributeName string
14+
}
15+
16+
// New{{ .RuleNameCC }}Rule returns new rule with default attributes
17+
func New{{ .RuleNameCC }}Rule() *{{ .RuleNameCC }}Rule {
18+
return &{{ .RuleNameCC }}Rule{
19+
// TODO: Write resource type and attribute name here
20+
resourceType: "...",
21+
attributeName: "...",
22+
}
23+
}
24+
25+
// Name returns the rule name
26+
func (r *{{ .RuleNameCC }}Rule) Name() string {
27+
return "{{ .RuleName }}"
28+
}
29+
30+
// Enabled returns whether the rule is enabled by default
31+
func (r *{{ .RuleNameCC }}Rule) Enabled() bool {
32+
// TODO: Determine whether the rule is enabled by default
33+
return true
34+
}
35+
36+
// Severity returns the rule severity
37+
func (r *{{ .RuleNameCC }}Rule) Severity() string {
38+
// TODO: Determine the rule's severiry
39+
return tflint.ERROR
40+
}
41+
42+
// Link returns the rule reference link
43+
func (r *{{ .RuleNameCC }}Rule) Link() string {
44+
// TODO: If the rule is so trivial that no documentation is needed, return "" instead.
45+
return project.ReferenceLink(r.Name())
46+
}
47+
48+
// TODO: Write the details of the inspection
49+
// Check checks ...
50+
func (r *{{ .RuleNameCC }}Rule) Check(runner tflint.Runner) error {
51+
// TODO: Write the implementation here. See this documentation for what tflint.Runner can do.
52+
// https://pkg.go.dev/github.com/terraform-linters/tflint-plugin-sdk/tflint#Runner
53+
54+
return runner.WalkResourceAttributes(r.resourceType, r.attributeName, func(attribute *hcl.Attribute) error {
55+
var val string
56+
err := runner.EvaluateExpr(attribute.Expr, &val, nil)
57+
58+
return runner.EnsureNoError(err, func() error {
59+
if val == "" {
60+
runner.EmitIssueOnExpr(
61+
r,
62+
"TODO",
63+
attribute.Expr,
64+
)
65+
}
66+
return nil
67+
})
68+
})
69+
}

rules/rule_test.go.tmpl

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package rules
2+
3+
import (
4+
"testing"
5+
6+
hcl "github.com/hashicorp/hcl/v2"
7+
"github.com/terraform-linters/tflint-plugin-sdk/helper"
8+
)
9+
10+
func Test_{{ .RuleNameCC }}(t *testing.T) {
11+
cases := []struct {
12+
Name string
13+
Content string
14+
Expected helper.Issues
15+
}{
16+
{
17+
Name: "basic",
18+
Content: `
19+
resource "null_resource" "null" {
20+
}
21+
`,
22+
Expected: helper.Issues{
23+
{
24+
Rule: New{{ .RuleNameCC }}Rule(),
25+
Message: "TODO",
26+
Range: hcl.Range{
27+
Filename: "resource.tf",
28+
Start: hcl.Pos{Line: 0, Column: 0},
29+
End: hcl.Pos{Line: 0, Column: 0},
30+
},
31+
},
32+
},
33+
},
34+
}
35+
36+
rule := New{{ .RuleNameCC }}Rule()
37+
38+
for _, tc := range cases {
39+
runner := helper.TestRunner(t, map[string]string{"resource.tf": tc.Content})
40+
41+
if err := rule.Check(runner); err != nil {
42+
t.Fatalf("Unexpected error occurred: %s", err)
43+
}
44+
45+
helper.AssertIssues(t, tc.Expected, runner.Issues)
46+
}
47+
}

0 commit comments

Comments
 (0)