Skip to content

Commit a963a04

Browse files
Add JSON configuration file support
This change adds support for JSON-formatted TFLint configuration files (.tflint.json) in addition to the existing HCL format (.tflint.hcl). Changes: - Update config loading to detect file extension and parse JSON/HCL accordingly - Add support for .tflint.json in default file discovery - Maintain HCL preference over JSON files - Add comprehensive unit tests for JSON config loading scenarios - Add integration tests for JSON config functionality and precedence - Update user guide documentation with JSON format examples The implementation uses the existing hclparse.ParseJSON() from the HCL library, following the same JSON structure conventions as Terraform's .tf.json files. All existing functionality remains unchanged. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 4d11a44 commit a963a04

File tree

7 files changed

+504
-10
lines changed

7 files changed

+504
-10
lines changed

docs/user-guide/config.md

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ You can change the behavior not only in CLI flags but also in config files. TFLi
44

55
1. File passed by the `--config` option
66
2. File set by the `TFLINT_CONFIG_FILE` environment variable
7-
3. Current directory (`./.tflint.hcl`)
8-
4. Home directory (`~/.tflint.hcl`)
7+
3. Current directory `./.tflint.hcl`
8+
4. Current directory `./.tflint.json`
9+
5. Home directory `~/.tflint.hcl`
10+
6. Home directory `~/.tflint.json`
911

10-
The config file is written in [HCL](https://github.com/hashicorp/hcl). An example is shown below:
12+
The config file can be written in either [HCL](https://github.com/hashicorp/hcl) or JSON format, determined by the file extension. JSON files use the [HCL-compatible JSON syntax](https://developer.hashicorp.com/terraform/language/syntax/json), following the same structure as Terraform's `.tf.json` files. An HCL example is shown below:
1113

1214
```hcl
1315
tflint {
@@ -42,10 +44,47 @@ rule "aws_instance_invalid_type" {
4244
}
4345
```
4446

47+
The same configuration can be written in JSON format as `.tflint.json`:
48+
49+
```json
50+
{
51+
"tflint": {
52+
"required_version": ">= 0.50"
53+
},
54+
"config": {
55+
"format": "compact",
56+
"plugin_dir": "~/.tflint.d/plugins",
57+
"call_module_type": "local",
58+
"force": false,
59+
"disabled_by_default": false,
60+
"ignore_module": {
61+
"terraform-aws-modules/vpc/aws": true,
62+
"terraform-aws-modules/security-group/aws": true
63+
},
64+
"varfile": ["example1.tfvars", "example2.tfvars"],
65+
"variables": ["foo=bar", "bar=[\"baz\"]"]
66+
},
67+
"plugin": {
68+
"aws": {
69+
"enabled": true,
70+
"version": "0.4.0",
71+
"source": "github.com/terraform-linters/tflint-ruleset-aws"
72+
}
73+
},
74+
"rule": {
75+
"aws_instance_invalid_type": {
76+
"enabled": false
77+
}
78+
}
79+
}
80+
```
81+
4582
The file path is resolved relative to the module directory when `--chdir` or `--recursive` is used. To use a config file from the working directory when recursing, pass an absolute path:
4683

4784
```sh
4885
tflint --recursive --config "$(pwd)/.tflint.hcl"
86+
# or
87+
tflint --recursive --config "$(pwd)/.tflint.json"
4988
```
5089

5190
### `required_version`

integrationtest/cli/cli_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,21 @@ func TestIntegration(t *testing.T) {
7171
status: cmd.ExitCodeOK,
7272
stdout: "[]",
7373
},
74+
{
75+
name: "JSON format config",
76+
command: "./tflint",
77+
dir: "json_config",
78+
status: cmd.ExitCodeOK,
79+
stdout: `<?xml version="1.0" encoding="UTF-8"?>
80+
<checkstyle></checkstyle>`,
81+
},
82+
{
83+
name: "HCL precedence over JSON config",
84+
command: "./tflint",
85+
dir: "hcl_json_precedence",
86+
status: cmd.ExitCodeOK,
87+
stdout: "",
88+
},
7489
{
7590
name: "`--force` option with no issues",
7691
command: "./tflint --force",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
config {
2+
format = "compact"
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"config": {
3+
"format": "checkstyle"
4+
}
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"config": {
3+
"format": "checkstyle"
4+
}
5+
}

tflint/config.go

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import (
2020
)
2121

2222
var defaultConfigFile = ".tflint.hcl"
23+
var defaultConfigFileJSON = ".tflint.json"
2324
var fallbackConfigFile = "~/.tflint.hcl"
25+
var fallbackConfigFileJSON = "~/.tflint.json"
2426

2527
var configSchema = &hcl.BodySchema{
2628
Blocks: []hcl.BlockHeaderSchema{
@@ -136,11 +138,15 @@ func EmptyConfig() *Config {
136138
//
137139
// 1. file passed by the --config option
138140
// 2. file set by the TFLINT_CONFIG_FILE environment variable
139-
// 3. current directory (./.tflint.hcl)
140-
// 4. home directory (~/.tflint.hcl)
141+
// 3. current directory ./.tflint.hcl
142+
// 4. current directory ./.tflint.json
143+
// 5. home directory ~/.tflint.hcl
144+
// 6. home directory ~/.tflint.json
141145
//
142-
// For 1 and 2, if the file does not exist, an error will be returned immediately.
143-
// If 3 fails, fallback to 4, and If it fails, an empty configuration is returned.
146+
// Files are parsed as HCL or JSON based on their file extension.
147+
// JSON files use HCL-compatible JSON syntax, following Terraform's .tf.json conventions.
148+
// For steps 1-2, if the file does not exist, an error will be returned immediately.
149+
// For steps 3-6, each step is tried in order until a file is found or all fail.
144150
//
145151
// It also automatically enables bundled plugin if the "terraform"
146152
// plugin block is not explicitly declared.
@@ -174,7 +180,7 @@ func LoadConfig(fs afero.Afero, file string) (*Config, error) {
174180
return cfg.enableBundledPlugin(), nil
175181
}
176182

177-
// Load the default config file
183+
// Load the default config file (prefer .hcl over .json)
178184
log.Printf("[INFO] Load config: %s", defaultConfigFile)
179185
if f, err := fs.Open(defaultConfigFile); err == nil {
180186
cfg, err := loadConfig(f)
@@ -185,7 +191,18 @@ func LoadConfig(fs afero.Afero, file string) (*Config, error) {
185191
}
186192
log.Printf("[INFO] file not found")
187193

188-
// Load the fallback config file
194+
// Try JSON config file if HCL not found
195+
log.Printf("[INFO] Load config: %s", defaultConfigFileJSON)
196+
if f, err := fs.Open(defaultConfigFileJSON); err == nil {
197+
cfg, err := loadConfig(f)
198+
if err != nil {
199+
return nil, err
200+
}
201+
return cfg.enableBundledPlugin(), nil
202+
}
203+
log.Printf("[INFO] file not found")
204+
205+
// Load the fallback config file (prefer .hcl over .json)
189206
fallback, err := homedir.Expand(fallbackConfigFile)
190207
if err != nil {
191208
return nil, err
@@ -200,6 +217,21 @@ func LoadConfig(fs afero.Afero, file string) (*Config, error) {
200217
}
201218
log.Printf("[INFO] file not found")
202219

220+
// Try JSON fallback config file if HCL not found
221+
fallbackJSON, err := homedir.Expand(fallbackConfigFileJSON)
222+
if err != nil {
223+
return nil, err
224+
}
225+
log.Printf("[INFO] Load config: %s", fallbackJSON)
226+
if f, err := fs.Open(fallbackJSON); err == nil {
227+
cfg, err := loadConfig(f)
228+
if err != nil {
229+
return nil, err
230+
}
231+
return cfg.enableBundledPlugin(), nil
232+
}
233+
log.Printf("[INFO] file not found")
234+
203235
// Use the default config
204236
log.Print("[INFO] Use default config")
205237
return EmptyConfig().enableBundledPlugin(), nil
@@ -212,7 +244,17 @@ func loadConfig(file afero.File) (*Config, error) {
212244
}
213245

214246
parser := hclparse.NewParser()
215-
f, diags := parser.ParseHCL(src, file.Name())
247+
var f *hcl.File
248+
var diags hcl.Diagnostics
249+
250+
// Parse based on file extension
251+
switch {
252+
case strings.HasSuffix(strings.ToLower(file.Name()), ".json"):
253+
f, diags = parser.ParseJSON(src, file.Name())
254+
default:
255+
f, diags = parser.ParseHCL(src, file.Name())
256+
}
257+
216258
if diags.HasErrors() {
217259
return nil, diags
218260
}

0 commit comments

Comments
 (0)