diff --git a/cmd/inspect.go b/cmd/inspect.go index d17fd06d9..9cffadb91 100644 --- a/cmd/inspect.go +++ b/cmd/inspect.go @@ -104,7 +104,7 @@ func (cli *CLI) inspectModule(opts Options, dir string, filterFiles []string) (t } // Setup runners - rootRunner, moduleRunners, err := cli.setupRunners(opts, dir) + rootRunner, moduleRunners, err := tflint.BuildRunners(cli.loader, cli.config, cli.originalWorkingDir, dir) if err != nil { return issues, changes, err } @@ -196,59 +196,6 @@ By setting TFLINT_LOG=trace, you can confirm the changes made by the autofix and return issues, changes, nil } -func (cli *CLI) setupRunners(_ Options, dir string) (*tflint.Runner, []*tflint.Runner, error) { - rootMod, diags := cli.loader.LoadRootModule(dir) - if diags.HasErrors() { - return nil, []*tflint.Runner{}, fmt.Errorf("Failed to load the root module; %w", diags) - } - - files, diags := cli.loader.LoadConfigDirFiles(dir) - if diags.HasErrors() { - return nil, []*tflint.Runner{}, fmt.Errorf("Failed to list configuration files; %w", diags) - } - annotations := map[string]tflint.Annotations{} - for path, file := range files { - ants, lexDiags := tflint.NewAnnotations(path, file) - diags = diags.Extend(lexDiags) - annotations[path] = ants - } - if diags.HasErrors() { - return nil, []*tflint.Runner{}, fmt.Errorf("Failed to parse annotations; %w", diags) - } - - variables, diags := cli.loader.LoadValuesFiles(dir, cli.config.Varfiles...) - if diags.HasErrors() { - return nil, []*tflint.Runner{}, fmt.Errorf("Failed to load values files; %w", diags) - } - cliVars, diags := terraform.ParseVariableValues(cli.config.Variables, rootMod.Variables) - if diags.HasErrors() { - return nil, []*tflint.Runner{}, fmt.Errorf("Failed to parse variables; %w", diags) - } - variables = append(variables, cliVars) - - configs, diags := terraform.BuildConfig( - rootMod, - cli.loader.ModuleWalker(cli.config.CallModuleType), - cli.originalWorkingDir, - variables..., - ) - if diags.HasErrors() { - return nil, []*tflint.Runner{}, fmt.Errorf("Failed to build configurations; %w", diags) - } - - runner, err := tflint.NewRunner(cli.originalWorkingDir, cli.config, annotations, configs, variables...) - if err != nil { - return nil, []*tflint.Runner{}, fmt.Errorf("Failed to initialize a runner; %w", err) - } - - moduleRunners, err := tflint.NewModuleRunners(runner) - if err != nil { - return nil, []*tflint.Runner{}, fmt.Errorf("Failed to prepare rule checking; %w", err) - } - - return runner, moduleRunners, nil -} - func launchPlugins(config *tflint.Config, fix bool) (*plugin.Plugin, error) { // Lookup plugins rulesetPlugin, err := plugin.Discovery(config) diff --git a/langserver/handler.go b/langserver/handler.go index 8fdc10e99..5c982f467 100644 --- a/langserver/handler.go +++ b/langserver/handler.go @@ -171,50 +171,11 @@ func (h *handler) inspect() (map[string][]lsp.Diagnostic, error) { return ret, fmt.Errorf("Failed to prepare loading: %w", err) } - rootMod, diags := loader.LoadRootModule(".") - if diags.HasErrors() { - return ret, fmt.Errorf("Failed to load the root module: %w", diags) - } - files, diags := loader.LoadConfigDirFiles(".") - if diags.HasErrors() { - return ret, fmt.Errorf("Failed to list configuration files: %w", diags) - } - annotations := map[string]tflint.Annotations{} - for path, file := range files { - ants, lexDiags := tflint.NewAnnotations(path, file) - diags = diags.Extend(lexDiags) - annotations[path] = ants - } - - variables, diags := loader.LoadValuesFiles(".", h.config.Varfiles...) - if diags.HasErrors() { - return ret, fmt.Errorf("Failed to load values files: %w", diags) - } - cliVars, diags := terraform.ParseVariableValues(h.config.Variables, rootMod.Variables) - if diags.HasErrors() { - return ret, fmt.Errorf("Failed to parse variables: %w", diags) - } - variables = append(variables, cliVars) - - configs, diags := terraform.BuildConfig( - rootMod, - loader.ModuleWalker(h.config.CallModuleType), - h.rootDir, - variables..., - ) - if diags.HasErrors() { - return ret, fmt.Errorf("Failed to build configurations: %w", diags) - } - - runner, err := tflint.NewRunner(h.rootDir, h.config, annotations, configs, variables...) - if err != nil { - return ret, fmt.Errorf("Failed to initialize a runner: %w", err) - } - runners, err := tflint.NewModuleRunners(runner) + runner, runners, err := tflint.BuildRunners(loader, h.config, h.rootDir, ".") if err != nil { - return ret, fmt.Errorf("Failed to prepare rule checking: %w", err) + return ret, err } - runners = append(runners, runner) + runners = append(runners, runner) // langserver iterates a single slice incl. root config := h.config.ToPluginConfig() for name, ruleset := range h.plugin.RuleSets { diff --git a/tflint/runner_set.go b/tflint/runner_set.go new file mode 100644 index 000000000..bc5ddd78b --- /dev/null +++ b/tflint/runner_set.go @@ -0,0 +1,62 @@ +package tflint + +import ( + "fmt" + + "github.com/terraform-linters/tflint/terraform" +) + +// BuildRunners loads the module rooted at dir using the given loader and config, +// returning the root runner and its module runners. +func BuildRunners(loader *terraform.Loader, config *Config, workingDir, dir string) (*Runner, []*Runner, error) { + rootMod, diags := loader.LoadRootModule(dir) + if diags.HasErrors() { + return nil, []*Runner{}, fmt.Errorf("Failed to load the root module; %w", diags) + } + + files, diags := loader.LoadConfigDirFiles(dir) + if diags.HasErrors() { + return nil, []*Runner{}, fmt.Errorf("Failed to list configuration files; %w", diags) + } + annotations := map[string]Annotations{} + for path, file := range files { + ants, lexDiags := NewAnnotations(path, file) + diags = diags.Extend(lexDiags) + annotations[path] = ants + } + if diags.HasErrors() { + return nil, []*Runner{}, fmt.Errorf("Failed to parse annotations; %w", diags) + } + + variables, diags := loader.LoadValuesFiles(dir, config.Varfiles...) + if diags.HasErrors() { + return nil, []*Runner{}, fmt.Errorf("Failed to load values files; %w", diags) + } + cliVars, diags := terraform.ParseVariableValues(config.Variables, rootMod.Variables) + if diags.HasErrors() { + return nil, []*Runner{}, fmt.Errorf("Failed to parse variables; %w", diags) + } + variables = append(variables, cliVars) + + configs, diags := terraform.BuildConfig( + rootMod, + loader.ModuleWalker(config.CallModuleType), + workingDir, + variables..., + ) + if diags.HasErrors() { + return nil, []*Runner{}, fmt.Errorf("Failed to build configurations; %w", diags) + } + + runner, err := NewRunner(workingDir, config, annotations, configs, variables...) + if err != nil { + return nil, []*Runner{}, fmt.Errorf("Failed to initialize a runner; %w", err) + } + + moduleRunners, err := NewModuleRunners(runner) + if err != nil { + return nil, []*Runner{}, fmt.Errorf("Failed to prepare rule checking; %w", err) + } + + return runner, moduleRunners, nil +} diff --git a/tflint/runner_set_test.go b/tflint/runner_set_test.go new file mode 100644 index 000000000..bc2076360 --- /dev/null +++ b/tflint/runner_set_test.go @@ -0,0 +1,111 @@ +package tflint + +import ( + "os" + "testing" + + "github.com/spf13/afero" + "github.com/terraform-linters/tflint/terraform" + "github.com/zclconf/go-cty/cty" +) + +func TestBuildRunners(t *testing.T) { + for _, tc := range []struct { + name string + files map[string]string + varfiles []string + wantModuleRunners int + wantVariable string + wantVariableValue cty.Value + wantAnnotated string + }{ + { + name: "variable from auto-loaded varfile", + files: map[string]string{ + "main.tf": ` +variable "instance_type" {} + +resource "aws_instance" "main" { + instance_type = var.instance_type // tflint-ignore: aws_instance_invalid_type +} +`, + "custom.auto.tfvars": `instance_type = "t2.micro"`, + }, + wantModuleRunners: 0, + wantVariable: "instance_type", + wantVariableValue: cty.StringVal("t2.micro"), + wantAnnotated: "main.tf", + }, + { + name: "variable from explicit varfile", + files: map[string]string{ + "main.tf": ` +variable "instance_type" {} +`, + "custom.tfvars": `instance_type = "m5.xlarge"`, + }, + varfiles: []string{"custom.tfvars"}, + wantModuleRunners: 0, + wantVariable: "instance_type", + wantVariableValue: cty.StringVal("m5.xlarge"), + }, + { + name: "with local module", + files: map[string]string{ + "main.tf": ` +variable "instance_type" {} + +module "child" { + source = "./module" +} +`, + "module/main.tf": ` +resource "aws_instance" "child" {} +`, + "custom.auto.tfvars": `instance_type = "t2.large"`, + }, + wantModuleRunners: 1, + wantVariable: "instance_type", + wantVariableValue: cty.StringVal("t2.large"), + }, + } { + t.Run(tc.name, func(t *testing.T) { + fs := afero.Afero{Fs: afero.NewMemMapFs()} + for name, src := range tc.files { + if err := fs.WriteFile(name, []byte(src), os.ModePerm); err != nil { + t.Fatal(err) + } + } + + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + loader, err := terraform.NewLoader(fs, wd) + if err != nil { + t.Fatal(err) + } + + config := EmptyConfig() + config.Varfiles = tc.varfiles + + runner, moduleRunners, err := BuildRunners(loader, config, wd, ".") + if err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + if len(moduleRunners) != tc.wantModuleRunners { + t.Errorf("Expected %d module runners, got %d", tc.wantModuleRunners, len(moduleRunners)) + } + + got := runner.Ctx.VariableValues[""][tc.wantVariable] + if !got.RawEquals(tc.wantVariableValue) { + t.Errorf("Expected variable %q to resolve to %#v, got %#v", tc.wantVariable, tc.wantVariableValue, got) + } + + if tc.wantAnnotated != "" && len(runner.annotations[tc.wantAnnotated]) == 0 { + t.Errorf("Expected annotations to be attached for %q, got none", tc.wantAnnotated) + } + }) + } +}