Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ignorelist for module indexing #696

Merged
merged 10 commits into from
Nov 8, 2021
12 changes: 12 additions & 0 deletions docs/SETTINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,18 @@ Or if left empty
This setting should be deprecated once the language server supports multiple workspaces,
as this arises in VS code because a server instance is started per VS Code workspace.

## `ignoreDirectoryNames` (`[]string`)

This allows excluding directories from being checked by the walker by passing a static list of directory names.
dbanck marked this conversation as resolved.
Show resolved Hide resolved

The following list of directories will always be ignored:

- `.git`
- `.idea`
- `.vscode`
- `terraform.tfstate.d`
- `.terragrunt-cache`

## `experimentalFeatures` (object)

This object contains inner settings used to opt into experimental features not yet ready to be on by default.
Expand Down
3 changes: 3 additions & 0 deletions internal/langserver/handlers/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams)
"options.rootModulePaths": false,
"options.excludeModulePaths": false,
"options.commandPrefix": false,
"options.ignoreDirectoryNames": false,
"options.experimentalFeatures.validateOnSave": false,
"options.terraformExecPath": false,
"options.terraformExecTimeout": "",
Expand Down Expand Up @@ -123,6 +124,7 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams)
properties["options.rootModulePaths"] = len(out.Options.ModulePaths) > 0
properties["options.excludeModulePaths"] = len(out.Options.ExcludeModulePaths) > 0
properties["options.commandPrefix"] = len(out.Options.CommandPrefix) > 0
properties["options.ignoreDirectoryNames"] = len(out.Options.IgnoreDirectoryNames) > 0
properties["options.experimentalFeatures.prefillRequiredFields"] = out.Options.ExperimentalFeatures.PrefillRequiredFields
properties["options.experimentalFeatures.validateOnSave"] = out.Options.ExperimentalFeatures.ValidateOnSave
properties["options.terraformExecPath"] = len(out.Options.TerraformExecPath) > 0
Expand Down Expand Up @@ -192,6 +194,7 @@ func (svc *service) Initialize(ctx context.Context, params lsp.InitializeParams)
excludeModulePaths = append(excludeModulePaths, modPath)
}

svc.walker.SetIgnoreDirectoryNames(cfgOpts.IgnoreDirectoryNames)
svc.walker.SetExcludeModulePaths(excludeModulePaths)
svc.walker.EnqueuePath(fh.Dir())

Expand Down
40 changes: 40 additions & 0 deletions internal/langserver/handlers/initialize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handlers

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

"github.com/creachadair/jrpc2/code"
Expand Down Expand Up @@ -119,3 +120,42 @@ func TestInitialize_multipleFolders(t *testing.T) {
]
}`, rootDir.URI(), rootDir.URI())})
}

func TestInitialize_ignoreDirectoryNames(t *testing.T) {
tmpDir := TempDir(t, "plugin", "ignore")
pluginDir := filepath.Join(tmpDir.Dir(), "plugin")
emptyDir := filepath.Join(tmpDir.Dir(), "ignore")

InitPluginCache(t, pluginDir)
InitPluginCache(t, emptyDir)

ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{
TerraformCalls: &exec.TerraformMockCalls{
PerWorkDir: map[string][]*mock.Call{
pluginDir: validTfMockCalls(),
emptyDir: {
// TODO! improve mock and remove entry for `emptyDir` here afterwards
dbanck marked this conversation as resolved.
Show resolved Hide resolved
{
Method: "GetExecPath",
Repeatability: 1,
ReturnArguments: []interface{}{
"",
},
},
},
},
}}))
stop := ls.Start(t)
defer stop()

ls.Call(t, &langserver.CallRequest{
Method: "initialize",
ReqParams: fmt.Sprintf(`{
"capabilities": {},
"rootUri": %q,
"processId": 12345,
"initializationOptions": {
"ignoreDirectoryNames": [%q]
}
}`, tmpDir.URI(), "ignore")})
}
21 changes: 18 additions & 3 deletions internal/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/hashicorp/terraform-ls/internal/terraform/datadir"
"github.com/mitchellh/mapstructure"
)

Expand All @@ -15,9 +17,10 @@ type ExperimentalFeatures struct {

type Options struct {
// ModulePaths describes a list of absolute paths to modules to load
ModulePaths []string `mapstructure:"rootModulePaths"`
ExcludeModulePaths []string `mapstructure:"excludeModulePaths"`
CommandPrefix string `mapstructure:"commandPrefix"`
ModulePaths []string `mapstructure:"rootModulePaths"`
ExcludeModulePaths []string `mapstructure:"excludeModulePaths"`
CommandPrefix string `mapstructure:"commandPrefix"`
IgnoreDirectoryNames []string `mapstructure:"ignoreDirectoryNames"`

// ExperimentalFeatures encapsulates experimental features users can opt into.
ExperimentalFeatures ExperimentalFeatures `mapstructure:"experimentalFeatures"`
Expand Down Expand Up @@ -46,6 +49,18 @@ func (o *Options) Validate() error {
}
}

if len(o.IgnoreDirectoryNames) > 0 {
for _, directory := range o.IgnoreDirectoryNames {
if directory == datadir.DataDirName {
return fmt.Errorf("cannot ignore data directory %q", datadir.DataDirName)
dbanck marked this conversation as resolved.
Show resolved Hide resolved
}

if strings.Contains(directory, string(filepath.Separator)) {
return fmt.Errorf("expected directory name, got a path: %q", directory)
}
}
}

return nil
}

Expand Down
40 changes: 40 additions & 0 deletions internal/settings/settings_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package settings

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

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-ls/internal/terraform/datadir"
)

func TestDecodeOptions_nil(t *testing.T) {
Expand Down Expand Up @@ -40,3 +43,40 @@ func TestDecodeOptions_success(t *testing.T) {
t.Fatalf("options mismatch: %s", diff)
}
}

func TestValidate_IgnoreDirectoryNames_error(t *testing.T) {
tables := []struct {
input string
result string
}{
{datadir.DataDirName, `cannot ignore data directory ".terraform"`},
{filepath.Join("path", "path"), fmt.Sprintf(`expected directory name, got a path: %q`, filepath.Join("path", "path"))},
}

for _, table := range tables {
out, err := DecodeOptions(map[string]interface{}{
"IgnoreDirectoryNames": []string{table.input},
dbanck marked this conversation as resolved.
Show resolved Hide resolved
})
if err != nil {
t.Fatal(err)
}

result := out.Options.Validate()
if result.Error() != table.result {
t.Fatalf("expected error: %s, got: %s", table.result, result)
}
}
}
func TestValidate_IgnoreDirectoryNames_success(t *testing.T) {
out, err := DecodeOptions(map[string]interface{}{
"IgnoreDirectoryNames": []string{"directory"},
dbanck marked this conversation as resolved.
Show resolved Hide resolved
})
if err != nil {
t.Fatal(err)
}

result := out.Options.Validate()
if result != nil {
t.Fatalf("did not expect error: %s", result)
}
}
48 changes: 29 additions & 19 deletions internal/terraform/module/walker.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ var (

// skipDirNames represent directory names which would never contain
// plugin/module cache, so it's safe to skip them during the walk
//
// please keep the list in `SETTINGS.md` in sync
skipDirNames = map[string]bool{
".git": true,
".idea": true,
Expand Down Expand Up @@ -48,7 +50,8 @@ type Walker struct {
cancelFunc context.CancelFunc
doneCh <-chan struct{}

excludeModulePaths map[string]bool
excludeModulePaths map[string]bool
ignoreDirectoryNames map[string]bool
}

// queueCap represents channel buffer size
Expand All @@ -58,14 +61,15 @@ const queueCap = 50

func NewWalker(fs filesystem.Filesystem, modMgr ModuleManager) *Walker {
return &Walker{
fs: fs,
modMgr: modMgr,
logger: discardLogger,
walkingMu: &sync.RWMutex{},
queue: newWalkerQueue(fs),
queueMu: &sync.Mutex{},
pushChan: make(chan struct{}, queueCap),
doneCh: make(chan struct{}, 0),
fs: fs,
modMgr: modMgr,
logger: discardLogger,
walkingMu: &sync.RWMutex{},
queue: newWalkerQueue(fs),
queueMu: &sync.Mutex{},
pushChan: make(chan struct{}, queueCap),
doneCh: make(chan struct{}, 0),
ignoreDirectoryNames: skipDirNames,
}
}

Expand All @@ -84,6 +88,12 @@ func (w *Walker) SetExcludeModulePaths(excludeModulePaths []string) {
}
}

func (w *Walker) SetIgnoreDirectoryNames(ignoreDirectoryNames []string) {
for _, path := range ignoreDirectoryNames {
w.ignoreDirectoryNames[path] = true
}
}

func (w *Walker) Stop() {
if w.cancelFunc != nil {
w.cancelFunc()
Expand Down Expand Up @@ -199,6 +209,11 @@ func (w *Walker) IsWalking() bool {
return w.walking
}

func (w *Walker) isSkippableDir(dirName string) bool {
_, ok := w.ignoreDirectoryNames[dirName]
return ok
}

func (w *Walker) walk(ctx context.Context, rootPath string) error {
// We ignore the passed FS and instead read straight from OS FS
// because that would require reimplementing filepath.Walk and
Expand All @@ -221,6 +236,11 @@ func (w *Walker) walk(ctx context.Context, rootPath string) error {
return err
}

if w.isSkippableDir(info.Name()) {
w.logger.Printf("skipping %s", path)
return filepath.SkipDir
}

if _, ok := w.excludeModulePaths[dir]; ok {
return filepath.SkipDir
}
Expand Down Expand Up @@ -272,18 +292,8 @@ func (w *Walker) walk(ctx context.Context, rootPath string) error {
return nil
}

if isSkippableDir(info.Name()) {
w.logger.Printf("skipping %s", path)
return filepath.SkipDir
}

return nil
})
w.logger.Printf("walking of %s finished", rootPath)
return err
}

func isSkippableDir(dirName string) bool {
_, ok := skipDirNames[dirName]
return ok
}