Skip to content

Commit

Permalink
Add WalkExpressions function
Browse files Browse the repository at this point in the history
  • Loading branch information
wata727 committed Aug 21, 2022
1 parent 9bf8cad commit aabf198
Show file tree
Hide file tree
Showing 6 changed files with 591 additions and 0 deletions.
47 changes: 47 additions & 0 deletions helper/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
"github.com/terraform-linters/tflint-plugin-sdk/terraform/addrs"
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
Expand Down Expand Up @@ -124,6 +125,52 @@ func (r *Runner) GetFiles() (map[string]*hcl.File, error) {
return r.files, nil
}

type nativeWalker struct {
walker tflint.ExprWalker
}

func (w *nativeWalker) Enter(node hclsyntax.Node) hcl.Diagnostics {
if expr, ok := node.(hcl.Expression); ok {
return w.walker.Enter(expr)
}
return nil
}

func (w *nativeWalker) Exit(node hclsyntax.Node) hcl.Diagnostics {
if expr, ok := node.(hcl.Expression); ok {
return w.walker.Exit(expr)
}
return nil
}

// WalkExpressions traverses expressions in all files by the passed walker.
func (r *Runner) WalkExpressions(walker tflint.ExprWalker) hcl.Diagnostics {
diags := hcl.Diagnostics{}
for _, file := range r.files {
if body, ok := file.Body.(*hclsyntax.Body); ok {
walkDiags := hclsyntax.Walk(body, &nativeWalker{walker: walker})
diags = diags.Extend(walkDiags)
continue
}

// In JSON syntax, everything can be walked as an attribute.
attrs, jsonDiags := file.Body.JustAttributes()
if jsonDiags.HasErrors() {
diags = diags.Extend(jsonDiags)
continue
}

for _, attr := range attrs {
enterDiags := walker.Enter(attr.Expr)
diags = diags.Extend(enterDiags)
exitDiags := walker.Exit(attr.Expr)
diags = diags.Extend(exitDiags)
}
}

return diags
}

// DecodeRuleConfig extracts the rule's configuration into the given value
func (r *Runner) DecodeRuleConfig(name string, ret interface{}) error {
schema := hclext.ImpliedBodySchema(ret)
Expand Down
215 changes: 215 additions & 0 deletions helper/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,221 @@ func Test_GetModuleContent_json(t *testing.T) {
}
}

func TestWalkExpressions(t *testing.T) {
tests := []struct {
name string
files map[string]string
walked []hcl.Range
}{
{
name: "resource",
files: map[string]string{
"resource.tf": `
resource "null_resource" "test" {
key = "foo"
}`,
},
walked: []hcl.Range{
{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
},
},
{
name: "data source",
files: map[string]string{
"data.tf": `
data "null_dataresource" "test" {
key = "foo"
}`,
},
walked: []hcl.Range{
{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
},
},
{
name: "module call",
files: map[string]string{
"module.tf": `
module "m" {
source = "./module"
key = "foo"
}`,
},
walked: []hcl.Range{
{Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 22}},
{Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 21}},
{Start: hcl.Pos{Line: 4, Column: 12}, End: hcl.Pos{Line: 4, Column: 17}},
{Start: hcl.Pos{Line: 4, Column: 13}, End: hcl.Pos{Line: 4, Column: 16}},
},
},
{
name: "provider config",
files: map[string]string{
"provider.tf": `
provider "p" {
key = "foo"
}`,
},
walked: []hcl.Range{
{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
},
},
{
name: "locals",
files: map[string]string{
"locals.tf": `
locals {
key = "foo"
}`,
},
walked: []hcl.Range{
{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
},
},
{
name: "output",
files: map[string]string{
"output.tf": `
output "o" {
value = "foo"
}`,
},
walked: []hcl.Range{
{Start: hcl.Pos{Line: 3, Column: 11}, End: hcl.Pos{Line: 3, Column: 16}},
{Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 15}},
},
},
{
name: "resource with block",
files: map[string]string{
"resource.tf": `
resource "null_resource" "test" {
key = "foo"
lifecycle {
ignore_changes = [key]
}
}`,
},
walked: []hcl.Range{
{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
{Start: hcl.Pos{Line: 6, Column: 22}, End: hcl.Pos{Line: 6, Column: 27}},
{Start: hcl.Pos{Line: 6, Column: 23}, End: hcl.Pos{Line: 6, Column: 26}},
},
},
{
name: "resource json",
files: map[string]string{
"resource.tf.json": `
{
"resource": {
"null_resource": {
"test": {
"key": "foo",
"nested": {
"key": "foo"
},
"list": [{
"key": "foo"
}]
}
}
}
}`,
},
walked: []hcl.Range{
{Start: hcl.Pos{Line: 3, Column: 15}, End: hcl.Pos{Line: 15, Column: 4}},
},
},
{
name: "multiple files",
files: map[string]string{
"main.tf": `
provider "aws" {
region = "us-east-1"
assume_role {
role_arn = "arn:aws:iam::123412341234:role/ExampleRole"
}
}`,
"main_override.tf": `
provider "aws" {
region = "us-east-1"
assume_role {
role_arn = null
}
}`,
},
walked: []hcl.Range{
{Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 23}, Filename: "main.tf"},
{Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 22}, Filename: "main.tf"},
{Start: hcl.Pos{Line: 6, Column: 16}, End: hcl.Pos{Line: 6, Column: 60}, Filename: "main.tf"},
{Start: hcl.Pos{Line: 6, Column: 17}, End: hcl.Pos{Line: 6, Column: 59}, Filename: "main.tf"},
{Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 23}, Filename: "main_override.tf"},
{Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 22}, Filename: "main_override.tf"},
{Start: hcl.Pos{Line: 6, Column: 16}, End: hcl.Pos{Line: 6, Column: 20}, Filename: "main_override.tf"},
},
},
{
name: "nested attributes",
files: map[string]string{
"data.tf": `
data "terraform_remote_state" "remote_state" {
backend = "remote"
config = {
organization = "Organization"
workspaces = {
name = "${var.environment}"
}
}
}`,
},
walked: []hcl.Range{
{Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 21}},
{Start: hcl.Pos{Line: 3, Column: 14}, End: hcl.Pos{Line: 3, Column: 20}},
{Start: hcl.Pos{Line: 5, Column: 12}, End: hcl.Pos{Line: 10, Column: 4}},
{Start: hcl.Pos{Line: 6, Column: 5}, End: hcl.Pos{Line: 6, Column: 17}},
{Start: hcl.Pos{Line: 6, Column: 20}, End: hcl.Pos{Line: 6, Column: 34}},
{Start: hcl.Pos{Line: 6, Column: 21}, End: hcl.Pos{Line: 6, Column: 33}},
{Start: hcl.Pos{Line: 7, Column: 5}, End: hcl.Pos{Line: 7, Column: 15}},
{Start: hcl.Pos{Line: 7, Column: 18}, End: hcl.Pos{Line: 9, Column: 6}},
{Start: hcl.Pos{Line: 8, Column: 7}, End: hcl.Pos{Line: 8, Column: 11}},
{Start: hcl.Pos{Line: 8, Column: 14}, End: hcl.Pos{Line: 8, Column: 34}},
{Start: hcl.Pos{Line: 8, Column: 17}, End: hcl.Pos{Line: 8, Column: 32}},
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
runner := TestRunner(t, test.files)

walked := []hcl.Range{}
diags := runner.WalkExpressions(tflint.ExprWalkFunc(func(expr hcl.Expression) hcl.Diagnostics {
walked = append(walked, expr.Range())
return nil
}))
if diags.HasErrors() {
t.Fatal(diags)
}
opts := cmp.Options{
cmpopts.IgnoreFields(hcl.Range{}, "Filename"),
cmpopts.IgnoreFields(hcl.Pos{}, "Byte"),
cmpopts.SortSlices(func(x, y hcl.Range) bool { return x.String() > y.String() }),
}
if diff := cmp.Diff(walked, test.walked, opts); diff != "" {
t.Error(diff)
}
})
}
}

func Test_DecodeRuleConfig(t *testing.T) {
files := map[string]string{
".tflint.hcl": `
Expand Down
66 changes: 66 additions & 0 deletions plugin/plugin2host/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,72 @@ func (c *GRPCClient) GetFiles() (map[string]*hcl.File, error) {
return files, nil
}

type nativeWalker struct {
walker tflint.ExprWalker
}

func (w *nativeWalker) Enter(node hclsyntax.Node) hcl.Diagnostics {
if expr, ok := node.(hcl.Expression); ok {
return w.walker.Enter(expr)
}
return nil
}

func (w *nativeWalker) Exit(node hclsyntax.Node) hcl.Diagnostics {
if expr, ok := node.(hcl.Expression); ok {
return w.walker.Exit(expr)
}
return nil
}

// WalkExpressions traverses expressions in all files by the passed walker.
// Note that it behaves differently in native HCL syntax and JSON syntax.
//
// In the HCL syntax, `var.foo` and `var.bar` in `[var.foo, var.bar]` are
// also passed to the walker. In other words, it traverses expressions recursively.
// To avoid redundant checks, the walker should check the kind of expression.
//
// In the JSON syntax, only an expression of an attribute seen from the top
// level of the file is passed. In other words, it doesn't traverse expressions
// recursively. This is a limitation of JSON syntax.
func (c *GRPCClient) WalkExpressions(walker tflint.ExprWalker) hcl.Diagnostics {
files, err := c.GetFiles()
if err != nil {
return hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "failed to call GetFiles()",
Detail: err.Error(),
},
}
}

diags := hcl.Diagnostics{}
for _, file := range files {
if body, ok := file.Body.(*hclsyntax.Body); ok {
walkDiags := hclsyntax.Walk(body, &nativeWalker{walker: walker})
diags = diags.Extend(walkDiags)
continue
}

// In JSON syntax, everything can be walked as an attribute.
attrs, jsonDiags := file.Body.JustAttributes()
if jsonDiags.HasErrors() {
diags = diags.Extend(jsonDiags)
continue
}

for _, attr := range attrs {
enterDiags := walker.Enter(attr.Expr)
diags = diags.Extend(enterDiags)
exitDiags := walker.Exit(attr.Expr)
diags = diags.Extend(exitDiags)
}
}

return diags
}

// DecodeRuleConfig guesses the schema of the rule config from the passed interface and sends the schema to GRPC server.
// Content retrieved based on the schema is decoded into the passed interface.
func (c *GRPCClient) DecodeRuleConfig(name string, ret interface{}) error {
Expand Down
Loading

0 comments on commit aabf198

Please sign in to comment.