Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tflint: Add WalkExpressions function #181

Merged
merged 1 commit into from
Aug 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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