Skip to content

Commit c4e00c9

Browse files
heschimarwan-at-work
authored andcommitted
internal/lsp: support directory inclusion/exclusion filters
Users working in large repositories may want to include only selected directories in their workspace to avoid memory usage and performance slowdowns. Add support for inclusion/exclusion filters that control what directories are searched for workspace packages and modules. Packages that are excluded by the filter may still be loaded as non-workspace packages if other things depend on them. For a description of the option's syntax, see the documentation. Note that because we don't have any way to communicate the filters to packages.Load, we still run go list on the unfiltered workspace scope, then throw away the irrelevant packages. That may cost us, especially in workspaces with many files. Comments on the naming welcome. Also, if you know any places I may have missed applying the filter, please do tell. One thing I thought of is file watching, but that's covered because allKnownSubdirs works off of workspace files and those are already filtered. Possible enhancements: - Support glob patterns. - Apply filters during the goimports scan. - Figure out how to apply the filters to packages.Load. I don't know how to do it while still being build system neutral though. Closes golang/go#42473, assuming none of the enhancements are required. Change-Id: I9006a7a361dc3bb3c11f78b05ff84981813035a0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/275253 Trust: Heschi Kreinick <[email protected]> Run-TryBot: Heschi Kreinick <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent 48e2f65 commit c4e00c9

File tree

12 files changed

+279
-17
lines changed

12 files changed

+279
-17
lines changed

gopls/doc/settings.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,21 @@ just "Foo.Field".
175175

176176

177177
Default: `"Dynamic"`.
178+
### **directoryFilters** *[]string*
179+
directoryFilters can be used to exclude unwanted directories from the
180+
workspace. By default, all directories are included. Filters are an
181+
operator, `+` to include and `-` to exclude, followed by a path prefix
182+
relative to the workspace folder. They are evaluated in order, and
183+
the last filter that applies to a path controls whether it is included.
184+
The path prefix can be empty, so an initial `-` excludes everything.
185+
186+
Examples:
187+
Exclude node_modules: `-node_modules`
188+
Include only project_a: `-` (exclude everything), `+project_a`
189+
Include only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules`
190+
191+
192+
Default: `[]`.
178193
<!-- END User: DO NOT MANUALLY EDIT THIS SECTION -->
179194

180195
## Experimental

gopls/internal/regtest/workspace_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,3 +645,96 @@ func main() {
645645
}
646646
})
647647
}
648+
649+
func TestDirectoryFiltersLoads(t *testing.T) {
650+
// exclude, and its error, should be excluded from the workspace.
651+
const files = `
652+
-- go.mod --
653+
module example.com
654+
655+
go 1.12
656+
-- exclude/exclude.go --
657+
package exclude
658+
659+
const _ = Nonexistant
660+
`
661+
cfg := EditorConfig{
662+
DirectoryFilters: []string{"-exclude"},
663+
}
664+
withOptions(cfg).run(t, files, func(t *testing.T, env *Env) {
665+
env.Await(NoDiagnostics("exclude/x.go"))
666+
})
667+
}
668+
669+
func TestDirectoryFiltersTransitiveDep(t *testing.T) {
670+
// Even though exclude is excluded from the workspace, it should
671+
// still be importable as a non-workspace package.
672+
const files = `
673+
-- go.mod --
674+
module example.com
675+
676+
go 1.12
677+
-- include/include.go --
678+
package include
679+
import "example.com/exclude"
680+
681+
const _ = exclude.X
682+
-- exclude/exclude.go --
683+
package exclude
684+
685+
const _ = Nonexistant // should be ignored, since this is a non-workspace package
686+
const X = 1
687+
`
688+
689+
cfg := EditorConfig{
690+
DirectoryFilters: []string{"-exclude"},
691+
}
692+
withOptions(cfg).run(t, files, func(t *testing.T, env *Env) {
693+
env.Await(
694+
NoDiagnostics("exclude/exclude.go"), // filtered out
695+
NoDiagnostics("include/include.go"), // successfully builds
696+
)
697+
})
698+
}
699+
700+
func TestDirectoryFiltersWorkspaceModules(t *testing.T) {
701+
// Define a module include.com which should be in the workspace, plus a
702+
// module exclude.com which should be excluded and therefore come from
703+
// the proxy.
704+
const files = `
705+
-- include/go.mod --
706+
module include.com
707+
708+
go 1.12
709+
710+
require exclude.com v1.0.0
711+
-- include/include.go --
712+
package include
713+
714+
import "exclude.com"
715+
716+
var _ = exclude.X // satisfied only by the workspace version
717+
-- exclude/go.mod --
718+
module exclude.com
719+
720+
go 1.12
721+
-- exclude/exclude.go --
722+
package exclude
723+
724+
const X = 1
725+
`
726+
const proxy = `
727+
-- [email protected]/go.mod --
728+
module exclude.com
729+
730+
go 1.12
731+
-- [email protected]/exclude.go --
732+
package exclude
733+
`
734+
cfg := EditorConfig{
735+
DirectoryFilters: []string{"-exclude"},
736+
}
737+
withOptions(cfg, WithModes(Experimental), WithProxyFiles(proxy)).run(t, files, func(t *testing.T, env *Env) {
738+
env.Await(env.DiagnosticAtRegexp("include/include.go", `exclude.(X)`))
739+
})
740+
}

internal/lsp/cache/load.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf
159159
if isTestMain(pkg, s.view.gocache) {
160160
continue
161161
}
162+
// Skip filtered packages. They may be added anyway if they're
163+
// dependencies of non-filtered packages.
164+
if s.view.allFilesExcluded(pkg) {
165+
continue
166+
}
162167
// Set the metadata for this package.
163168
m, err := s.setMetadata(ctx, packagePath(pkg.PkgPath), pkg, cfg, map[packageID]struct{}{})
164169
if err != nil {

internal/lsp/cache/session.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,14 +175,14 @@ func (s *Session) createView(ctx context.Context, name string, folder, tempWorks
175175
}
176176
root := folder
177177
if options.ExpandWorkspaceToModule {
178-
root, err = findWorkspaceRoot(ctx, root, s, options.ExperimentalWorkspaceModule)
178+
root, err = findWorkspaceRoot(ctx, root, s, pathExcludedByFilterFunc(options), options.ExperimentalWorkspaceModule)
179179
if err != nil {
180180
return nil, nil, func() {}, err
181181
}
182182
}
183183

184184
// Build the gopls workspace, collecting active modules in the view.
185-
workspace, err := newWorkspace(ctx, root, s, ws.userGo111Module == off, options.ExperimentalWorkspaceModule)
185+
workspace, err := newWorkspace(ctx, root, s, pathExcludedByFilterFunc(options), ws.userGo111Module == off, options.ExperimentalWorkspaceModule)
186186
if err != nil {
187187
return nil, nil, func() {}, err
188188
}

internal/lsp/cache/snapshot.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,12 +1608,12 @@ func (s *snapshot) buildBuiltinPackage(ctx context.Context, goFiles []string) er
16081608

16091609
// BuildGoplsMod generates a go.mod file for all modules in the workspace. It
16101610
// bypasses any existing gopls.mod.
1611-
func BuildGoplsMod(ctx context.Context, root span.URI, fs source.FileSource) (*modfile.File, error) {
1612-
allModules, err := findModules(ctx, root, 0)
1611+
func BuildGoplsMod(ctx context.Context, root span.URI, s source.Snapshot) (*modfile.File, error) {
1612+
allModules, err := findModules(ctx, root, pathExcludedByFilterFunc(s.View().Options()), 0)
16131613
if err != nil {
16141614
return nil, err
16151615
}
1616-
return buildWorkspaceModFile(ctx, allModules, fs)
1616+
return buildWorkspaceModFile(ctx, allModules, s)
16171617
}
16181618

16191619
// TODO(rfindley): move this to workspacemodule.go

internal/lsp/cache/view.go

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
"golang.org/x/mod/modfile"
2525
"golang.org/x/mod/semver"
26+
"golang.org/x/tools/go/packages"
2627
"golang.org/x/tools/internal/event"
2728
"golang.org/x/tools/internal/gocommand"
2829
"golang.org/x/tools/internal/imports"
@@ -242,6 +243,9 @@ func minorOptionsChange(a, b *source.Options) bool {
242243
if !reflect.DeepEqual(a.Env, b.Env) {
243244
return false
244245
}
246+
if !reflect.DeepEqual(a.DirectoryFilters, b.DirectoryFilters) {
247+
return false
248+
}
245249
aBuildFlags := make([]string, len(a.BuildFlags))
246250
bBuildFlags := make([]string, len(b.BuildFlags))
247251
copy(aBuildFlags, a.BuildFlags)
@@ -323,7 +327,16 @@ func (s *snapshot) RunProcessEnvFunc(ctx context.Context, fn func(*imports.Optio
323327
}
324328

325329
func (v *View) contains(uri span.URI) bool {
326-
return source.InDir(v.rootURI.Filename(), uri.Filename()) || source.InDir(v.folder.Filename(), uri.Filename())
330+
inRoot := source.InDir(v.rootURI.Filename(), uri.Filename())
331+
inFolder := source.InDir(v.folder.Filename(), uri.Filename())
332+
if !inRoot && !inFolder {
333+
return false
334+
}
335+
// Filters are applied relative to the workspace folder.
336+
if inFolder {
337+
return !pathExcludedByFilter(strings.TrimPrefix(uri.Filename(), v.folder.Filename()), v.Options())
338+
}
339+
return true
327340
}
328341

329342
func (v *View) mapFile(uri span.URI, f *fileBase) {
@@ -699,7 +712,7 @@ func go111moduleForVersion(go111module string, goversion int) go111module {
699712
// Otherwise, it returns folder.
700713
// TODO (rFindley): move this to workspace.go
701714
// TODO (rFindley): simplify this once workspace modules are enabled by default.
702-
func findWorkspaceRoot(ctx context.Context, folder span.URI, fs source.FileSource, experimental bool) (span.URI, error) {
715+
func findWorkspaceRoot(ctx context.Context, folder span.URI, fs source.FileSource, excludePath func(string) bool, experimental bool) (span.URI, error) {
703716
patterns := []string{"go.mod"}
704717
if experimental {
705718
patterns = []string{"gopls.mod", "go.mod"}
@@ -720,7 +733,7 @@ func findWorkspaceRoot(ctx context.Context, folder span.URI, fs source.FileSourc
720733
}
721734

722735
// ...else we should check if there's exactly one nested module.
723-
all, err := findModules(ctx, folder, 2)
736+
all, err := findModules(ctx, folder, excludePath, 2)
724737
if err == errExhausted {
725738
// Fall-back behavior: if we don't find any modules after searching 10000
726739
// files, assume there are none.
@@ -915,3 +928,43 @@ func (s *snapshot) vendorEnabled(ctx context.Context, modURI span.URI, modConten
915928
vendorEnabled := modFile.Go != nil && modFile.Go.Version != "" && semver.Compare("v"+modFile.Go.Version, "v1.14") >= 0
916929
return vendorEnabled, nil
917930
}
931+
932+
func (v *View) allFilesExcluded(pkg *packages.Package) bool {
933+
opts := v.Options()
934+
folder := filepath.ToSlash(v.folder.Filename())
935+
for _, f := range pkg.GoFiles {
936+
f = filepath.ToSlash(f)
937+
if !strings.HasPrefix(f, folder) {
938+
return false
939+
}
940+
if !pathExcludedByFilter(strings.TrimPrefix(f, folder), opts) {
941+
return false
942+
}
943+
}
944+
return true
945+
}
946+
947+
func pathExcludedByFilterFunc(opts *source.Options) func(string) bool {
948+
return func(path string) bool {
949+
return pathExcludedByFilter(path, opts)
950+
}
951+
}
952+
953+
func pathExcludedByFilter(path string, opts *source.Options) bool {
954+
path = strings.TrimPrefix(filepath.ToSlash(path), "/")
955+
956+
excluded := false
957+
for _, filter := range opts.DirectoryFilters {
958+
op, prefix := filter[0], filter[1:]
959+
// Non-empty prefixes have to be precise directory matches.
960+
if prefix != "" {
961+
prefix = prefix + "/"
962+
path = path + "/"
963+
}
964+
if !strings.HasPrefix(path, prefix) {
965+
continue
966+
}
967+
excluded = op == '-'
968+
}
969+
return excluded
970+
}

internal/lsp/cache/view_test.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ module fg
9595
ctx := context.Background()
9696
rel := fake.RelativeTo(dir)
9797
folderURI := span.URIFromPath(rel.AbsPath(test.folder))
98-
got, err := findWorkspaceRoot(ctx, folderURI, osFileSource{}, test.experimental)
98+
excludeNothing := func(string) bool { return false }
99+
got, err := findWorkspaceRoot(ctx, folderURI, osFileSource{}, excludeNothing, test.experimental)
99100
if err != nil {
100101
t.Fatal(err)
101102
}
@@ -210,3 +211,49 @@ func TestInVendor(t *testing.T) {
210211
}
211212
}
212213
}
214+
215+
func TestFilters(t *testing.T) {
216+
tests := []struct {
217+
filters []string
218+
included []string
219+
excluded []string
220+
}{
221+
{
222+
included: []string{"x"},
223+
},
224+
{
225+
filters: []string{"-"},
226+
excluded: []string{"x", "x/a"},
227+
},
228+
{
229+
filters: []string{"-x", "+y"},
230+
included: []string{"y", "y/a", "z"},
231+
excluded: []string{"x", "x/a"},
232+
},
233+
{
234+
filters: []string{"-x", "+x/y", "-x/y/z"},
235+
included: []string{"x/y", "x/y/a", "a"},
236+
excluded: []string{"x", "x/a", "x/y/z/a"},
237+
},
238+
{
239+
filters: []string{"+foobar", "-foo"},
240+
included: []string{"foobar", "foobar/a"},
241+
excluded: []string{"foo", "foo/a"},
242+
},
243+
}
244+
245+
for _, tt := range tests {
246+
opts := &source.Options{}
247+
opts.DirectoryFilters = tt.filters
248+
for _, inc := range tt.included {
249+
if pathExcludedByFilter(inc, opts) {
250+
t.Errorf("filters %q excluded %v, wanted included", tt.filters, inc)
251+
}
252+
}
253+
for _, exc := range tt.excluded {
254+
if !pathExcludedByFilter(exc, opts) {
255+
t.Errorf("filters %q included %v, wanted excluded", tt.filters, exc)
256+
}
257+
}
258+
}
259+
}

0 commit comments

Comments
 (0)