Skip to content

Commit

Permalink
Add new 'module.callers' command
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko committed May 13, 2021
1 parent 4b7771b commit 1e4f20a
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 0 deletions.
29 changes: 29 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Commands

The server exposes the following executable commands via LSP to clients.
Typically these commands are not invokable by end-users automatically.
Instead this serves as a documentation for client maintainers,
and clients may expose these e.g. via command palette where appropriate.

Every care is taken to avoid breaking changes, but these interfaces
should not be considered stable yet and may change.

Either way clients should always follow LSP spec in the sense
that they check whether a command is actually supported or not
(via `ServerCapabilities.executeCommandProvider.commands`).

## `terraform.init`

TODO - inputs, output, description

## `terraform.validate`

TODO - inputs, output, description

## `module.callers`

TODO - inputs, output, description

## `rootmodules` (DEPRECATED, use `module.callers` instead)

TODO - inputs, output, description
61 changes: 61 additions & 0 deletions internal/langserver/handlers/command/module_callers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package command

import (
"context"
"fmt"
"sort"

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

const moduleCallersVersion = 0

type moduleCallersResponse struct {
FormatVersion int `json:"v"`
Callers []moduleCaller `json:"callers"`
}

type moduleCaller struct {
URI string `json:"uri"`
Name string `json:"name"`
}

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

modPath, err := uri.PathFromURI(modUri)
if err != nil {
return nil, err
}

mf, err := lsctx.ModuleFinder(ctx)
if err != nil {
return nil, err
}

modCallers, err := mf.CallersOfModule(modPath)
if err != nil {
return nil, err
}

callers := make([]moduleCaller, 0)
for _, caller := range modCallers {
callers = append(callers, moduleCaller{
URI: uri.FromPath(caller.Path),
Name: humanReadablePath(modPath, caller.Path),
})
}
sort.SliceStable(callers, func(i, j int) bool {
return callers[i].URI < callers[j].URI
})
return moduleCallersResponse{
FormatVersion: moduleCallersVersion,
Callers: callers,
}, nil
}
1 change: 1 addition & 0 deletions internal/langserver/handlers/execute_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

var handlers = cmd.Handlers{
cmd.Name("rootmodules"): command.ModulesHandler,
cmd.Name("module.callers"): command.ModuleCallersHandler,
cmd.Name("terraform.init"): command.TerraformInitHandler,
cmd.Name("terraform.validate"): command.TerraformValidateHandler,
}
Expand Down
164 changes: 164 additions & 0 deletions internal/langserver/handlers/execute_command_module_callers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package handlers

import (
"fmt"
"os"
"path/filepath"
"testing"

"github.com/creachadair/jrpc2/code"
"github.com/hashicorp/terraform-ls/internal/langserver"
"github.com/hashicorp/terraform-ls/internal/langserver/cmd"
"github.com/hashicorp/terraform-ls/internal/terraform/exec"
"github.com/hashicorp/terraform-ls/internal/uri"
"github.com/stretchr/testify/mock"
)

func TestLangServer_workspaceExecuteCommand_moduleCallers_argumentError(t *testing.T) {
rootDir := t.TempDir()
rootUri := uri.FromPath(rootDir)

ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{
TerraformCalls: &exec.TerraformMockCalls{
PerWorkDir: map[string][]*mock.Call{
rootDir: validTfMockCalls(),
},
},
}))
stop := ls.Start(t)
defer stop()

ls.Call(t, &langserver.CallRequest{
Method: "initialize",
ReqParams: fmt.Sprintf(`{
"capabilities": {},
"rootUri": %q,
"processId": 12345
}`, rootUri)})
ls.Notify(t, &langserver.CallRequest{
Method: "initialized",
ReqParams: "{}",
})
ls.Call(t, &langserver.CallRequest{
Method: "textDocument/didOpen",
ReqParams: fmt.Sprintf(`{
"textDocument": {
"version": 0,
"languageId": "terraform",
"text": "provider \"github\" {}",
"uri": %q
}
}`, fmt.Sprintf("%s/main.tf", rootUri))})

ls.CallAndExpectError(t, &langserver.CallRequest{
Method: "workspace/executeCommand",
ReqParams: fmt.Sprintf(`{
"command": %q
}`, cmd.Name("module.callers"))}, code.InvalidParams.Err())
}

func TestLangServer_workspaceExecuteCommand_moduleCallers_basic(t *testing.T) {
rootDir := t.TempDir()
rootUri := uri.FromPath(rootDir)
baseDirUri := uri.FromPath(filepath.Join(rootDir, "base"))

createModuleCalling(t, "../base", filepath.Join(rootDir, "dev"))
createModuleCalling(t, "../base", filepath.Join(rootDir, "staging"))
createModuleCalling(t, "../base", filepath.Join(rootDir, "prod"))

ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{
TerraformCalls: &exec.TerraformMockCalls{
PerWorkDir: map[string][]*mock.Call{
rootDir: validTfMockCalls(),
},
},
}))
stop := ls.Start(t)
defer stop()

ls.Call(t, &langserver.CallRequest{
Method: "initialize",
ReqParams: fmt.Sprintf(`{
"capabilities": {},
"rootUri": %q,
"processId": 12345
}`, rootUri)})
ls.Notify(t, &langserver.CallRequest{
Method: "initialized",
ReqParams: "{}",
})
ls.Call(t, &langserver.CallRequest{
Method: "textDocument/didOpen",
ReqParams: fmt.Sprintf(`{
"textDocument": {
"version": 0,
"languageId": "terraform",
"text": "provider \"github\" {}",
"uri": %q
}
}`, fmt.Sprintf("%s/main.tf", baseDirUri))})

ls.CallAndExpectResponse(t, &langserver.CallRequest{
Method: "workspace/executeCommand",
ReqParams: fmt.Sprintf(`{
"command": %q,
"arguments": ["uri=%s"]
}`, cmd.Name("module.callers"), baseDirUri)}, fmt.Sprintf(`{
"jsonrpc": "2.0",
"id": 3,
"result": {
"v": 0,
"callers": [
{
"uri": "%s/dev",
"name": "../dev"
},
{
"uri": "%s/prod",
"name": "../prod"
},
{
"uri": "%s/staging",
"name": "../staging"
}
]
}
}`, rootUri, rootUri, rootUri))
}

func createModuleCalling(t *testing.T, src, modPath string) {
modulesDir := filepath.Join(modPath, ".terraform", "modules")
err := os.MkdirAll(modulesDir, 0755)
if err != nil {
t.Fatal(err)
}

configBytes := []byte(fmt.Sprintf(`
module "local" {
source = %q
}
`, src))
err = os.WriteFile(filepath.Join(modPath, "module.tf"), configBytes, 0755)
if err != nil {
t.Fatal(err)
}

manifestBytes := []byte(fmt.Sprintf(`{
"Modules": [
{
"Key": "",
"Source": "",
"Dir": "."
},
{
"Key": "local",
"Source": %q,
"Dir": %q
}
]
}`, src, src))
err = os.WriteFile(filepath.Join(modulesDir, "modules.json"), manifestBytes, 0755)
if err != nil {
t.Fatal(err)
}
}
1 change: 1 addition & 0 deletions internal/langserver/handlers/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) {

ctx = lsctx.WithCommandPrefix(ctx, &commandPrefix)
ctx = lsctx.WithModuleManager(ctx, svc.modMgr)
ctx = lsctx.WithModuleFinder(ctx, svc.modMgr)
ctx = lsctx.WithModuleWalker(ctx, svc.walker)
ctx = lsctx.WithWatcher(ctx, ww)
ctx = lsctx.WithRootDirectory(ctx, &rootDir)
Expand Down
14 changes: 14 additions & 0 deletions internal/terraform/module/module_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,20 @@ func (mm *moduleManager) SchemaForModule(modPath string) (*schema.BodySchema, er
return sm.SchemaForModule(meta)
}

func (mm *moduleManager) CallersOfModule(modPath string) ([]Module, error) {
modules := make([]Module, 0)
callers, err := mm.moduleStore.CallersOfModule(modPath)
if err != nil {
return modules, err
}

for _, mod := range callers {
modules = append(modules, mod)
}

return modules, nil
}

// SchemaSourcesForModule is DEPRECATED and should NOT be used anymore
// it is just maintained for backwards compatibility in the "rootmodules"
// custom LSP command which itself will be DEPRECATED as external parties
Expand Down
1 change: 1 addition & 0 deletions internal/terraform/module/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type ModuleFinder interface {
SchemaForModule(path string) (*schema.BodySchema, error)
SchemaSourcesForModule(path string) ([]SchemaSource, error)
ListModules() ([]Module, error)
CallersOfModule(modPath string) ([]Module, error)
}

type ModuleLoader func(dir string) (Module, error)
Expand Down

0 comments on commit 1e4f20a

Please sign in to comment.