Skip to content

Commit cf69671

Browse files
committed
Add support for tflint-ignore-file annotations in JSON
1 parent fcbcce5 commit cf69671

File tree

5 files changed

+220
-22
lines changed

5 files changed

+220
-22
lines changed

cmd/inspect.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"io"
77
"os"
88
"path/filepath"
9-
"strings"
109

1110
"github.com/hashicorp/go-version"
1211
"github.com/hashicorp/hcl/v2"
@@ -223,9 +222,6 @@ func (cli *CLI) setupRunners(opts Options, dir string) (*tflint.Runner, []*tflin
223222
}
224223
annotations := map[string]tflint.Annotations{}
225224
for path, file := range files {
226-
if !strings.HasSuffix(path, ".tf") {
227-
continue
228-
}
229225
ants, lexDiags := tflint.NewAnnotations(path, file)
230226
diags = diags.Extend(lexDiags)
231227
annotations[path] = ants

docs/user-guide/annotations.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,52 @@ resource "aws_instance" "foo" { # tflint-ignore-file: aws_instance_invalid_type
7979
instance_type = "t1.2xlarge"
8080
}
8181
```
82+
83+
The `tflint-ignore-file` annotation is also supported in Terraform JSON by
84+
using a top-level [comment property](https://developer.hashicorp.com/terraform/language/syntax/json#comment-properties):
85+
86+
```json
87+
{
88+
"//": "tflint-ignore-file: aws_instance_invalid_type",
89+
"resource": {
90+
"aws_instance": {
91+
"foo": {
92+
"instance_type": "t2.micro"
93+
}
94+
}
95+
}
96+
}
97+
```
98+
99+
As with annotations in HCL files, multiple rules can be specified as a
100+
comma-separated list:
101+
102+
```json
103+
{
104+
"//": "tflint-ignore-file: aws_instance_invalid_type, other_rule",
105+
"resource": {
106+
"aws_instance": {
107+
"foo": {
108+
"instance_type": "t2.micro"
109+
}
110+
}
111+
}
112+
}
113+
```
114+
115+
Similarly, annotations in JSON can be followed with arbitrary comments, but the annotation must be the first thing in the comment property string:
116+
117+
```json
118+
{
119+
"//": "tflint-ignore-file: aws_instance_invalid_type, other_rule # This instance type is new and TFLint doesn't know about it yet",
120+
"resource": {
121+
"aws_instance": {
122+
"foo": {
123+
"instance_type": "t2.micro"
124+
}
125+
}
126+
}
127+
}
128+
```
129+
130+
The `tflint-ignore` annotation is not supported in JSON configuration.

langserver/handler.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,6 @@ func (h *handler) inspect() (map[string][]lsp.Diagnostic, error) {
178178
}
179179
annotations := map[string]tflint.Annotations{}
180180
for path, file := range files {
181-
if !strings.HasSuffix(path, ".tf") {
182-
continue
183-
}
184181
ants, lexDiags := tflint.NewAnnotations(path, file)
185182
diags = diags.Extend(lexDiags)
186183
annotations[path] = ants

tflint/annotation.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package tflint
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"regexp"
67
"slices"
@@ -21,6 +22,15 @@ type Annotations []Annotation
2122

2223
// NewAnnotations find annotations from the passed tokens and return that list.
2324
func NewAnnotations(path string, file *hcl.File) (Annotations, hcl.Diagnostics) {
25+
switch {
26+
case strings.HasSuffix(path, ".json"):
27+
return jsonAnnotations(path, file)
28+
default:
29+
return hclAnnotations(path, file)
30+
}
31+
}
32+
33+
func hclAnnotations(path string, file *hcl.File) (Annotations, hcl.Diagnostics) {
2434
ret := Annotations{}
2535

2636
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)
6676
return ret, diags
6777
}
6878

79+
// jsonAnnotations finds annotations in .tf.json files. Only file-level ignores
80+
// are supported, by specifying a root-level comment property (with key "//")
81+
// which is an object containing a string property with the key
82+
// "tflint-ignore-file".
83+
func jsonAnnotations(path string, file *hcl.File) (Annotations, hcl.Diagnostics) {
84+
ret := Annotations{}
85+
diags := hcl.Diagnostics{}
86+
87+
var config jsonConfigWithComment
88+
if err := json.Unmarshal(file.Bytes, &config); err != nil {
89+
return ret, diags
90+
}
91+
92+
// tflint-ignore-file annotation
93+
matchIndexes := fileAnnotationPattern.FindStringSubmatchIndex(config.Comment)
94+
if len(matchIndexes) == 4 {
95+
if matchIndexes[0] != 0 {
96+
diags = append(diags, &hcl.Diagnostic{
97+
Severity: hcl.DiagError,
98+
Summary: "tflint-ignore-file annotation must appear at the beginning of the JSON comment property value",
99+
Detail: fmt.Sprintf("tflint-ignore-file annotation is written at index %d of the comment property value", matchIndexes[0]),
100+
Subject: &hcl.Range{
101+
// Cannot set Start/End because encoding/json does not expose it
102+
Filename: path,
103+
},
104+
})
105+
return ret, diags
106+
}
107+
ret = append(ret, &FileAnnotation{
108+
Content: strings.TrimSpace(config.Comment[matchIndexes[2]:matchIndexes[3]]),
109+
Token: hclsyntax.Token{
110+
Range: hcl.Range{
111+
// Cannot set Start/End because encoding/json does not expose it
112+
Filename: path,
113+
},
114+
},
115+
})
116+
}
117+
118+
return ret, diags
119+
}
120+
121+
type jsonConfigWithComment struct {
122+
Comment string `json:"//,omitempty"`
123+
}
124+
69125
var lineAnnotationPattern = regexp.MustCompile(`tflint-ignore: ([^\n*/#]+)`)
70126

71127
// LineAnnotation is an annotation for ignoring issues in a line

tflint/annotation_test.go

Lines changed: 115 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
package tflint
22

33
import (
4+
"strings"
45
"testing"
56

67
"github.com/google/go-cmp/cmp"
78
"github.com/google/go-cmp/cmp/cmpopts"
89
hcl "github.com/hashicorp/hcl/v2"
10+
"github.com/hashicorp/hcl/v2/hclparse"
911
"github.com/hashicorp/hcl/v2/hclsyntax"
1012
)
1113

1214
func Test_NewAnnotations(t *testing.T) {
1315
tests := []struct {
14-
name string
15-
src string
16-
want Annotations
17-
diags string
16+
name string
17+
filename string
18+
src string
19+
want Annotations
20+
diags string
1821
}{
1922
{
20-
name: "annotation starting with #",
23+
name: "annotation starting with #",
24+
filename: "resource.tf",
2125
src: `
2226
resource "aws_instance" "foo" {
2327
# tflint-ignore: aws_instance_invalid_type
@@ -39,7 +43,8 @@ resource "aws_instance" "foo" {
3943
},
4044
},
4145
{
42-
name: "annotation starting with //",
46+
name: "annotation starting with //",
47+
filename: "resource.tf",
4348
src: `
4449
resource "aws_instance" "foo" {
4550
// This is also comment
@@ -61,7 +66,8 @@ resource "aws_instance" "foo" {
6166
},
6267
},
6368
{
64-
name: "annotation starting with /*",
69+
name: "annotation starting with /*",
70+
filename: "resource.tf",
6571
src: `
6672
resource "aws_instance" "foo" {
6773
/* tflint-ignore: aws_instance_invalid_type */
@@ -83,7 +89,8 @@ resource "aws_instance" "foo" {
8389
},
8490
},
8591
{
86-
name: "ignoring multiple rules",
92+
name: "ignoring multiple rules",
93+
filename: "resource.tf",
8794
src: `
8895
resource "aws_instance" "foo" {
8996
/* tflint-ignore: aws_instance_invalid_type, terraform_deprecated_syntax */
@@ -105,7 +112,8 @@ resource "aws_instance" "foo" {
105112
},
106113
},
107114
{
108-
name: "with reason starting with //",
115+
name: "with reason starting with //",
116+
filename: "resource.tf",
109117
src: `
110118
resource "aws_instance" "foo" {
111119
instance_type = "t2.micro" // tflint-ignore: aws_instance_invalid_type // With reason
@@ -126,7 +134,8 @@ resource "aws_instance" "foo" {
126134
},
127135
},
128136
{
129-
name: "with reason starting with #",
137+
name: "with reason starting with #",
138+
filename: "resource.tf",
130139
src: `
131140
resource "aws_instance" "foo" {
132141
# tflint-ignore: aws_instance_invalid_type # With reason
@@ -148,7 +157,8 @@ resource "aws_instance" "foo" {
148157
},
149158
},
150159
{
151-
name: "tflint-ignore-file annotation",
160+
name: "tflint-ignore-file annotation",
161+
filename: "resource.tf",
152162
src: `# tflint-ignore-file: aws_instance_invalid_type
153163
resource "aws_instance" "foo" {
154164
instance_type = "t2.micro"
@@ -169,7 +179,8 @@ resource "aws_instance" "foo" {
169179
},
170180
},
171181
{
172-
name: "tflint-ignore-file annotation outside the first line",
182+
name: "tflint-ignore-file annotation outside the first line",
183+
filename: "resource.tf",
173184
src: `
174185
resource "aws_instance" "foo" {
175186
# tflint-ignore-file: aws_instance_invalid_type
@@ -179,22 +190,111 @@ resource "aws_instance" "foo" {
179190
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",
180191
},
181192
{
182-
name: "tflint-ignore-file annotation outside the first column",
193+
name: "tflint-ignore-file annotation outside the first column",
194+
filename: "resource.tf",
183195
src: `resource "aws_instance" "foo" { # tflint-ignore-file: aws_instance_invalid_type
184196
instance_type = "t2.micro"
185197
}`,
186198
want: Annotations{},
187199
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",
188200
},
201+
{
202+
name: "tflint-ignore-file in JSON comment property",
203+
filename: "resource.tf.json",
204+
src: `{
205+
"//": "tflint-ignore-file: aws_instance_invalid_type",
206+
"resource": {
207+
"aws_instance": {
208+
"foo": {
209+
"instance_type": "t2.micro"
210+
}
211+
}
212+
}
213+
}`,
214+
want: Annotations{
215+
&FileAnnotation{
216+
Content: "aws_instance_invalid_type",
217+
Token: hclsyntax.Token{
218+
Range: hcl.Range{
219+
Filename: "resource.tf.json",
220+
},
221+
},
222+
},
223+
},
224+
},
225+
{
226+
name: "tglint-ignore-file with multiple rules in JSON comment property and following comment",
227+
filename: "resource.tf.json",
228+
src: `{
229+
"//": "tflint-ignore-file: aws_instance_invalid_type, terraform_deprecated_syntax # this is an extra comment",
230+
"resource": {
231+
"aws_instance": {
232+
"foo": {
233+
"instance_type": "t2.micro"
234+
}
235+
}
236+
}
237+
}`,
238+
want: Annotations{
239+
&FileAnnotation{
240+
Content: "aws_instance_invalid_type, terraform_deprecated_syntax",
241+
Token: hclsyntax.Token{
242+
Range: hcl.Range{
243+
Filename: "resource.tf.json",
244+
},
245+
},
246+
},
247+
},
248+
},
249+
{
250+
name: "no errors if JSON comment property is not the expected structure",
251+
filename: "resource.tf.json",
252+
src: `{
253+
"//": {"foo": "bar"},
254+
"resource": {
255+
"aws_instance": {
256+
"foo": {
257+
"instance_type": "t2.micro"
258+
}
259+
}
260+
}
261+
}`,
262+
want: Annotations{},
263+
},
264+
{
265+
name: "tflint-ignore-file annotation outside the first column of the JSON comment property",
266+
filename: "resource.tf.json",
267+
src: `{
268+
"//": "blah blah # tflint-ignore-file: aws_instance_invalid_type",
269+
"resource": {
270+
"aws_instance": {
271+
"foo": {
272+
"instance_type": "t2.micro"
273+
}
274+
}
275+
}
276+
}`,
277+
want: Annotations{},
278+
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",
279+
},
189280
}
190281

191282
for _, test := range tests {
192283
t.Run(test.name, func(t *testing.T) {
193-
file, diags := hclsyntax.ParseConfig([]byte(test.src), "resource.tf", hcl.InitialPos)
284+
parser := hclparse.NewParser()
285+
var file *hcl.File
286+
var diags hcl.Diagnostics
287+
switch {
288+
case strings.HasSuffix(test.filename, ".json"):
289+
file, diags = parser.ParseJSON([]byte(test.src), test.filename)
290+
default:
291+
file, diags = parser.ParseHCL([]byte(test.src), test.filename)
292+
}
194293
if diags.HasErrors() {
195294
t.Fatal(diags)
196295
}
197-
got, diags := NewAnnotations("resource.tf", file)
296+
297+
got, diags := NewAnnotations(test.filename, file)
198298
if diags.HasErrors() || test.diags != "" {
199299
if diags.Error() != test.diags {
200300
t.Errorf("want=%s, got=%s", test.diags, diags.Error())

0 commit comments

Comments
 (0)