1// Copyright 2022 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package pkgpattern
6
7import (
8	"regexp"
9	"strings"
10)
11
12// Note: most of this code was originally part of the cmd/go/internal/search
13// package; it was migrated here in order to support the use case of
14// commands other than cmd/go that need to accept package pattern args.
15
16// TreeCanMatchPattern(pattern)(name) reports whether
17// name or children of name can possibly match pattern.
18// Pattern is the same limited glob accepted by MatchPattern.
19func TreeCanMatchPattern(pattern string) func(name string) bool {
20	wildCard := false
21	if i := strings.Index(pattern, "..."); i >= 0 {
22		wildCard = true
23		pattern = pattern[:i]
24	}
25	return func(name string) bool {
26		return len(name) <= len(pattern) && hasPathPrefix(pattern, name) ||
27			wildCard && strings.HasPrefix(name, pattern)
28	}
29}
30
31// MatchPattern(pattern)(name) reports whether
32// name matches pattern. Pattern is a limited glob
33// pattern in which '...' means 'any string' and there
34// is no other special syntax.
35// Unfortunately, there are two special cases. Quoting "go help packages":
36//
37// First, /... at the end of the pattern can match an empty string,
38// so that net/... matches both net and packages in its subdirectories, like net/http.
39// Second, any slash-separated pattern element containing a wildcard never
40// participates in a match of the "vendor" element in the path of a vendored
41// package, so that ./... does not match packages in subdirectories of
42// ./vendor or ./mycode/vendor, but ./vendor/... and ./mycode/vendor/... do.
43// Note, however, that a directory named vendor that itself contains code
44// is not a vendored package: cmd/vendor would be a command named vendor,
45// and the pattern cmd/... matches it.
46func MatchPattern(pattern string) func(name string) bool {
47	return matchPatternInternal(pattern, true)
48}
49
50// MatchSimplePattern returns a function that can be used to check
51// whether a given name matches a pattern, where pattern is a limited
52// glob pattern in which '...' means 'any string', with no other
53// special syntax. There is one special case for MatchPatternSimple:
54// according to the rules in "go help packages": a /... at the end of
55// the pattern can match an empty string, so that net/... matches both
56// net and packages in its subdirectories, like net/http.
57func MatchSimplePattern(pattern string) func(name string) bool {
58	return matchPatternInternal(pattern, false)
59}
60
61func matchPatternInternal(pattern string, vendorExclude bool) func(name string) bool {
62	// Convert pattern to regular expression.
63	// The strategy for the trailing /... is to nest it in an explicit ? expression.
64	// The strategy for the vendor exclusion is to change the unmatchable
65	// vendor strings to a disallowed code point (vendorChar) and to use
66	// "(anything but that codepoint)*" as the implementation of the ... wildcard.
67	// This is a bit complicated but the obvious alternative,
68	// namely a hand-written search like in most shell glob matchers,
69	// is too easy to make accidentally exponential.
70	// Using package regexp guarantees linear-time matching.
71
72	const vendorChar = "\x00"
73
74	if vendorExclude && strings.Contains(pattern, vendorChar) {
75		return func(name string) bool { return false }
76	}
77
78	re := regexp.QuoteMeta(pattern)
79	wild := `.*`
80	if vendorExclude {
81		wild = `[^` + vendorChar + `]*`
82		re = replaceVendor(re, vendorChar)
83		switch {
84		case strings.HasSuffix(re, `/`+vendorChar+`/\.\.\.`):
85			re = strings.TrimSuffix(re, `/`+vendorChar+`/\.\.\.`) + `(/vendor|/` + vendorChar + `/\.\.\.)`
86		case re == vendorChar+`/\.\.\.`:
87			re = `(/vendor|/` + vendorChar + `/\.\.\.)`
88		}
89	}
90	if strings.HasSuffix(re, `/\.\.\.`) {
91		re = strings.TrimSuffix(re, `/\.\.\.`) + `(/\.\.\.)?`
92	}
93	re = strings.ReplaceAll(re, `\.\.\.`, wild)
94
95	reg := regexp.MustCompile(`^` + re + `$`)
96
97	return func(name string) bool {
98		if vendorExclude {
99			if strings.Contains(name, vendorChar) {
100				return false
101			}
102			name = replaceVendor(name, vendorChar)
103		}
104		return reg.MatchString(name)
105	}
106}
107
108// hasPathPrefix reports whether the path s begins with the
109// elements in prefix.
110func hasPathPrefix(s, prefix string) bool {
111	switch {
112	default:
113		return false
114	case len(s) == len(prefix):
115		return s == prefix
116	case len(s) > len(prefix):
117		if prefix != "" && prefix[len(prefix)-1] == '/' {
118			return strings.HasPrefix(s, prefix)
119		}
120		return s[len(prefix)] == '/' && s[:len(prefix)] == prefix
121	}
122}
123
124// replaceVendor returns the result of replacing
125// non-trailing vendor path elements in x with repl.
126func replaceVendor(x, repl string) string {
127	if !strings.Contains(x, "vendor") {
128		return x
129	}
130	elem := strings.Split(x, "/")
131	for i := 0; i < len(elem)-1; i++ {
132		if elem[i] == "vendor" {
133			elem[i] = repl
134		}
135	}
136	return strings.Join(elem, "/")
137}
138