Skip to content

Commit

Permalink
Add support for tflint-ignore-file annotations in JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
isobit committed Feb 11, 2025
1 parent fcbcce5 commit bb1bf7c
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 22 deletions.
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
}
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

0 comments on commit bb1bf7c

Please sign in to comment.