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

Add support for tflint-ignore-file annotations in JSON #2230

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
4 changes: 0 additions & 4 deletions cmd/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"io"
"os"
"path/filepath"
"strings"

"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2"
Expand Down Expand Up @@ -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
}
isobit marked this conversation as resolved.
Show resolved Hide resolved
ants, lexDiags := tflint.NewAnnotations(path, file)
diags = diags.Extend(lexDiags)
annotations[path] = ants
Expand Down
49 changes: 49 additions & 0 deletions docs/user-guide/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,52 @@ 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"
}
}
}
}
```

Similarly, annotations in JSON can be followed with arbitrary comments, but the annotation must be the first thing in the comment property string:

```json
{
"//": "tflint-ignore-file: aws_instance_invalid_type # This instance type is new and TFLint doesn't know about it yet",
"resource": {
"aws_instance": {
"foo": {
"instance_type": "t2.micro"
}
}
}
}
```

The `tflint-ignore` annotation is not supported in JSON configuration.
3 changes: 0 additions & 3 deletions langserver/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions tflint/annotation.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tflint

import (
"encoding/json"
"fmt"
"regexp"
"slices"
Expand All @@ -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})
Expand Down Expand Up @@ -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
Expand Down
130 changes: 115 additions & 15 deletions tflint/annotation_test.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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 */
Expand All @@ -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 */
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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"
Expand All @@ -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
Expand All @@ -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())
Expand Down
Loading