diff --git a/helper/runner.go b/helper/runner.go index 41428c5..713fa9f 100644 --- a/helper/runner.go +++ b/helper/runner.go @@ -17,13 +17,30 @@ import ( // Runner is a mock that satisfies the Runner interface for plugin testing. type Runner struct { - Files map[string]*hcl.File + files map[string]*hcl.File Issues Issues tfconfig *configs.Config config Config } +// NewLocalRunner initialises a new test runner. +// Internal use only. +func NewLocalRunner(files map[string]*hcl.File, issues Issues) *Runner { + return &Runner{files: map[string]*hcl.File{}, Issues: issues} +} + +// AddLocalFile adds a new file to the current mapped files. +// Internal use only. +func (r *Runner) AddLocalFile(name string, file *hcl.File) bool { + if _, exists := r.files[name]; exists { + return false + } + + r.files[name] = file + return true +} + // Config is a pseudo TFLint config file object for testing from plugins. type Config struct { Rules []RuleConfig `hcl:"rule,block"` @@ -134,7 +151,12 @@ func (r *Runner) Config() (*configs.Config, error) { // File returns the hcl.File object func (r *Runner) File(filename string) (*hcl.File, error) { - return r.Files[filename], nil + return r.files[filename], nil +} + +// Files returns a map[string]hcl.File object +func (r *Runner) Files() (map[string]*hcl.File, error) { + return r.files, nil } // RootProvider returns the provider configuration. @@ -264,7 +286,7 @@ func (r *Runner) initFromFiles() error { }, } - for _, file := range r.Files { + for _, file := range r.files { content, diags := file.Body.Content(configFileSchema) if diags.HasErrors() { return diags diff --git a/helper/runner_test.go b/helper/runner_test.go index 229388a..12775aa 100644 --- a/helper/runner_test.go +++ b/helper/runner_test.go @@ -471,3 +471,31 @@ func Test_EnsureNoError(t *testing.T) { t.Fatal("Expected to exec the passed proc, but doesn't") } } + +func Test_Files(t *testing.T) { + var sources = map[string]string{ + "main.tf": ` + resource "aws_instance" "foo" { + instance_type = "t2.micro" + }`, + "outputs.tf": ` + output "dummy" { + value = "test" + }`, + "providers.tf": ` + provider "aws" { + region = "us-east-1" + }`, + } + + runner := TestRunner(t, sources) + + files, err := runner.Files() + if err != nil { + t.Fatalf("The response has an unexpected error: %s", err) + } + + if !cmp.Equal(len(sources), len(files)) { + t.Fatalf("Sources and Files differ: %s", cmp.Diff(sources, files)) + } +} diff --git a/helper/testing.go b/helper/testing.go index 49d3a8e..13f7b93 100644 --- a/helper/testing.go +++ b/helper/testing.go @@ -16,7 +16,7 @@ import ( // TestRunner returns a mock Runner for testing. // You can pass the map of file names and their contents in the second argument. func TestRunner(t *testing.T, files map[string]string) *Runner { - runner := &Runner{Files: map[string]*hcl.File{}, Issues: Issues{}} + runner := NewLocalRunner(map[string]*hcl.File{}, Issues{}) parser := hclparse.NewParser() for name, src := range files { @@ -32,7 +32,7 @@ func TestRunner(t *testing.T, files map[string]string) *Runner { } runner.config = config } else { - runner.Files[name] = file + runner.AddLocalFile(name, file) } } diff --git a/tflint/client/client.go b/tflint/client/client.go index 2e5b985..b7d2ebe 100644 --- a/tflint/client/client.go +++ b/tflint/client/client.go @@ -186,6 +186,26 @@ func (c *Client) File(filename string) (*hcl.File, error) { return file, nil } +// Files calls the server-side Files method and returns a collection of hcl.File object. +func (c *Client) Files() (map[string]*hcl.File, error) { + var response FilesResponse + if err := c.rpcClient.Call("Plugin.Files", FilesRequest{}, &response); err != nil { + return nil, err + } + + files := make(map[string]*hcl.File) + for filename, content := range response.Files { + file, diags := parseConfig(content, filename, hcl.InitialPos) + + if diags.HasErrors() { + return nil, diags + } + + files[filename] = file + } + return files, nil +} + // RootProvider calls the server-side RootProvider method and returns the provider configuration. func (c *Client) RootProvider(name string) (*configs.Provider, error) { log.Printf("[DEBUG] Accessing to the `%s` provider config in the root module", name) diff --git a/tflint/client/rpc.go b/tflint/client/rpc.go index d7153b8..1264f98 100644 --- a/tflint/client/rpc.go +++ b/tflint/client/rpc.go @@ -78,6 +78,15 @@ type FileResponse struct { Range hcl.Range } +// FilesRequest is a request to the server-side Files method. +type FilesRequest struct{} + +// FilesResponse is a response to the server-side Files method. +type FilesResponse struct { + Files map[string][]byte + Err error +} + // RootProviderRequest is a request to the server-side RootProvider method. type RootProviderRequest struct { Name string diff --git a/tflint/interface.go b/tflint/interface.go index c143bb2..3e538d9 100644 --- a/tflint/interface.go +++ b/tflint/interface.go @@ -59,6 +59,10 @@ type Runner interface { // When accessing resources, expressions, etc, it is recommended to use high-level APIs. File(string) (*hcl.File, error) + // Files returns a map[string]hcl.File object, where the key is the file name. + // This is low level API for accessing information such as comments and syntax. + Files() (map[string]*hcl.File, error) + // RootProvider returns the provider configuration in the root module. // It can be used by child modules to access the credentials defined in the root module. RootProvider(name string) (*configs.Provider, error) diff --git a/tflint/server/server.go b/tflint/server/server.go index 790c1db..67c7e85 100644 --- a/tflint/server/server.go +++ b/tflint/server/server.go @@ -11,4 +11,5 @@ type Server interface { Backend(*client.BackendRequest, *client.BackendResponse) error EvalExpr(*client.EvalExprRequest, *client.EvalExprResponse) error EmitIssue(*client.EmitIssueRequest, *interface{}) error + Files(*client.FilesRequest, *client.FilesResponse) error }