forked from hashicorp/terraform-ls
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add new 'module.callers' command (hashicorp#508)
* decouple remaining URI logic to uri package * Add new 'module.callers' command
- Loading branch information
1 parent
29b3d95
commit 41d49b3
Showing
15 changed files
with
501 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
# 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`). | ||
|
||
## Command Prefix | ||
|
||
All commands use `terraform-ls.` prefix to avoid any conflicts | ||
with commands registered by any other language servers user | ||
may be using at the same time. | ||
|
||
Some clients may also choose to generate additional prefix | ||
where e.g. the language server runs in multiple instances | ||
and registering the same commands would lead to conflicts. | ||
|
||
This can be passed as part of `initializationOptions`, | ||
as documented in [Settings](./SETTINGS.md#commandprefix). | ||
|
||
## Arguments | ||
|
||
All commands accept arguments as string arrays with `=` used | ||
as a separator between key and value. i.e. | ||
|
||
```json | ||
{ | ||
"command": "command-name", | ||
"arguments": [ "key=value" ] | ||
} | ||
``` | ||
|
||
## Supported Commands | ||
|
||
### `terraform.init` | ||
|
||
Runs [`terraform init`](https://www.terraform.io/docs/cli/commands/init.html) using available `terraform` installation from `$PATH`. | ||
|
||
**Arguments:** | ||
|
||
- `uri` - URI of the directory in which to run `terraform init` | ||
|
||
**Outputs:** | ||
|
||
Error is returned e.g. when `terraform` is not installed, or when execution fails, | ||
but no output is returned if `init` successfully finishes. | ||
|
||
### `terraform.validate` | ||
|
||
Runs [`terraform validate`](https://www.terraform.io/docs/cli/commands/validate.html) using available `terraform` installation from `$PATH`. | ||
|
||
Any violations are published back the the client via [`textDocument/publishDiagnostics` notification](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_publishDiagnostics). | ||
|
||
Diagnostics are not persisted and any document change will cause them to be lost. | ||
|
||
**Arguments:** | ||
|
||
- `uri` - URI of the directory in which to run `terraform validate` | ||
|
||
**Outputs:** | ||
|
||
Error is returned e.g. when `terraform` is not installed, or when execution fails, | ||
but no output is returned if `validate` successfully finishes. | ||
|
||
### `module.callers` | ||
|
||
In Terraform module hierarchy "callers" are modules which _call_ another module | ||
via `module "..." {` blocks. | ||
|
||
Language server will attempt to discover any module hierarchy within the workspace | ||
and this command can be used to obtain the data about such hierarchy, which | ||
can be used to hint the user e.g. where to run `init` or `validate` from. | ||
|
||
**Arguments:** | ||
|
||
- `uri` - URI of the directory of the module in question, e.g. `file:///path/to/network` | ||
|
||
**Outputs:** | ||
|
||
- `v` - describes version of the format; Will be used in the future to communicate format changes. | ||
- `callers` - array of any modules found in the workspace which call the module in question | ||
- `uri` - URI of the directory (absolute path) | ||
- `rel_path` - path relative to the module in question, suitable to display in any UI elements | ||
|
||
```json | ||
{ | ||
"v": 0, | ||
"callers": [ | ||
{ | ||
"uri": "file:///path/to/dev", | ||
"rel_path": "../dev" | ||
}, | ||
{ | ||
"uri": "file:///path/to/prod", | ||
"rel_path": "../prod" | ||
} | ||
] | ||
} | ||
``` | ||
|
||
### `rootmodules` (DEPRECATED, use `module.callers` instead) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package command | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"path/filepath" | ||
"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"` | ||
RelativePath string `json:"rel_path"` | ||
} | ||
|
||
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 { | ||
relPath, err := filepath.Rel(modPath, caller.Path) | ||
if err != nil { | ||
return nil, err | ||
} | ||
callers = append(callers, moduleCaller{ | ||
URI: uri.FromPath(caller.Path), | ||
RelativePath: relPath, | ||
}) | ||
} | ||
sort.SliceStable(callers, func(i, j int) bool { | ||
return callers[i].URI < callers[j].URI | ||
}) | ||
return moduleCallersResponse{ | ||
FormatVersion: moduleCallersVersion, | ||
Callers: callers, | ||
}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
168 changes: 168 additions & 0 deletions
168
internal/langserver/handlers/execute_command_module_callers_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
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))}) | ||
|
||
devName := filepath.Join("..", "dev") | ||
prodName := filepath.Join("..", "prod") | ||
stagingName := filepath.Join("..", "staging") | ||
|
||
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", | ||
"rel_path": %q | ||
}, | ||
{ | ||
"uri": "%s/prod", | ||
"rel_path": %q | ||
}, | ||
{ | ||
"uri": "%s/staging", | ||
"rel_path": %q | ||
} | ||
] | ||
} | ||
}`, rootUri, devName, rootUri, prodName, rootUri, stagingName)) | ||
} | ||
|
||
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.