Skip to content

Commit 3b6ea1a

Browse files
authored
Add WalkExpressions function (#181)
1 parent 9bf8cad commit 3b6ea1a

File tree

6 files changed

+591
-0
lines changed

6 files changed

+591
-0
lines changed

helper/runner.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66

77
"github.com/hashicorp/hcl/v2"
8+
"github.com/hashicorp/hcl/v2/hclsyntax"
89
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
910
"github.com/terraform-linters/tflint-plugin-sdk/terraform/addrs"
1011
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
@@ -124,6 +125,52 @@ func (r *Runner) GetFiles() (map[string]*hcl.File, error) {
124125
return r.files, nil
125126
}
126127

128+
type nativeWalker struct {
129+
walker tflint.ExprWalker
130+
}
131+
132+
func (w *nativeWalker) Enter(node hclsyntax.Node) hcl.Diagnostics {
133+
if expr, ok := node.(hcl.Expression); ok {
134+
return w.walker.Enter(expr)
135+
}
136+
return nil
137+
}
138+
139+
func (w *nativeWalker) Exit(node hclsyntax.Node) hcl.Diagnostics {
140+
if expr, ok := node.(hcl.Expression); ok {
141+
return w.walker.Exit(expr)
142+
}
143+
return nil
144+
}
145+
146+
// WalkExpressions traverses expressions in all files by the passed walker.
147+
func (r *Runner) WalkExpressions(walker tflint.ExprWalker) hcl.Diagnostics {
148+
diags := hcl.Diagnostics{}
149+
for _, file := range r.files {
150+
if body, ok := file.Body.(*hclsyntax.Body); ok {
151+
walkDiags := hclsyntax.Walk(body, &nativeWalker{walker: walker})
152+
diags = diags.Extend(walkDiags)
153+
continue
154+
}
155+
156+
// In JSON syntax, everything can be walked as an attribute.
157+
attrs, jsonDiags := file.Body.JustAttributes()
158+
if jsonDiags.HasErrors() {
159+
diags = diags.Extend(jsonDiags)
160+
continue
161+
}
162+
163+
for _, attr := range attrs {
164+
enterDiags := walker.Enter(attr.Expr)
165+
diags = diags.Extend(enterDiags)
166+
exitDiags := walker.Exit(attr.Expr)
167+
diags = diags.Extend(exitDiags)
168+
}
169+
}
170+
171+
return diags
172+
}
173+
127174
// DecodeRuleConfig extracts the rule's configuration into the given value
128175
func (r *Runner) DecodeRuleConfig(name string, ret interface{}) error {
129176
schema := hclext.ImpliedBodySchema(ret)

helper/runner_test.go

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,221 @@ func Test_GetModuleContent_json(t *testing.T) {
280280
}
281281
}
282282

283+
func TestWalkExpressions(t *testing.T) {
284+
tests := []struct {
285+
name string
286+
files map[string]string
287+
walked []hcl.Range
288+
}{
289+
{
290+
name: "resource",
291+
files: map[string]string{
292+
"resource.tf": `
293+
resource "null_resource" "test" {
294+
key = "foo"
295+
}`,
296+
},
297+
walked: []hcl.Range{
298+
{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
299+
{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
300+
},
301+
},
302+
{
303+
name: "data source",
304+
files: map[string]string{
305+
"data.tf": `
306+
data "null_dataresource" "test" {
307+
key = "foo"
308+
}`,
309+
},
310+
walked: []hcl.Range{
311+
{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
312+
{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
313+
},
314+
},
315+
{
316+
name: "module call",
317+
files: map[string]string{
318+
"module.tf": `
319+
module "m" {
320+
source = "./module"
321+
key = "foo"
322+
}`,
323+
},
324+
walked: []hcl.Range{
325+
{Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 22}},
326+
{Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 21}},
327+
{Start: hcl.Pos{Line: 4, Column: 12}, End: hcl.Pos{Line: 4, Column: 17}},
328+
{Start: hcl.Pos{Line: 4, Column: 13}, End: hcl.Pos{Line: 4, Column: 16}},
329+
},
330+
},
331+
{
332+
name: "provider config",
333+
files: map[string]string{
334+
"provider.tf": `
335+
provider "p" {
336+
key = "foo"
337+
}`,
338+
},
339+
walked: []hcl.Range{
340+
{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
341+
{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
342+
},
343+
},
344+
{
345+
name: "locals",
346+
files: map[string]string{
347+
"locals.tf": `
348+
locals {
349+
key = "foo"
350+
}`,
351+
},
352+
walked: []hcl.Range{
353+
{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
354+
{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
355+
},
356+
},
357+
{
358+
name: "output",
359+
files: map[string]string{
360+
"output.tf": `
361+
output "o" {
362+
value = "foo"
363+
}`,
364+
},
365+
walked: []hcl.Range{
366+
{Start: hcl.Pos{Line: 3, Column: 11}, End: hcl.Pos{Line: 3, Column: 16}},
367+
{Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 15}},
368+
},
369+
},
370+
{
371+
name: "resource with block",
372+
files: map[string]string{
373+
"resource.tf": `
374+
resource "null_resource" "test" {
375+
key = "foo"
376+
377+
lifecycle {
378+
ignore_changes = [key]
379+
}
380+
}`,
381+
},
382+
walked: []hcl.Range{
383+
{Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}},
384+
{Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}},
385+
{Start: hcl.Pos{Line: 6, Column: 22}, End: hcl.Pos{Line: 6, Column: 27}},
386+
{Start: hcl.Pos{Line: 6, Column: 23}, End: hcl.Pos{Line: 6, Column: 26}},
387+
},
388+
},
389+
{
390+
name: "resource json",
391+
files: map[string]string{
392+
"resource.tf.json": `
393+
{
394+
"resource": {
395+
"null_resource": {
396+
"test": {
397+
"key": "foo",
398+
"nested": {
399+
"key": "foo"
400+
},
401+
"list": [{
402+
"key": "foo"
403+
}]
404+
}
405+
}
406+
}
407+
}`,
408+
},
409+
walked: []hcl.Range{
410+
{Start: hcl.Pos{Line: 3, Column: 15}, End: hcl.Pos{Line: 15, Column: 4}},
411+
},
412+
},
413+
{
414+
name: "multiple files",
415+
files: map[string]string{
416+
"main.tf": `
417+
provider "aws" {
418+
region = "us-east-1"
419+
420+
assume_role {
421+
role_arn = "arn:aws:iam::123412341234:role/ExampleRole"
422+
}
423+
}`,
424+
"main_override.tf": `
425+
provider "aws" {
426+
region = "us-east-1"
427+
428+
assume_role {
429+
role_arn = null
430+
}
431+
}`,
432+
},
433+
walked: []hcl.Range{
434+
{Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 23}, Filename: "main.tf"},
435+
{Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 22}, Filename: "main.tf"},
436+
{Start: hcl.Pos{Line: 6, Column: 16}, End: hcl.Pos{Line: 6, Column: 60}, Filename: "main.tf"},
437+
{Start: hcl.Pos{Line: 6, Column: 17}, End: hcl.Pos{Line: 6, Column: 59}, Filename: "main.tf"},
438+
{Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 23}, Filename: "main_override.tf"},
439+
{Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 22}, Filename: "main_override.tf"},
440+
{Start: hcl.Pos{Line: 6, Column: 16}, End: hcl.Pos{Line: 6, Column: 20}, Filename: "main_override.tf"},
441+
},
442+
},
443+
{
444+
name: "nested attributes",
445+
files: map[string]string{
446+
"data.tf": `
447+
data "terraform_remote_state" "remote_state" {
448+
backend = "remote"
449+
450+
config = {
451+
organization = "Organization"
452+
workspaces = {
453+
name = "${var.environment}"
454+
}
455+
}
456+
}`,
457+
},
458+
walked: []hcl.Range{
459+
{Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 21}},
460+
{Start: hcl.Pos{Line: 3, Column: 14}, End: hcl.Pos{Line: 3, Column: 20}},
461+
{Start: hcl.Pos{Line: 5, Column: 12}, End: hcl.Pos{Line: 10, Column: 4}},
462+
{Start: hcl.Pos{Line: 6, Column: 5}, End: hcl.Pos{Line: 6, Column: 17}},
463+
{Start: hcl.Pos{Line: 6, Column: 20}, End: hcl.Pos{Line: 6, Column: 34}},
464+
{Start: hcl.Pos{Line: 6, Column: 21}, End: hcl.Pos{Line: 6, Column: 33}},
465+
{Start: hcl.Pos{Line: 7, Column: 5}, End: hcl.Pos{Line: 7, Column: 15}},
466+
{Start: hcl.Pos{Line: 7, Column: 18}, End: hcl.Pos{Line: 9, Column: 6}},
467+
{Start: hcl.Pos{Line: 8, Column: 7}, End: hcl.Pos{Line: 8, Column: 11}},
468+
{Start: hcl.Pos{Line: 8, Column: 14}, End: hcl.Pos{Line: 8, Column: 34}},
469+
{Start: hcl.Pos{Line: 8, Column: 17}, End: hcl.Pos{Line: 8, Column: 32}},
470+
},
471+
},
472+
}
473+
474+
for _, test := range tests {
475+
t.Run(test.name, func(t *testing.T) {
476+
runner := TestRunner(t, test.files)
477+
478+
walked := []hcl.Range{}
479+
diags := runner.WalkExpressions(tflint.ExprWalkFunc(func(expr hcl.Expression) hcl.Diagnostics {
480+
walked = append(walked, expr.Range())
481+
return nil
482+
}))
483+
if diags.HasErrors() {
484+
t.Fatal(diags)
485+
}
486+
opts := cmp.Options{
487+
cmpopts.IgnoreFields(hcl.Range{}, "Filename"),
488+
cmpopts.IgnoreFields(hcl.Pos{}, "Byte"),
489+
cmpopts.SortSlices(func(x, y hcl.Range) bool { return x.String() > y.String() }),
490+
}
491+
if diff := cmp.Diff(walked, test.walked, opts); diff != "" {
492+
t.Error(diff)
493+
}
494+
})
495+
}
496+
}
497+
283498
func Test_DecodeRuleConfig(t *testing.T) {
284499
files := map[string]string{
285500
".tflint.hcl": `

plugin/plugin2host/client.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,72 @@ func (c *GRPCClient) GetFiles() (map[string]*hcl.File, error) {
165165
return files, nil
166166
}
167167

168+
type nativeWalker struct {
169+
walker tflint.ExprWalker
170+
}
171+
172+
func (w *nativeWalker) Enter(node hclsyntax.Node) hcl.Diagnostics {
173+
if expr, ok := node.(hcl.Expression); ok {
174+
return w.walker.Enter(expr)
175+
}
176+
return nil
177+
}
178+
179+
func (w *nativeWalker) Exit(node hclsyntax.Node) hcl.Diagnostics {
180+
if expr, ok := node.(hcl.Expression); ok {
181+
return w.walker.Exit(expr)
182+
}
183+
return nil
184+
}
185+
186+
// WalkExpressions traverses expressions in all files by the passed walker.
187+
// Note that it behaves differently in native HCL syntax and JSON syntax.
188+
//
189+
// In the HCL syntax, `var.foo` and `var.bar` in `[var.foo, var.bar]` are
190+
// also passed to the walker. In other words, it traverses expressions recursively.
191+
// To avoid redundant checks, the walker should check the kind of expression.
192+
//
193+
// In the JSON syntax, only an expression of an attribute seen from the top
194+
// level of the file is passed. In other words, it doesn't traverse expressions
195+
// recursively. This is a limitation of JSON syntax.
196+
func (c *GRPCClient) WalkExpressions(walker tflint.ExprWalker) hcl.Diagnostics {
197+
files, err := c.GetFiles()
198+
if err != nil {
199+
return hcl.Diagnostics{
200+
{
201+
Severity: hcl.DiagError,
202+
Summary: "failed to call GetFiles()",
203+
Detail: err.Error(),
204+
},
205+
}
206+
}
207+
208+
diags := hcl.Diagnostics{}
209+
for _, file := range files {
210+
if body, ok := file.Body.(*hclsyntax.Body); ok {
211+
walkDiags := hclsyntax.Walk(body, &nativeWalker{walker: walker})
212+
diags = diags.Extend(walkDiags)
213+
continue
214+
}
215+
216+
// In JSON syntax, everything can be walked as an attribute.
217+
attrs, jsonDiags := file.Body.JustAttributes()
218+
if jsonDiags.HasErrors() {
219+
diags = diags.Extend(jsonDiags)
220+
continue
221+
}
222+
223+
for _, attr := range attrs {
224+
enterDiags := walker.Enter(attr.Expr)
225+
diags = diags.Extend(enterDiags)
226+
exitDiags := walker.Exit(attr.Expr)
227+
diags = diags.Extend(exitDiags)
228+
}
229+
}
230+
231+
return diags
232+
}
233+
168234
// DecodeRuleConfig guesses the schema of the rule config from the passed interface and sends the schema to GRPC server.
169235
// Content retrieved based on the schema is decoded into the passed interface.
170236
func (c *GRPCClient) DecodeRuleConfig(name string, ret interface{}) error {

0 commit comments

Comments
 (0)