Skip to content

Commit dc45e74

Browse files
author
Dylan Le
committed
internal/lsp: Update FilterDisallow to support matching directories at arbitrary depth.
In FilterDisallow, change filter to regex form to match with file paths. Add a unit regtest for FilterDisallow. For golang/go#46438 Change-Id: I7de1986c1cb1b65844828fa618b72b1e6b76b5b9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/414317 Run-TryBot: Dylan Le <dungtuanle@google.com> Reviewed-by: Robert Findley <rfindley@google.com> gopls-CI: kokoro <noreply+kokoro@google.com> TryBot-Result: Gopher Robot <gobot@golang.org>
1 parent ce6ce76 commit dc45e74

File tree

6 files changed

+200
-37
lines changed

6 files changed

+200
-37
lines changed

internal/lsp/cache/load.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf
156156
}
157157

158158
moduleErrs := make(map[string][]packages.Error) // module path -> errors
159+
filterer := buildFilterer(s.view.rootURI.Filename(), s.view.gomodcache, s.view.options)
159160
newMetadata := make(map[PackageID]*KnownMetadata)
160161
for _, pkg := range pkgs {
161162
// The Go command returns synthetic list results for module queries that
@@ -201,7 +202,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf
201202
//
202203
// TODO(rfindley): why exclude metadata arbitrarily here? It should be safe
203204
// to capture all metadata.
204-
if s.view.allFilesExcluded(pkg) {
205+
if s.view.allFilesExcluded(pkg, filterer) {
205206
continue
206207
}
207208
if err := buildMetadata(ctx, PackagePath(pkg.PkgPath), pkg, cfg, query, newMetadata, nil); err != nil {
@@ -581,10 +582,11 @@ func containsPackageLocked(s *snapshot, m *Metadata) bool {
581582
uris[uri] = struct{}{}
582583
}
583584

585+
filterFunc := s.view.filterFunc()
584586
for uri := range uris {
585587
// Don't use view.contains here. go.work files may include modules
586588
// outside of the workspace folder.
587-
if !strings.Contains(string(uri), "/vendor/") && !s.view.filters(uri) {
589+
if !strings.Contains(string(uri), "/vendor/") && !filterFunc(uri) {
588590
return true
589591
}
590592
}

internal/lsp/cache/view.go

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -385,13 +385,14 @@ func (s *snapshot) locateTemplateFiles(ctx context.Context) {
385385
relativeTo := s.view.folder.Filename()
386386

387387
searched := 0
388+
filterer := buildFilterer(dir, s.view.gomodcache, s.view.options)
388389
// Change to WalkDir when we move up to 1.16
389390
err := filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
390391
if err != nil {
391392
return err
392393
}
393394
relpath := strings.TrimPrefix(path, relativeTo)
394-
excluded := pathExcludedByFilter(relpath, dir, s.view.gomodcache, s.view.options)
395+
excluded := pathExcludedByFilter(relpath, filterer)
395396
if fileHasExtension(path, suffixes) && !excluded && !fi.IsDir() {
396397
k := span.URIFromPath(path)
397398
_, err := s.GetVersionedFile(ctx, k)
@@ -421,17 +422,20 @@ func (v *View) contains(uri span.URI) bool {
421422
return false
422423
}
423424

424-
return !v.filters(uri)
425+
return !v.filterFunc()(uri)
425426
}
426427

427-
// filters reports whether uri is filtered by the currently configured
428+
// filterFunc returns a func that reports whether uri is filtered by the currently configured
428429
// directoryFilters.
429-
func (v *View) filters(uri span.URI) bool {
430-
// Only filter relative to the configured root directory.
431-
if source.InDirLex(v.folder.Filename(), uri.Filename()) {
432-
return pathExcludedByFilter(strings.TrimPrefix(uri.Filename(), v.folder.Filename()), v.rootURI.Filename(), v.gomodcache, v.Options())
430+
func (v *View) filterFunc() func(span.URI) bool {
431+
filterer := buildFilterer(v.rootURI.Filename(), v.gomodcache, v.Options())
432+
return func(uri span.URI) bool {
433+
// Only filter relative to the configured root directory.
434+
if source.InDirLex(v.folder.Filename(), uri.Filename()) {
435+
return pathExcludedByFilter(strings.TrimPrefix(uri.Filename(), v.folder.Filename()), filterer)
436+
}
437+
return false
433438
}
434-
return false
435439
}
436440

437441
func (v *View) mapFile(uri span.URI, f *fileBase) {
@@ -1070,24 +1074,24 @@ func (s *snapshot) vendorEnabled(ctx context.Context, modURI span.URI, modConten
10701074
return vendorEnabled, nil
10711075
}
10721076

1073-
func (v *View) allFilesExcluded(pkg *packages.Package) bool {
1074-
opts := v.Options()
1077+
func (v *View) allFilesExcluded(pkg *packages.Package, filterer *source.Filterer) bool {
10751078
folder := filepath.ToSlash(v.folder.Filename())
10761079
for _, f := range pkg.GoFiles {
10771080
f = filepath.ToSlash(f)
10781081
if !strings.HasPrefix(f, folder) {
10791082
return false
10801083
}
1081-
if !pathExcludedByFilter(strings.TrimPrefix(f, folder), v.rootURI.Filename(), v.gomodcache, opts) {
1084+
if !pathExcludedByFilter(strings.TrimPrefix(f, folder), filterer) {
10821085
return false
10831086
}
10841087
}
10851088
return true
10861089
}
10871090

10881091
func pathExcludedByFilterFunc(root, gomodcache string, opts *source.Options) func(string) bool {
1092+
filterer := buildFilterer(root, gomodcache, opts)
10891093
return func(path string) bool {
1090-
return pathExcludedByFilter(path, root, gomodcache, opts)
1094+
return pathExcludedByFilter(path, filterer)
10911095
}
10921096
}
10931097

@@ -1097,12 +1101,16 @@ func pathExcludedByFilterFunc(root, gomodcache string, opts *source.Options) fun
10971101
// TODO(rfindley): passing root and gomodcache here makes it confusing whether
10981102
// path should be absolute or relative, and has already caused at least one
10991103
// bug.
1100-
func pathExcludedByFilter(path, root, gomodcache string, opts *source.Options) bool {
1104+
func pathExcludedByFilter(path string, filterer *source.Filterer) bool {
11011105
path = strings.TrimPrefix(filepath.ToSlash(path), "/")
1106+
return filterer.Disallow(path)
1107+
}
1108+
1109+
func buildFilterer(root, gomodcache string, opts *source.Options) *source.Filterer {
11021110
gomodcache = strings.TrimPrefix(filepath.ToSlash(strings.TrimPrefix(gomodcache, root)), "/")
11031111
filters := opts.DirectoryFilters
11041112
if gomodcache != "" {
11051113
filters = append(filters, "-"+gomodcache)
11061114
}
1107-
return source.FiltersDisallow(path, filters)
1115+
return source.NewFilterer(filters)
11081116
}

internal/lsp/cache/view_test.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,15 +161,14 @@ func TestFilters(t *testing.T) {
161161
}
162162

163163
for _, tt := range tests {
164-
opts := &source.Options{}
165-
opts.DirectoryFilters = tt.filters
164+
filterer := source.NewFilterer(tt.filters)
166165
for _, inc := range tt.included {
167-
if pathExcludedByFilter(inc, "root", "root/gopath/pkg/mod", opts) {
166+
if pathExcludedByFilter(inc, filterer) {
168167
t.Errorf("filters %q excluded %v, wanted included", tt.filters, inc)
169168
}
170169
}
171170
for _, exc := range tt.excluded {
172-
if !pathExcludedByFilter(exc, "root", "root/gopath/pkg/mod", opts) {
171+
if !pathExcludedByFilter(exc, filterer) {
173172
t.Errorf("filters %q included %v, wanted excluded", tt.filters, exc)
174173
}
175174
}

internal/lsp/source/options.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,30 @@ func (o *Options) enableAllExperimentMaps() {
806806
}
807807
}
808808

809+
// validateDirectoryFilter validates if the filter string
810+
// - is not empty
811+
// - start with either + or -
812+
// - doesn't contain currently unsupported glob operators: *, ?
813+
func validateDirectoryFilter(ifilter string) (string, error) {
814+
filter := fmt.Sprint(ifilter)
815+
if filter == "" || (filter[0] != '+' && filter[0] != '-') {
816+
return "", fmt.Errorf("invalid filter %v, must start with + or -", filter)
817+
}
818+
segs := strings.Split(filter, "/")
819+
unsupportedOps := [...]string{"?", "*"}
820+
for _, seg := range segs {
821+
if seg != "**" {
822+
for _, op := range unsupportedOps {
823+
if strings.Contains(seg, op) {
824+
return "", fmt.Errorf("invalid filter %v, operator %v not supported. If you want to have this operator supported, consider filing an issue.", filter, op)
825+
}
826+
}
827+
}
828+
}
829+
830+
return strings.TrimRight(filepath.FromSlash(filter), "/"), nil
831+
}
832+
809833
func (o *Options) set(name string, value interface{}, seen map[string]struct{}) OptionResult {
810834
// Flatten the name in case we get options with a hierarchy.
811835
split := strings.Split(name, ".")
@@ -850,9 +874,9 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{})
850874
}
851875
var filters []string
852876
for _, ifilter := range ifilters {
853-
filter := fmt.Sprint(ifilter)
854-
if filter == "" || (filter[0] != '+' && filter[0] != '-') {
855-
result.errorf("invalid filter %q, must start with + or -", filter)
877+
filter, err := validateDirectoryFilter(fmt.Sprintf("%v", ifilter))
878+
if err != nil {
879+
result.errorf(err.Error())
856880
return result
857881
}
858882
filters = append(filters, strings.TrimRight(filepath.FromSlash(filter), "/"))

internal/lsp/source/workspace_symbol.go

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88
"context"
99
"fmt"
1010
"go/types"
11+
"path"
1112
"path/filepath"
13+
"regexp"
1214
"runtime"
1315
"sort"
1416
"strings"
@@ -305,11 +307,12 @@ func collectSymbols(ctx context.Context, views []View, matcherType SymbolMatcher
305307
roots = append(roots, strings.TrimRight(string(v.Folder()), "/"))
306308

307309
filters := v.Options().DirectoryFilters
310+
filterer := NewFilterer(filters)
308311
folder := filepath.ToSlash(v.Folder().Filename())
309312
for uri, syms := range snapshot.Symbols(ctx) {
310313
norm := filepath.ToSlash(uri.Filename())
311314
nm := strings.TrimPrefix(norm, folder)
312-
if FiltersDisallow(nm, filters) {
315+
if filterer.Disallow(nm) {
313316
continue
314317
}
315318
// Only scan each file once.
@@ -358,28 +361,70 @@ func collectSymbols(ctx context.Context, views []View, matcherType SymbolMatcher
358361
return unified.results(), nil
359362
}
360363

361-
// FilterDisallow is code from the body of cache.pathExcludedByFilter in cache/view.go
362-
// Exporting and using that function would cause an import cycle.
363-
// Moving it here and exporting it would leave behind view_test.go.
364-
// (This code is exported and used in the body of cache.pathExcludedByFilter)
365-
func FiltersDisallow(path string, filters []string) bool {
364+
type Filterer struct {
365+
// Whether a filter is excluded depends on the operator (first char of the raw filter).
366+
// Slices filters and excluded then should have the same length.
367+
filters []*regexp.Regexp
368+
excluded []bool
369+
}
370+
371+
// NewFilterer computes regular expression form of all raw filters
372+
func NewFilterer(rawFilters []string) *Filterer {
373+
var f Filterer
374+
for _, filter := range rawFilters {
375+
filter = path.Clean(filepath.ToSlash(filter))
376+
op, prefix := filter[0], filter[1:]
377+
// convertFilterToRegexp adds "/" at the end of prefix to handle cases where a filter is a prefix of another filter.
378+
// For example, it prevents [+foobar, -foo] from excluding "foobar".
379+
f.filters = append(f.filters, convertFilterToRegexp(filepath.ToSlash(prefix)))
380+
f.excluded = append(f.excluded, op == '-')
381+
}
382+
383+
return &f
384+
}
385+
386+
// Disallow return true if the path is excluded from the filterer's filters.
387+
func (f *Filterer) Disallow(path string) bool {
366388
path = strings.TrimPrefix(path, "/")
367389
var excluded bool
368-
for _, filter := range filters {
369-
op, prefix := filter[0], filter[1:]
370-
// Non-empty prefixes have to be precise directory matches.
371-
if prefix != "" {
372-
prefix = prefix + "/"
373-
path = path + "/"
390+
391+
for i, filter := range f.filters {
392+
path := path
393+
if !strings.HasSuffix(path, "/") {
394+
path += "/"
374395
}
375-
if !strings.HasPrefix(path, prefix) {
396+
if !filter.MatchString(path) {
376397
continue
377398
}
378-
excluded = op == '-'
399+
excluded = f.excluded[i]
379400
}
401+
380402
return excluded
381403
}
382404

405+
// convertFilterToRegexp replaces glob-like operator substrings in a string file path to their equivalent regex forms.
406+
// Supporting glob-like operators:
407+
// - **: match zero or more complete path segments
408+
func convertFilterToRegexp(filter string) *regexp.Regexp {
409+
var ret strings.Builder
410+
segs := strings.Split(filter, "/")
411+
for i, seg := range segs {
412+
if seg == "**" {
413+
switch i {
414+
case 0:
415+
ret.WriteString("^.*")
416+
default:
417+
ret.WriteString(".*")
418+
}
419+
} else {
420+
ret.WriteString(regexp.QuoteMeta(seg))
421+
}
422+
ret.WriteString("/")
423+
}
424+
425+
return regexp.MustCompile(ret.String())
426+
}
427+
383428
// symbolFile holds symbol information for a single file.
384429
type symbolFile struct {
385430
uri span.URI

internal/lsp/source/workspace_symbol_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,88 @@ func TestParseQuery(t *testing.T) {
4444
}
4545
}
4646
}
47+
48+
func TestFiltererDisallow(t *testing.T) {
49+
tests := []struct {
50+
filters []string
51+
included []string
52+
excluded []string
53+
}{
54+
{
55+
[]string{"+**/c.go"},
56+
[]string{"a/c.go", "a/b/c.go"},
57+
[]string{},
58+
},
59+
{
60+
[]string{"+a/**/c.go"},
61+
[]string{"a/b/c.go", "a/b/d/c.go", "a/c.go"},
62+
[]string{},
63+
},
64+
{
65+
[]string{"-a/c.go", "+a/**"},
66+
[]string{"a/c.go"},
67+
[]string{},
68+
},
69+
{
70+
[]string{"+a/**/c.go", "-**/c.go"},
71+
[]string{},
72+
[]string{"a/b/c.go"},
73+
},
74+
{
75+
[]string{"+a/**/c.go", "-a/**"},
76+
[]string{},
77+
[]string{"a/b/c.go"},
78+
},
79+
{
80+
[]string{"+**/c.go", "-a/**/c.go"},
81+
[]string{},
82+
[]string{"a/b/c.go"},
83+
},
84+
{
85+
[]string{"+foobar", "-foo"},
86+
[]string{"foobar", "foobar/a"},
87+
[]string{"foo", "foo/a"},
88+
},
89+
{
90+
[]string{"+", "-"},
91+
[]string{},
92+
[]string{"foobar", "foobar/a", "foo", "foo/a"},
93+
},
94+
{
95+
[]string{"-", "+"},
96+
[]string{"foobar", "foobar/a", "foo", "foo/a"},
97+
[]string{},
98+
},
99+
{
100+
[]string{"-a/**/b/**/c.go"},
101+
[]string{},
102+
[]string{"a/x/y/z/b/f/g/h/c.go"},
103+
},
104+
// tests for unsupported glob operators
105+
{
106+
[]string{"+**/c.go", "-a/*/c.go"},
107+
[]string{"a/b/c.go"},
108+
[]string{},
109+
},
110+
{
111+
[]string{"+**/c.go", "-a/?/c.go"},
112+
[]string{"a/b/c.go"},
113+
[]string{},
114+
},
115+
}
116+
117+
for _, test := range tests {
118+
filterer := NewFilterer(test.filters)
119+
for _, inc := range test.included {
120+
if filterer.Disallow(inc) {
121+
t.Errorf("Filters %v excluded %v, wanted included", test.filters, inc)
122+
}
123+
}
124+
125+
for _, exc := range test.excluded {
126+
if !filterer.Disallow(exc) {
127+
t.Errorf("Filters %v included %v, wanted excluded", test.filters, exc)
128+
}
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)