@@ -2,10 +2,12 @@ package cmd
2
2
3
3
import (
4
4
"fmt"
5
+ "io"
5
6
"os"
6
7
"path/filepath"
7
8
"strings"
8
9
10
+ "github.com/hashicorp/go-version"
9
11
"github.com/hashicorp/hcl/v2"
10
12
"github.com/spf13/afero"
11
13
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
@@ -27,6 +29,7 @@ func (cli *CLI) inspect(opts Options, args []string) int {
27
29
}
28
30
29
31
issues := tflint.Issues {}
32
+ changes := map [string ][]byte {}
30
33
31
34
for _ , wd := range workingDirs {
32
35
err := cli .withinChangedDir (wd , func () error {
@@ -62,11 +65,16 @@ func (cli *CLI) inspect(opts Options, args []string) int {
62
65
for i , file := range filterFiles {
63
66
filterFiles [i ] = filepath .Join (wd , file )
64
67
}
65
- moduleIssues , err := cli .inspectModule (opts , targetDir , filterFiles )
68
+
69
+ moduleIssues , moduleChanges , err := cli .inspectModule (opts , targetDir , filterFiles )
66
70
if err != nil {
67
71
return err
68
72
}
69
73
issues = append (issues , moduleIssues ... )
74
+ for path , source := range moduleChanges {
75
+ changes [path ] = source
76
+ }
77
+
70
78
return nil
71
79
})
72
80
if err != nil {
@@ -91,8 +99,16 @@ func (cli *CLI) inspect(opts Options, args []string) int {
91
99
force = cli .config .Force
92
100
}
93
101
102
+ cli .formatter .Fix = opts .Fix
94
103
cli .formatter .Print (issues , nil , cli .sources )
95
104
105
+ if opts .Fix {
106
+ if err := writeChanges (changes ); err != nil {
107
+ cli .formatter .Print (tflint.Issues {}, err , cli .sources )
108
+ return ExitCodeError
109
+ }
110
+ }
111
+
96
112
if len (issues ) > 0 && ! force && exceedsMinimumFailure (issues , opts .MinimumFailureSeverity ) {
97
113
return ExitCodeIssuesFound
98
114
}
@@ -143,75 +159,113 @@ func processArgs(args []string) (string, []string, error) {
143
159
return dir , filterFiles , nil
144
160
}
145
161
146
- func (cli * CLI ) inspectModule (opts Options , dir string , filterFiles []string ) (tflint.Issues , error ) {
162
+ func (cli * CLI ) inspectModule (opts Options , dir string , filterFiles []string ) (tflint.Issues , map [ string ][] byte , error ) {
147
163
issues := tflint.Issues {}
164
+ changes := map [string ][]byte {}
148
165
var err error
149
166
150
167
// Setup config
151
168
cli .config , err = tflint .LoadConfig (afero.Afero {Fs : afero .NewOsFs ()}, opts .Config )
152
169
if err != nil {
153
- return tflint. Issues {} , fmt .Errorf ("Failed to load TFLint config; %w" , err )
170
+ return issues , changes , fmt .Errorf ("Failed to load TFLint config; %w" , err )
154
171
}
155
172
cli .config .Merge (opts .toConfig ())
156
173
157
174
// Setup loader
158
175
cli .loader , err = terraform .NewLoader (afero.Afero {Fs : afero .NewOsFs ()}, cli .originalWorkingDir )
159
176
if err != nil {
160
- return tflint. Issues {} , fmt .Errorf ("Failed to prepare loading; %w" , err )
177
+ return issues , changes , fmt .Errorf ("Failed to prepare loading; %w" , err )
161
178
}
162
179
if opts .Recursive && ! cli .loader .IsConfigDir (dir ) {
163
180
// Ignore non-module directories in recursive mode
164
- return tflint. Issues {} , nil
181
+ return issues , changes , nil
165
182
}
166
183
167
184
// Setup runners
168
185
runners , err := cli .setupRunners (opts , dir )
169
186
if err != nil {
170
- return tflint. Issues {} , err
187
+ return issues , changes , err
171
188
}
172
189
rootRunner := runners [len (runners )- 1 ]
173
190
174
191
// Launch plugin processes
175
- rulesetPlugin , err := launchPlugins (cli .config )
192
+ rulesetPlugin , err := launchPlugins (cli .config , opts . Fix )
176
193
if rulesetPlugin != nil {
177
194
defer rulesetPlugin .Clean ()
178
195
}
179
196
if err != nil {
180
- return tflint. Issues {} , err
197
+ return issues , changes , err
181
198
}
182
199
183
- // Run inspection
200
+ // Check preconditions
201
+ sdkVersions := map [string ]* version.Version {}
184
202
for name , ruleset := range rulesetPlugin .RuleSets {
185
203
sdkVersion , err := ruleset .SDKVersion ()
186
204
if err != nil {
187
205
if st , ok := status .FromError (err ); ok && st .Code () == codes .Unimplemented {
188
206
// SDKVersion endpoint is available in tflint-plugin-sdk v0.14+.
189
- return tflint. Issues {} , fmt .Errorf (`Plugin "%s" SDK version is incompatible. Compatible versions: %s` , name , plugin .SDKVersionConstraints )
207
+ return issues , changes , fmt .Errorf (`Plugin "%s" SDK version is incompatible. Compatible versions: %s` , name , plugin .SDKVersionConstraints )
190
208
} else {
191
- return tflint. Issues {} , fmt .Errorf (`Failed to get plugin "%s" SDK version; %w` , name , err )
209
+ return issues , changes , fmt .Errorf (`Failed to get plugin "%s" SDK version; %w` , name , err )
192
210
}
193
211
}
194
212
if ! plugin .SDKVersionConstraints .Check (sdkVersion ) {
195
- return tflint.Issues {}, fmt .Errorf (`Plugin "%s" SDK version (%s) is incompatible. Compatible versions: %s` , name , sdkVersion , plugin .SDKVersionConstraints )
213
+ return issues , changes , fmt .Errorf (`Plugin "%s" SDK version (%s) is incompatible. Compatible versions: %s` , name , sdkVersion , plugin .SDKVersionConstraints )
214
+ }
215
+ sdkVersions [name ] = sdkVersion
216
+ }
217
+
218
+ // Run inspection
219
+ //
220
+ // Repeat an inspection until there are no more changes or the limit is reached,
221
+ // in case an autofix introduces new issues.
222
+ for loop := 1 ; ; loop ++ {
223
+ if loop > 10 {
224
+ return issues , changes , fmt .Errorf (`Reached the limit of autofix attempts, and the changes made by the autofix will not be applied. This may be due to the following reasons:
225
+
226
+ 1. The autofix is making changes that do not fix the issue.
227
+ 2. The autofix is continuing to introduce new issues.
228
+
229
+ By setting TFLINT_LOG=trace, you can confirm the changes made by the autofix and start troubleshooting.` )
230
+ }
231
+
232
+ for name , ruleset := range rulesetPlugin .RuleSets {
233
+ for _ , runner := range runners {
234
+ err = ruleset .Check (plugin .NewGRPCServer (runner , rootRunner , cli .loader .Files (), sdkVersions [name ]))
235
+ if err != nil {
236
+ return issues , changes , fmt .Errorf ("Failed to check ruleset; %w" , err )
237
+ }
238
+ }
196
239
}
197
240
241
+ changesInAttempt := map [string ][]byte {}
198
242
for _ , runner := range runners {
199
- err = ruleset .Check (plugin .NewGRPCServer (runner , rootRunner , cli .loader .Files (), sdkVersion ))
200
- if err != nil {
201
- return tflint.Issues {}, fmt .Errorf ("Failed to check ruleset; %w" , err )
243
+ for _ , issue := range runner .LookupIssues (filterFiles ... ) {
244
+ // On the second attempt, only fixable issues are appended to avoid duplicates.
245
+ if loop == 1 || issue .Fixable {
246
+ issues = append (issues , issue )
247
+ }
248
+ }
249
+ runner .Issues = tflint.Issues {}
250
+
251
+ for path , source := range runner .LookupChanges (filterFiles ... ) {
252
+ changesInAttempt [path ] = source
253
+ changes [path ] = source
202
254
}
255
+ runner .ClearChanges ()
203
256
}
204
- }
205
257
206
- for _ , runner := range runners {
207
- issues = append (issues , runner .LookupIssues (filterFiles ... )... )
258
+ if ! opts .Fix || len (changesInAttempt ) == 0 {
259
+ break
260
+ }
208
261
}
262
+
209
263
// Set module sources to CLI
210
264
for path , source := range cli .loader .Sources () {
211
265
cli .sources [path ] = source
212
266
}
213
267
214
- return issues , nil
268
+ return issues , changes , nil
215
269
}
216
270
217
271
func (cli * CLI ) setupRunners (opts Options , dir string ) ([]* tflint.Runner , error ) {
@@ -260,7 +314,7 @@ func (cli *CLI) setupRunners(opts Options, dir string) ([]*tflint.Runner, error)
260
314
return append (runners , runner ), nil
261
315
}
262
316
263
- func launchPlugins (config * tflint.Config ) (* plugin.Plugin , error ) {
317
+ func launchPlugins (config * tflint.Config , fix bool ) (* plugin.Plugin , error ) {
264
318
// Lookup plugins
265
319
rulesetPlugin , err := plugin .Discovery (config )
266
320
if err != nil {
@@ -269,6 +323,7 @@ func launchPlugins(config *tflint.Config) (*plugin.Plugin, error) {
269
323
270
324
rulesets := []tflint.RuleSet {}
271
325
pluginConf := config .ToPluginConfig ()
326
+ pluginConf .Fix = fix
272
327
273
328
// Check version constraints and apply a config to plugins
274
329
for name , ruleset := range rulesetPlugin .RuleSets {
@@ -316,6 +371,28 @@ func launchPlugins(config *tflint.Config) (*plugin.Plugin, error) {
316
371
return rulesetPlugin , nil
317
372
}
318
373
374
+ func writeChanges (changes map [string ][]byte ) error {
375
+ fs := afero .NewOsFs ()
376
+ for path , source := range changes {
377
+ f , err := fs .OpenFile (path , os .O_WRONLY | os .O_TRUNC , 0644 )
378
+ if err != nil {
379
+ return fmt .Errorf ("Failed to apply autofixes; failed to open %s: %w" , path , err )
380
+ }
381
+
382
+ n , err := f .Write (source )
383
+ if err == nil && n < len (source ) {
384
+ err = io .ErrShortWrite
385
+ }
386
+ if err1 := f .Close (); err == nil {
387
+ err = err1
388
+ }
389
+ if err != nil {
390
+ return fmt .Errorf ("Failed to apply autofixes; failed to write source code to %s: %w" , path , err )
391
+ }
392
+ }
393
+ return nil
394
+ }
395
+
319
396
// Checks if the given issues contain severities above or equal to the given minimum failure opt. Defaults to true if an error occurs
320
397
func exceedsMinimumFailure (issues tflint.Issues , minimumFailureOpt string ) bool {
321
398
if minimumFailureOpt != "" {
0 commit comments