From 8b15cf7551de0cc8137dafd479bb960c2a53f58f Mon Sep 17 00:00:00 2001 From: Josh Glendenning Date: Sat, 8 Feb 2025 18:52:22 -0600 Subject: [PATCH] Add support for tflint-ignore-file annotations in JSON --- cmd/inspect.go | 4 - docs/user-guide/annotations.md | 38 ++++++++++ langserver/handler.go | 3 - tflint/annotation.go | 56 ++++++++++++++ tflint/annotation_test.go | 130 +++++++++++++++++++++++++++++---- 5 files changed, 209 insertions(+), 22 deletions(-) diff --git a/cmd/inspect.go b/cmd/inspect.go index 460c8ac71..129aef837 100644 --- a/cmd/inspect.go +++ b/cmd/inspect.go @@ -6,7 +6,6 @@ import ( "io" "os" "path/filepath" - "strings" "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" @@ -223,9 +222,6 @@ func (cli *CLI) setupRunners(opts Options, dir string) (*tflint.Runner, []*tflin } annotations := map[string]tflint.Annotations{} for path, file := range files { - if !strings.HasSuffix(path, ".tf") { - continue - } ants, lexDiags := tflint.NewAnnotations(path, file) diags = diags.Extend(lexDiags) annotations[path] = ants diff --git a/docs/user-guide/annotations.md b/docs/user-guide/annotations.md index 8a8052bbf..fde2a0d64 100644 --- a/docs/user-guide/annotations.md +++ b/docs/user-guide/annotations.md @@ -79,3 +79,41 @@ resource "aws_instance" "foo" { # tflint-ignore-file: aws_instance_invalid_type instance_type = "t1.2xlarge" } ``` + +The `tflint-ignore-file` annotation is also supported in Terraform JSON by +using a top-level [comment property](https://developer.hashicorp.com/terraform/language/syntax/json#comment-properties): + +```json +{ + "//": { + "tflint-ignore-file": "aws_instance_invalid_type" + }, + "resource": { + "aws_instance": { + "foo": { + "instance_type": "t2.micro" + } + } + } +} +``` + +As with annotations in HCL files, multiple rules can be specified as a +comma-separated list: + +```json +{ + "//": { + "tflint-ignore-file": "aws_instance_invalid_type, other_rule" + }, + "resource": { + "aws_instance": { + "foo": { + "instance_type": "t2.micro" + } + } + } +} +``` + +The `tflint-ignore` annotation is not supported JSON configuration. diff --git a/langserver/handler.go b/langserver/handler.go index dab385ccb..79f7a59cc 100644 --- a/langserver/handler.go +++ b/langserver/handler.go @@ -178,9 +178,6 @@ func (h *handler) inspect() (map[string][]lsp.Diagnostic, error) { } annotations := map[string]tflint.Annotations{} for path, file := range files { - if !strings.HasSuffix(path, ".tf") { - continue - } ants, lexDiags := tflint.NewAnnotations(path, file) diags = diags.Extend(lexDiags) annotations[path] = ants diff --git a/tflint/annotation.go b/tflint/annotation.go index 3d95d388e..6ce82af19 100644 --- a/tflint/annotation.go +++ b/tflint/annotation.go @@ -1,6 +1,7 @@ package tflint import ( + "encoding/json" "fmt" "regexp" "slices" @@ -21,6 +22,15 @@ type Annotations []Annotation // NewAnnotations find annotations from the passed tokens and return that list. func NewAnnotations(path string, file *hcl.File) (Annotations, hcl.Diagnostics) { + switch { + case strings.HasSuffix(path, ".json"): + return jsonAnnotations(path, file) + default: + return hclAnnotations(path, file) + } +} + +func hclAnnotations(path string, file *hcl.File) (Annotations, hcl.Diagnostics) { ret := Annotations{} tokens, diags := hclsyntax.LexConfig(file.Bytes, path, hcl.Pos{Byte: 0, Line: 1, Column: 1}) @@ -66,6 +76,52 @@ func NewAnnotations(path string, file *hcl.File) (Annotations, hcl.Diagnostics) return ret, diags } +// jsonAnnotations finds annotations in .tf.json files. Only file-level ignores +// are supported, by specifying a root-level comment property (with key "//") +// which is an object containing a string property with the key +// "tflint-ignore-file". +func jsonAnnotations(path string, file *hcl.File) (Annotations, hcl.Diagnostics) { + ret := Annotations{} + diags := hcl.Diagnostics{} + + var config jsonConfigWithComment + if err := json.Unmarshal(file.Bytes, &config); err != nil { + return ret, diags + } + + // tflint-ignore-file annotation + matchIndexes := fileAnnotationPattern.FindStringSubmatchIndex(config.Comment) + if len(matchIndexes) == 4 { + if matchIndexes[0] != 0 { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "tflint-ignore-file annotation must appear at the beginning of the JSON comment property value", + Detail: fmt.Sprintf("tflint-ignore-file annotation is written at index %d of the comment property value", matchIndexes[0]), + Subject: &hcl.Range{ + // Cannot set Start/End because encoding/json does not expose it + Filename: path, + }, + }) + return ret, diags + } + ret = append(ret, &FileAnnotation{ + Content: strings.TrimSpace(config.Comment[matchIndexes[2]:matchIndexes[3]]), + Token: hclsyntax.Token{ + Range: hcl.Range{ + // Cannot set Start/End because encoding/json does not expose it + Filename: path, + }, + }, + }) + } + + return ret, diags +} + +type jsonConfigWithComment struct { + Comment string `json:"//,omitempty"` +} + var lineAnnotationPattern = regexp.MustCompile(`tflint-ignore: ([^\n*/#]+)`) // LineAnnotation is an annotation for ignoring issues in a line diff --git a/tflint/annotation_test.go b/tflint/annotation_test.go index 64678108c..93a485166 100644 --- a/tflint/annotation_test.go +++ b/tflint/annotation_test.go @@ -1,23 +1,27 @@ package tflint import ( + "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" hcl "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclparse" "github.com/hashicorp/hcl/v2/hclsyntax" ) func Test_NewAnnotations(t *testing.T) { tests := []struct { - name string - src string - want Annotations - diags string + name string + filename string + src string + want Annotations + diags string }{ { - name: "annotation starting with #", + name: "annotation starting with #", + filename: "resource.tf", src: ` resource "aws_instance" "foo" { # tflint-ignore: aws_instance_invalid_type @@ -39,7 +43,8 @@ resource "aws_instance" "foo" { }, }, { - name: "annotation starting with //", + name: "annotation starting with //", + filename: "resource.tf", src: ` resource "aws_instance" "foo" { // This is also comment @@ -61,7 +66,8 @@ resource "aws_instance" "foo" { }, }, { - name: "annotation starting with /*", + name: "annotation starting with /*", + filename: "resource.tf", src: ` resource "aws_instance" "foo" { /* tflint-ignore: aws_instance_invalid_type */ @@ -83,7 +89,8 @@ resource "aws_instance" "foo" { }, }, { - name: "ignoring multiple rules", + name: "ignoring multiple rules", + filename: "resource.tf", src: ` resource "aws_instance" "foo" { /* tflint-ignore: aws_instance_invalid_type, terraform_deprecated_syntax */ @@ -105,7 +112,8 @@ resource "aws_instance" "foo" { }, }, { - name: "with reason starting with //", + name: "with reason starting with //", + filename: "resource.tf", src: ` resource "aws_instance" "foo" { instance_type = "t2.micro" // tflint-ignore: aws_instance_invalid_type // With reason @@ -126,7 +134,8 @@ resource "aws_instance" "foo" { }, }, { - name: "with reason starting with #", + name: "with reason starting with #", + filename: "resource.tf", src: ` resource "aws_instance" "foo" { # tflint-ignore: aws_instance_invalid_type # With reason @@ -148,7 +157,8 @@ resource "aws_instance" "foo" { }, }, { - name: "tflint-ignore-file annotation", + name: "tflint-ignore-file annotation", + filename: "resource.tf", src: `# tflint-ignore-file: aws_instance_invalid_type resource "aws_instance" "foo" { instance_type = "t2.micro" @@ -169,7 +179,8 @@ resource "aws_instance" "foo" { }, }, { - name: "tflint-ignore-file annotation outside the first line", + name: "tflint-ignore-file annotation outside the first line", + filename: "resource.tf", src: ` resource "aws_instance" "foo" { # tflint-ignore-file: aws_instance_invalid_type @@ -179,22 +190,111 @@ resource "aws_instance" "foo" { diags: "resource.tf:3,3-4,1: tflint-ignore-file annotation must be written at the top of file; tflint-ignore-file annotation is written at line 3, column 3", }, { - name: "tflint-ignore-file annotation outside the first column", + name: "tflint-ignore-file annotation outside the first column", + filename: "resource.tf", src: `resource "aws_instance" "foo" { # tflint-ignore-file: aws_instance_invalid_type instance_type = "t2.micro" }`, want: Annotations{}, diags: "resource.tf:1,33-2,1: tflint-ignore-file annotation must be written at the top of file; tflint-ignore-file annotation is written at line 1, column 33", }, + { + name: "tflint-ignore-file in JSON comment property", + filename: "resource.tf.json", + src: `{ + "//": "tflint-ignore-file: aws_instance_invalid_type", + "resource": { + "aws_instance": { + "foo": { + "instance_type": "t2.micro" + } + } + } +}`, + want: Annotations{ + &FileAnnotation{ + Content: "aws_instance_invalid_type", + Token: hclsyntax.Token{ + Range: hcl.Range{ + Filename: "resource.tf.json", + }, + }, + }, + }, + }, + { + name: "tglint-ignore-file with multiple rules in JSON comment property and following comment", + filename: "resource.tf.json", + src: `{ + "//": "tflint-ignore-file: aws_instance_invalid_type, terraform_deprecated_syntax # this is an extra comment", + "resource": { + "aws_instance": { + "foo": { + "instance_type": "t2.micro" + } + } + } +}`, + want: Annotations{ + &FileAnnotation{ + Content: "aws_instance_invalid_type, terraform_deprecated_syntax", + Token: hclsyntax.Token{ + Range: hcl.Range{ + Filename: "resource.tf.json", + }, + }, + }, + }, + }, + { + name: "no errors if JSON comment property is not the expected structure", + filename: "resource.tf.json", + src: `{ + "//": {"foo": "bar"}, + "resource": { + "aws_instance": { + "foo": { + "instance_type": "t2.micro" + } + } + } +}`, + want: Annotations{}, + }, + { + name: "tflint-ignore-file annotation outside the first column of the JSON comment property", + filename: "resource.tf.json", + src: `{ + "//": "blah blah # tflint-ignore-file: aws_instance_invalid_type", + "resource": { + "aws_instance": { + "foo": { + "instance_type": "t2.micro" + } + } + } +}`, + want: Annotations{}, + diags: "resource.tf.json:0,0-0: tflint-ignore-file annotation must appear at the beginning of the JSON comment property value; tflint-ignore-file annotation is written at index 12 of the comment property value", + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - file, diags := hclsyntax.ParseConfig([]byte(test.src), "resource.tf", hcl.InitialPos) + parser := hclparse.NewParser() + var file *hcl.File + var diags hcl.Diagnostics + switch { + case strings.HasSuffix(test.filename, ".json"): + file, diags = parser.ParseJSON([]byte(test.src), test.filename) + default: + file, diags = parser.ParseHCL([]byte(test.src), test.filename) + } if diags.HasErrors() { t.Fatal(diags) } - got, diags := NewAnnotations("resource.tf", file) + + got, diags := NewAnnotations(test.filename, file) if diags.HasErrors() || test.diags != "" { if diags.Error() != test.diags { t.Errorf("want=%s, got=%s", test.diags, diags.Error())