diff options
Diffstat (limited to 'src/cmd/internal/pkgpattern')
-rw-r--r-- | src/cmd/internal/pkgpattern/pat_test.go | 199 | ||||
-rw-r--r-- | src/cmd/internal/pkgpattern/pkgpattern.go | 137 |
2 files changed, 336 insertions, 0 deletions
diff --git a/src/cmd/internal/pkgpattern/pat_test.go b/src/cmd/internal/pkgpattern/pat_test.go new file mode 100644 index 0000000..0a11570 --- /dev/null +++ b/src/cmd/internal/pkgpattern/pat_test.go @@ -0,0 +1,199 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pkgpattern + +import ( + "strings" + "testing" +) + +var matchPatternTests = ` + pattern ... + match foo + + pattern net + match net + not net/http + + pattern net/http + match net/http + not net + + pattern net... + match net net/http netchan + not not/http not/net/http + + # Special cases. Quoting docs: + + # First, /... at the end of the pattern can match an empty string, + # so that net/... matches both net and packages in its subdirectories, like net/http. + pattern net/... + match net net/http + not not/http not/net/http netchan + + # Second, any slash-separated pattern element containing a wildcard never + # participates in a match of the "vendor" element in the path of a vendored + # package, so that ./... does not match packages in subdirectories of + # ./vendor or ./mycode/vendor, but ./vendor/... and ./mycode/vendor/... do. + # Note, however, that a directory named vendor that itself contains code + # is not a vendored package: cmd/vendor would be a command named vendor, + # and the pattern cmd/... matches it. + pattern ./... + match ./vendor ./mycode/vendor + not ./vendor/foo ./mycode/vendor/foo + + pattern ./vendor/... + match ./vendor/foo ./vendor/foo/vendor + not ./vendor/foo/vendor/bar + + pattern mycode/vendor/... + match mycode/vendor mycode/vendor/foo mycode/vendor/foo/vendor + not mycode/vendor/foo/vendor/bar + + pattern x/vendor/y + match x/vendor/y + not x/vendor + + pattern x/vendor/y/... + match x/vendor/y x/vendor/y/z x/vendor/y/vendor x/vendor/y/z/vendor + not x/vendor/y/vendor/z + + pattern .../vendor/... + match x/vendor/y x/vendor/y/z x/vendor/y/vendor x/vendor/y/z/vendor +` + +func TestMatchPattern(t *testing.T) { + testPatterns(t, "MatchPattern", matchPatternTests, func(pattern, name string) bool { + return MatchPattern(pattern)(name) + }) +} + +var matchSimplePatternTests = ` + pattern ... + match foo + + pattern .../bar/.../baz + match foo/bar/abc/baz + + pattern net + match net + not net/http + + pattern net/http + match net/http + not net + + pattern net... + match net net/http netchan + not not/http not/net/http + + # Special cases. Quoting docs: + + # First, /... at the end of the pattern can match an empty string, + # so that net/... matches both net and packages in its subdirectories, like net/http. + pattern net/... + match net net/http + not not/http not/net/http netchan +` + +func TestSimpleMatchPattern(t *testing.T) { + testPatterns(t, "MatchSimplePattern", matchSimplePatternTests, func(pattern, name string) bool { + return MatchSimplePattern(pattern)(name) + }) +} + +var treeCanMatchPatternTests = ` + pattern ... + match foo + + pattern net + match net + not net/http + + pattern net/http + match net net/http + + pattern net... + match net netchan net/http + not not/http not/net/http + + pattern net/... + match net net/http + not not/http netchan + + pattern abc.../def + match abcxyz + not xyzabc + + pattern x/y/z/... + match x x/y x/y/z x/y/z/w + + pattern x/y/z + match x x/y x/y/z + not x/y/z/w + + pattern x/.../y/z + match x/a/b/c + not y/x/a/b/c +` + +func TestTreeCanMatchPattern(t *testing.T) { + testPatterns(t, "TreeCanMatchPattern", treeCanMatchPatternTests, func(pattern, name string) bool { + return TreeCanMatchPattern(pattern)(name) + }) +} + +var hasPathPrefixTests = []stringPairTest{ + {"abc", "a", false}, + {"a/bc", "a", true}, + {"a", "a", true}, + {"a/bc", "a/", true}, +} + +func TestHasPathPrefix(t *testing.T) { + testStringPairs(t, "hasPathPrefix", hasPathPrefixTests, hasPathPrefix) +} + +type stringPairTest struct { + in1 string + in2 string + out bool +} + +func testStringPairs(t *testing.T, name string, tests []stringPairTest, f func(string, string) bool) { + for _, tt := range tests { + if out := f(tt.in1, tt.in2); out != tt.out { + t.Errorf("%s(%q, %q) = %v, want %v", name, tt.in1, tt.in2, out, tt.out) + } + } +} + +func testPatterns(t *testing.T, name, tests string, fn func(string, string) bool) { + var patterns []string + for _, line := range strings.Split(tests, "\n") { + if i := strings.Index(line, "#"); i >= 0 { + line = line[:i] + } + f := strings.Fields(line) + if len(f) == 0 { + continue + } + switch f[0] { + default: + t.Fatalf("unknown directive %q", f[0]) + case "pattern": + patterns = f[1:] + case "match", "not": + want := f[0] == "match" + for _, pattern := range patterns { + for _, in := range f[1:] { + if fn(pattern, in) != want { + t.Errorf("%s(%q, %q) = %v, want %v", name, pattern, in, !want, want) + } + } + } + } + } +} diff --git a/src/cmd/internal/pkgpattern/pkgpattern.go b/src/cmd/internal/pkgpattern/pkgpattern.go new file mode 100644 index 0000000..1496eeb --- /dev/null +++ b/src/cmd/internal/pkgpattern/pkgpattern.go @@ -0,0 +1,137 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pkgpattern + +import ( + "regexp" + "strings" +) + +// Note: most of this code was originally part of the cmd/go/internal/search +// package; it was migrated here in order to support the use case of +// commands other than cmd/go that need to accept package pattern args. + +// TreeCanMatchPattern(pattern)(name) reports whether +// name or children of name can possibly match pattern. +// Pattern is the same limited glob accepted by MatchPattern. +func TreeCanMatchPattern(pattern string) func(name string) bool { + wildCard := false + if i := strings.Index(pattern, "..."); i >= 0 { + wildCard = true + pattern = pattern[:i] + } + return func(name string) bool { + return len(name) <= len(pattern) && hasPathPrefix(pattern, name) || + wildCard && strings.HasPrefix(name, pattern) + } +} + +// MatchPattern(pattern)(name) reports whether +// name matches pattern. Pattern is a limited glob +// pattern in which '...' means 'any string' and there +// is no other special syntax. +// Unfortunately, there are two special cases. Quoting "go help packages": +// +// First, /... at the end of the pattern can match an empty string, +// so that net/... matches both net and packages in its subdirectories, like net/http. +// Second, any slash-separated pattern element containing a wildcard never +// participates in a match of the "vendor" element in the path of a vendored +// package, so that ./... does not match packages in subdirectories of +// ./vendor or ./mycode/vendor, but ./vendor/... and ./mycode/vendor/... do. +// Note, however, that a directory named vendor that itself contains code +// is not a vendored package: cmd/vendor would be a command named vendor, +// and the pattern cmd/... matches it. +func MatchPattern(pattern string) func(name string) bool { + return matchPatternInternal(pattern, true) +} + +// MatchSimplePattern returns a function that can be used to check +// whether a given name matches a pattern, where pattern is a limited +// glob pattern in which '...' means 'any string', with no other +// special syntax. There is one special case for MatchPatternSimple: +// according to the rules in "go help packages": a /... at the end of +// the pattern can match an empty string, so that net/... matches both +// net and packages in its subdirectories, like net/http. +func MatchSimplePattern(pattern string) func(name string) bool { + return matchPatternInternal(pattern, false) +} + +func matchPatternInternal(pattern string, vendorExclude bool) func(name string) bool { + // Convert pattern to regular expression. + // The strategy for the trailing /... is to nest it in an explicit ? expression. + // The strategy for the vendor exclusion is to change the unmatchable + // vendor strings to a disallowed code point (vendorChar) and to use + // "(anything but that codepoint)*" as the implementation of the ... wildcard. + // This is a bit complicated but the obvious alternative, + // namely a hand-written search like in most shell glob matchers, + // is too easy to make accidentally exponential. + // Using package regexp guarantees linear-time matching. + + const vendorChar = "\x00" + + if vendorExclude && strings.Contains(pattern, vendorChar) { + return func(name string) bool { return false } + } + + re := regexp.QuoteMeta(pattern) + wild := `.*` + if vendorExclude { + wild = `[^` + vendorChar + `]*` + re = replaceVendor(re, vendorChar) + switch { + case strings.HasSuffix(re, `/`+vendorChar+`/\.\.\.`): + re = strings.TrimSuffix(re, `/`+vendorChar+`/\.\.\.`) + `(/vendor|/` + vendorChar + `/\.\.\.)` + case re == vendorChar+`/\.\.\.`: + re = `(/vendor|/` + vendorChar + `/\.\.\.)` + } + } + if strings.HasSuffix(re, `/\.\.\.`) { + re = strings.TrimSuffix(re, `/\.\.\.`) + `(/\.\.\.)?` + } + re = strings.ReplaceAll(re, `\.\.\.`, wild) + + reg := regexp.MustCompile(`^` + re + `$`) + + return func(name string) bool { + if vendorExclude { + if strings.Contains(name, vendorChar) { + return false + } + name = replaceVendor(name, vendorChar) + } + return reg.MatchString(name) + } +} + +// hasPathPrefix reports whether the path s begins with the +// elements in prefix. +func hasPathPrefix(s, prefix string) bool { + switch { + default: + return false + case len(s) == len(prefix): + return s == prefix + case len(s) > len(prefix): + if prefix != "" && prefix[len(prefix)-1] == '/' { + return strings.HasPrefix(s, prefix) + } + return s[len(prefix)] == '/' && s[:len(prefix)] == prefix + } +} + +// replaceVendor returns the result of replacing +// non-trailing vendor path elements in x with repl. +func replaceVendor(x, repl string) string { + if !strings.Contains(x, "vendor") { + return x + } + elem := strings.Split(x, "/") + for i := 0; i < len(elem)-1; i++ { + if elem[i] == "vendor" { + elem[i] = repl + } + } + return strings.Join(elem, "/") +} |