blob: 1496eebb3e4d9d13a3dee4db1944b66f42e4043d [file] [log] [blame]
// 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, "/")
}