Skip to content

Commit 3afbf08

Browse files
authored
Add rule generator (#83)
1 parent 2d4dc1b commit 3afbf08

File tree

7 files changed

+303
-1
lines changed

7 files changed

+303
-1
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,13 @@ You can easily install the built plugin with the following:
3939
```
4040
$ make install
4141
```
42+
43+
## Add a new rule
44+
45+
If you are interested in adding a new rule to this ruleset, you can use the generator. Run the following command:
46+
47+
```
48+
$ go run ./rules/generator
49+
```
50+
51+
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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ module github.com/terraform-linters/tflint-ruleset-google
33
go 1.16
44

55
require (
6+
github.com/dave/dst v0.26.2
67
github.com/google/go-cmp v0.5.4
78
github.com/hashicorp/hcl/v2 v2.9.0
89
github.com/hashicorp/terraform-plugin-sdk/v2 v2.4.4
10+
github.com/onsi/ginkgo v1.15.2 // indirect
11+
github.com/onsi/gomega v1.11.0 // indirect
12+
github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e
913
github.com/terraform-linters/tflint-plugin-sdk v0.8.1
1014
google.golang.org/api v0.40.0
1115
)

go.sum

Lines changed: 46 additions & 1 deletion
Large diffs are not rendered by default.

rules/generator/main.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"go/format"
7+
"io/ioutil"
8+
"os"
9+
"strings"
10+
"text/template"
11+
12+
"github.com/dave/dst"
13+
"github.com/dave/dst/decorator"
14+
"github.com/serenize/snaker"
15+
)
16+
17+
type metadata struct {
18+
RuleName string
19+
RuleNameCC string
20+
}
21+
22+
func main() {
23+
buf := bufio.NewReader(os.Stdin)
24+
fmt.Print("Rule name? (e.g. google_compute_instance_invalid_machine_type): ")
25+
ruleName, err := buf.ReadString('\n')
26+
if err != nil {
27+
panic(err)
28+
}
29+
ruleName = strings.Trim(ruleName, "\n")
30+
31+
meta := &metadata{RuleNameCC: snaker.SnakeToCamel(ruleName), RuleName: ruleName}
32+
33+
GenerateFileWithLogs(fmt.Sprintf("rules/%s.go", ruleName), "rules/rule.go.tmpl", meta)
34+
GenerateFileWithLogs(fmt.Sprintf("rules/%s_test.go", ruleName), "rules/rule_test.go.tmpl", meta)
35+
GenerateFileWithLogs(fmt.Sprintf("docs/rules/%s.md", ruleName), "docs/rules/rule.md.tmpl", meta)
36+
37+
src, err := ioutil.ReadFile("rules/provider.go")
38+
if err != nil {
39+
panic(err)
40+
}
41+
dstf, err := decorator.Parse(src)
42+
if err != nil {
43+
panic(err)
44+
}
45+
46+
dst.Inspect(dstf, func(n dst.Node) bool {
47+
switch node := n.(type) {
48+
case *dst.CompositeLit:
49+
expr := &dst.CallExpr{
50+
Fun: &dst.Ident{
51+
Name: fmt.Sprintf("New%sRule", meta.RuleNameCC),
52+
},
53+
}
54+
expr.Decs.Before = dst.NewLine
55+
expr.Decs.After = dst.NewLine
56+
node.Elts = append(node.Elts, expr)
57+
}
58+
return true
59+
})
60+
61+
fset, astf, err := decorator.RestoreFile(dstf)
62+
if err != nil {
63+
panic(err)
64+
}
65+
66+
fp, err := os.OpenFile("rules/provider.go", os.O_RDWR, 0755)
67+
if err != nil {
68+
panic(err)
69+
}
70+
if err := format.Node(fp, fset, astf); err != nil {
71+
panic(err)
72+
}
73+
fmt.Println("Modified: rules/provider.go")
74+
75+
fmt.Println(`
76+
TODO:
77+
1. Remove all "TODO" comments from generated files.
78+
2. Write implementation of the rule.
79+
3. Add a link to the generated documentation into docs/rules/README.md`)
80+
}
81+
82+
// GenerateFile generates a new file from the passed template and metadata
83+
func GenerateFile(fileName string, tmplName string, meta interface{}) {
84+
file, err := os.Create(fileName)
85+
if err != nil {
86+
panic(err)
87+
}
88+
89+
tmpl := template.Must(template.ParseFiles(tmplName))
90+
err = tmpl.Execute(file, meta)
91+
if err != nil {
92+
panic(err)
93+
}
94+
}
95+
96+
// GenerateFileWithLogs generates a new file from the passed template and metadata
97+
// The difference from GenerateFile function is to output logs
98+
func GenerateFileWithLogs(fileName string, tmplName string, meta interface{}) {
99+
GenerateFile(fileName, tmplName, meta)
100+
fmt.Printf("Created: %s\n", fileName)
101+
}

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-google/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)