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..d54e3623d 100644 --- a/docs/user-guide/annotations.md +++ b/docs/user-guide/annotations.md @@ -79,3 +79,24 @@ 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" + } + } + } +} +``` + +Note that the `tflint-ignore` annotation is not currently supported in +Terraform JSON config. diff --git a/tflint/annotation.go b/tflint/annotation.go index 3d95d388e..6faf7255e 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,63 @@ func NewAnnotations(path string, file *hcl.File) (Annotations, hcl.Diagnostics) return ret, diags } +// jsonAnnotations finds annotations in .tf.json files. Currently 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 data map[string]json.RawMessage + if err := json.Unmarshal(file.Bytes, &data); err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("failed to unmarshal file as JSON"), + Detail: err.Error(), + Subject: &hcl.Range{ + Filename: path, + }, + }) + return ret, diags + } + + rawComment, ok := data["//"] + if !ok { + return ret, diags + } + + var comment jsonAnnotationComment + if err := json.Unmarshal(rawComment, &comment); err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("failed to unmarshal comment"), + Detail: err.Error(), + Subject: &hcl.Range{ + Filename: path, + }, + }) + return ret, diags + } + + if len(comment.FileIgnore) > 0 { + ret = append(ret, &FileAnnotation{ + Content: comment.FileIgnore, + Token: hclsyntax.Token{ + Range: hcl.Range{ + Filename: path, + }, + }, + }) + } + + return ret, diags +} + +type jsonAnnotationComment struct { + FileIgnore string `json:"tflint-ignore-file,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..4a2de1eb4 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,58 @@ 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 propery", + 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", + }, + }, + }, + }, + }, } 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())