Skip to content

Commit

Permalink
Support Terraform Validate Command (#323)
Browse files Browse the repository at this point in the history
* Implement module validation on save

* go mod tidy

* gofmt

* Adapt validation to command

* Use tfjson diagnostic type

Bump tfjson and terraform-schema

* update ref

* Add tests regarding diag source cache

* Apply suggestions from code review

Co-authored-by: Radek Simko <radek.simko@gmail.com>

* fix var

* bump terraform-schema

* clean up internals with typedefs, surface WasInitialized error

Co-authored-by: Radek Simko <radek.simko@gmail.com>
  • Loading branch information
appilon and radeksimko authored Dec 8, 2020
1 parent f02b7fa commit 87c4e22
Show file tree
Hide file tree
Showing 14 changed files with 353 additions and 36 deletions.
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ require (
github.com/creachadair/jrpc2 v0.11.0
github.com/fsnotify/fsnotify v1.4.9
github.com/gammazero/workerpool v1.0.0
github.com/google/go-cmp v0.5.1
github.com/google/go-cmp v0.5.2
github.com/google/uuid v1.1.2
github.com/hashicorp/go-multierror v1.1.0
github.com/hashicorp/go-version v1.2.1
github.com/hashicorp/hcl-lang v0.0.0-20201116081236-948e43712a65
github.com/hashicorp/hcl/v2 v2.6.0
github.com/hashicorp/terraform-exec v0.11.1-0.20201007122305-ea2094d52cb5
github.com/hashicorp/terraform-json v0.6.0
github.com/hashicorp/terraform-schema v0.0.0-20201204171308-0c9744a02c65
github.com/hashicorp/terraform-exec v0.11.1-0.20201207223938-9186a7c3bb24
github.com/hashicorp/terraform-json v0.7.0
github.com/hashicorp/terraform-schema v0.0.0-20201208163444-44d0347ab290
github.com/mh-cbon/go-fmt-fail v0.0.0-20160815164508-67765b3fbcb5
github.com/mitchellh/cli v1.1.1
github.com/mitchellh/go-homedir v1.1.0
Expand All @@ -24,6 +24,6 @@ require (
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546
github.com/spf13/afero v1.3.2
github.com/stretchr/testify v1.4.0
github.com/stretchr/testify v1.6.1
github.com/vektra/mockery/v2 v2.3.0
)
22 changes: 14 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
Expand Down Expand Up @@ -196,14 +198,14 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/terraform-config-inspect v0.0.0-20201102131242-0c45ba392e51 h1:SEGO1vz/pFLfKy4QpABIMCe7wffmtsOiWO4yc1E87cU=
github.com/hashicorp/terraform-config-inspect v0.0.0-20201102131242-0c45ba392e51/go.mod h1:Z0Nnk4+3Cy89smEbrq+sl1bxc9198gIP4I7wcQF6Kqs=
github.com/hashicorp/terraform-exec v0.11.1-0.20201007122305-ea2094d52cb5 h1:P+lBGicJEG3ijvOrDdQf/Oo8UrG4QAJbdY3g9OGBnr0=
github.com/hashicorp/terraform-exec v0.11.1-0.20201007122305-ea2094d52cb5/go.mod h1:eQdBvA0Xr/ZJNilY8TzrtePLSqLyexk9PSwVwzzHTjY=
github.com/hashicorp/terraform-json v0.5.0 h1:7TV3/F3y7QVSuN4r9BEXqnWqrAyeOtON8f0wvREtyzs=
github.com/hashicorp/terraform-json v0.5.0/go.mod h1:eAbqb4w0pSlRmdvl8fOyHAi/+8jnkVYN28gJkSJrLhU=
github.com/hashicorp/terraform-json v0.6.0 h1:nMTj4t9ysC7xJ72rvVsDqhUccvbUINrjhPqafeUeREk=
github.com/hashicorp/terraform-json v0.6.0/go.mod h1:eAbqb4w0pSlRmdvl8fOyHAi/+8jnkVYN28gJkSJrLhU=
github.com/hashicorp/terraform-schema v0.0.0-20201204171308-0c9744a02c65 h1:CFZI/uaZCoAN0SI4A9edORfj+tnq+Hg0v9PH2AZ+1tA=
github.com/hashicorp/terraform-schema v0.0.0-20201204171308-0c9744a02c65/go.mod h1:tU5zEQwdtUJbMQXh5+s0TnBOFshXSAcdAx8Ki8p2S4w=
github.com/hashicorp/terraform-exec v0.11.1-0.20201207223938-9186a7c3bb24 h1:kMl+qKoUCH6Uj8wzlT+ovHMbJAFJgY1Zw6ek1yHwWyo=
github.com/hashicorp/terraform-exec v0.11.1-0.20201207223938-9186a7c3bb24/go.mod h1:o1PZgrOpDMiDeKT0Hcr2oo6KoVKyaeTN867jcaXMg4E=
github.com/hashicorp/terraform-json v0.7.0 h1:DgkfLARKMQ/xmzVtSRX9Vz/fzPCL3vskHIgj6s+SQwQ=
github.com/hashicorp/terraform-json v0.7.0/go.mod h1:3defM4kkMfttwiE7VakJDwCd4R+umhSQnvJwORXbprE=
github.com/hashicorp/terraform-schema v0.0.0-20201208004742-b5e321a36f41 h1:FvVpjaQJXT9AH70Vs9i90QDTLz92BsL69N9kaLGK8qE=
github.com/hashicorp/terraform-schema v0.0.0-20201208004742-b5e321a36f41/go.mod h1:eRHMO4QL4TTka07aC7fH+AXvi/tYlv6udrA8nSFOl6g=
github.com/hashicorp/terraform-schema v0.0.0-20201208163444-44d0347ab290 h1:kAs5ZG+cgtWy3+81Z7G/Blj2imiDLfFBRaqmNs8mD4o=
github.com/hashicorp/terraform-schema v0.0.0-20201208163444-44d0347ab290/go.mod h1:eRHMO4QL4TTka07aC7fH+AXvi/tYlv6udrA8nSFOl6g=
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 h1:HKLsbzeOsfXmKNpr3GiT18XAblV0BjCbzL8KQAMZGa0=
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg=
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
Expand Down Expand Up @@ -351,6 +353,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
Expand Down Expand Up @@ -551,6 +555,8 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
65 changes: 49 additions & 16 deletions internal/langserver/diagnostics/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,39 @@ import (
type diagContext struct {
ctx context.Context
uri lsp.DocumentURI
diags hcl.Diagnostics
source string
diags []lsp.Diagnostic
}

type diagnosticSource string

type fileDiagnostics map[diagnosticSource][]lsp.Diagnostic

// Notifier is a type responsible for queueing hcl diagnostics to be converted
// and sent to the client
type Notifier struct {
logger *log.Logger
sessCtx context.Context
diags chan diagContext
diagsCache map[lsp.DocumentURI]fileDiagnostics
closeDiagsOnce sync.Once
}

func NewNotifier(sessCtx context.Context, logger *log.Logger) *Notifier {
diags := make(chan diagContext, 50)
go notify(diags, logger)
return &Notifier{sessCtx: sessCtx, diags: diags}
n := &Notifier{
logger: logger,
sessCtx: sessCtx,
diags: make(chan diagContext, 50),
diagsCache: make(map[lsp.DocumentURI]fileDiagnostics),
}
go n.notify()
return n
}

// Publish accepts a map of diagnostics per file and queues them for publishing
func (n *Notifier) Publish(ctx context.Context, rmDir string, diags map[string]hcl.Diagnostics, source string) {
// PublishHCLDiags accepts a map of hcl diagnostics per file and queues them for publishing.
// A dir path is passed which is joined with the filename keys of the map, to form a file URI.
// A source string is passed and set for each diagnostic, this is typically displayed in the client UI.
func (n *Notifier) PublishHCLDiags(ctx context.Context, dirPath string, diags map[string]hcl.Diagnostics, source string) {
select {
case <-n.sessCtx.Done():
n.closeDiagsOnce.Do(func() {
Expand All @@ -45,22 +58,42 @@ func (n *Notifier) Publish(ctx context.Context, rmDir string, diags map[string]h
default:
}

if source == "" {
source = "Terraform"
}

for path, ds := range diags {
n.diags <- diagContext{ctx: ctx, diags: ds, source: source, uri: lsp.DocumentURI(uri.FromPath(filepath.Join(rmDir, path)))}
for filename, ds := range diags {
n.diags <- diagContext{
ctx: ctx, source: source,
diags: ilsp.HCLDiagsToLSP(ds, source),
uri: lsp.DocumentURI(uri.FromPath(filepath.Join(dirPath, filename))),
}
}
}

func notify(diags <-chan diagContext, logger *log.Logger) {
for d := range diags {
func (n *Notifier) notify() {
for d := range n.diags {
if err := jrpc2.PushNotify(d.ctx, "textDocument/publishDiagnostics", lsp.PublishDiagnosticsParams{
URI: d.uri,
Diagnostics: ilsp.HCLDiagsToLSP(d.diags, d.source),
Diagnostics: n.mergeDiags(d.uri, d.source, d.diags),
}); err != nil {
logger.Printf("Error pushing diagnostics: %s", err)
n.logger.Printf("Error pushing diagnostics: %s", err)
}
}
}

// mergeDiags will return all diags from all cached sources for a given uri.
// the passed diags overwrites the cached entry for the passed source key
// even if empty
func (n *Notifier) mergeDiags(uri lsp.DocumentURI, source string, diags []lsp.Diagnostic) []lsp.Diagnostic {

fileDiags, ok := n.diagsCache[uri]
if !ok {
fileDiags = make(fileDiagnostics)
}

fileDiags[diagnosticSource(source)] = diags
n.diagsCache[uri] = fileDiags

all := []lsp.Diagnostic{}
for _, diags := range fileDiags {
all = append(all, diags...)
}
return all
}
75 changes: 73 additions & 2 deletions internal/langserver/diagnostics/diagnostics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"github.com/hashicorp/hcl/v2"
lsp "github.com/hashicorp/terraform-ls/internal/protocol"
)

var discardLogger = log.New(ioutil.Discard, "", 0)
Expand All @@ -16,7 +17,7 @@ func TestDiags_Closes(t *testing.T) {
n := NewNotifier(ctx, discardLogger)

cancel()
n.Publish(context.Background(), "", map[string]hcl.Diagnostics{
n.PublishHCLDiags(context.Background(), "", map[string]hcl.Diagnostics{
"test": {
{
Severity: hcl.DiagError,
Expand All @@ -40,11 +41,81 @@ func TestPublish_DoesNotSendAfterClose(t *testing.T) {
n := NewNotifier(ctx, discardLogger)

cancel()
n.Publish(context.Background(), "", map[string]hcl.Diagnostics{
n.PublishHCLDiags(context.Background(), "", map[string]hcl.Diagnostics{
"test": {
{
Severity: hcl.DiagError,
},
},
}, "test")
}

func TestMergeDiags_CachesMultipleSourcesPerURI(t *testing.T) {
uri := lsp.DocumentURI("test.tf")

n := NewNotifier(context.Background(), discardLogger)

all := n.mergeDiags(uri, "source1", []lsp.Diagnostic{
{
Severity: lsp.SeverityError,
Message: "diag1",
},
})
if len(all) != 1 {
t.Fatalf("returns diags is incorrect length: expected %d, got %d", 1, len(all))
}

all = n.mergeDiags(uri, "source2", []lsp.Diagnostic{
{
Severity: lsp.SeverityError,
Message: "diag2",
},
})
if len(all) != 2 {
t.Fatalf("returns diags is incorrect length: expected %d, got %d", 2, len(all))
}
}

func TestMergeDiags_OverwritesSource_EvenWithEmptySlice(t *testing.T) {
uri := lsp.DocumentURI("test.tf")

n := NewNotifier(context.Background(), discardLogger)

all := n.mergeDiags(uri, "source1", []lsp.Diagnostic{
{
Severity: lsp.SeverityError,
Message: "diag1",
},
})
if len(all) != 1 {
t.Fatalf("returns diags is incorrect length: expected %d, got %d", 1, len(all))
}

all = n.mergeDiags(uri, "source1", []lsp.Diagnostic{
{
Severity: lsp.SeverityError,
Message: "diagOverwritten",
},
})
if len(all) != 1 {
t.Fatalf("returns diags is incorrect length: expected %d, got %d", 1, len(all))
}
if all[0].Message != "diagOverwritten" {
t.Fatalf("diag has incorrect message: expected %s, got %s", "diagOverwritten", all[0].Message)
}

all = n.mergeDiags(uri, "source2", []lsp.Diagnostic{
{
Severity: lsp.SeverityError,
Message: "diag2",
},
})
if len(all) != 2 {
t.Fatalf("returns diags is incorrect length: expected %d, got %d", 2, len(all))
}

all = n.mergeDiags(uri, "source2", []lsp.Diagnostic{})
if len(all) != 1 {
t.Fatalf("returns diags is incorrect length: expected %d, got %d", 1, len(all))
}
}
52 changes: 52 additions & 0 deletions internal/langserver/handlers/command/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package command

import (
"context"
"fmt"

"github.com/creachadair/jrpc2/code"
lsctx "github.com/hashicorp/terraform-ls/internal/context"
"github.com/hashicorp/terraform-ls/internal/langserver/cmd"
ilsp "github.com/hashicorp/terraform-ls/internal/lsp"
lsp "github.com/hashicorp/terraform-ls/internal/protocol"
)

func TerraformValidateHandler(ctx context.Context, args cmd.CommandArgs) (interface{}, error) {
dirUri, ok := args.GetString("uri")
if !ok || dirUri == "" {
return nil, fmt.Errorf("%w: expected dir uri argument to be set", code.InvalidParams.Err())
}

dh := ilsp.FileHandlerFromDirURI(lsp.DocumentURI(dirUri))

cf, err := lsctx.RootModuleFinder(ctx)
if err != nil {
return nil, err
}

rm, err := cf.RootModuleByPath(dh.Dir())
if err != nil {
return nil, err
}

wasInit, err := rm.WasInitialized()
if err != nil {
return nil, fmt.Errorf("error checking if %s was initialized: %s", dirUri, err)
}
if !wasInit {
return nil, fmt.Errorf("%s is not an initialized module, terraform validate cannot be called", dirUri)
}

diags, err := lsctx.Diagnostics(ctx)
if err != nil {
return nil, err
}

hclDiags, err := rm.ExecuteTerraformValidate(ctx)
if err != nil {
return nil, err
}
diags.PublishHCLDiags(ctx, rm.Path(), hclDiags, "terraform validate")

return nil, nil
}
2 changes: 1 addition & 1 deletion internal/langserver/handlers/did_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func TextDocumentDidChange(ctx context.Context, params lsp.DidChangeTextDocument
if err != nil {
return err
}
diags.Publish(ctx, rm.Path(), rm.ParsedDiagnostics(), "HCL")
diags.PublishHCLDiags(ctx, rm.Path(), rm.ParsedDiagnostics(), "HCL")

return nil
}
2 changes: 1 addition & 1 deletion internal/langserver/handlers/did_open.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func (lh *logHandler) TextDocumentDidOpen(ctx context.Context, params lsp.DidOpe
if err != nil {
return err
}
diags.Publish(ctx, rm.Path(), rm.ParsedDiagnostics(), "HCL")
diags.PublishHCLDiags(ctx, rm.Path(), rm.ParsedDiagnostics(), "HCL")

candidates := rmm.RootModuleCandidatesByPath(f.Dir())

Expand Down
5 changes: 3 additions & 2 deletions internal/langserver/handlers/execute_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import (
)

var handlers = cmd.Handlers{
cmd.Name("rootmodules"): command.RootModulesHandler,
cmd.Name("terraform.init"): command.TerraformInitHandler,
cmd.Name("rootmodules"): command.RootModulesHandler,
cmd.Name("terraform.init"): command.TerraformInitHandler,
cmd.Name("terraform.validate"): command.TerraformValidateHandler,
}

func (lh *logHandler) WorkspaceExecuteCommand(ctx context.Context, params lsp.ExecuteCommandParams) (interface{}, error) {
Expand Down
Loading

0 comments on commit 87c4e22

Please sign in to comment.