1// Copyright 2017 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 search
6
7import (
8	"cmd/go/internal/base"
9	"cmd/go/internal/cfg"
10	"cmd/go/internal/fsys"
11	"cmd/go/internal/str"
12	"cmd/internal/pkgpattern"
13	"fmt"
14	"go/build"
15	"io/fs"
16	"os"
17	"path"
18	"path/filepath"
19	"strings"
20)
21
22// A Match represents the result of matching a single package pattern.
23type Match struct {
24	pattern string   // the pattern itself
25	Dirs    []string // if the pattern is local, directories that potentially contain matching packages
26	Pkgs    []string // matching packages (import paths)
27	Errs    []error  // errors matching the patterns to packages, NOT errors loading those packages
28
29	// Errs may be non-empty even if len(Pkgs) > 0, indicating that some matching
30	// packages could be located but results may be incomplete.
31	// If len(Pkgs) == 0 && len(Errs) == 0, the pattern is well-formed but did not
32	// match any packages.
33}
34
35// NewMatch returns a Match describing the given pattern,
36// without resolving its packages or errors.
37func NewMatch(pattern string) *Match {
38	return &Match{pattern: pattern}
39}
40
41// Pattern returns the pattern to be matched.
42func (m *Match) Pattern() string { return m.pattern }
43
44// AddError appends a MatchError wrapping err to m.Errs.
45func (m *Match) AddError(err error) {
46	m.Errs = append(m.Errs, &MatchError{Match: m, Err: err})
47}
48
49// IsLiteral reports whether the pattern is free of wildcards and meta-patterns.
50//
51// A literal pattern must match at most one package.
52func (m *Match) IsLiteral() bool {
53	return !strings.Contains(m.pattern, "...") && !m.IsMeta()
54}
55
56// IsLocal reports whether the pattern must be resolved from a specific root or
57// directory, such as a filesystem path or a single module.
58func (m *Match) IsLocal() bool {
59	return build.IsLocalImport(m.pattern) || filepath.IsAbs(m.pattern)
60}
61
62// IsMeta reports whether the pattern is a “meta-package” keyword that represents
63// multiple packages, such as "std", "cmd", or "all".
64func (m *Match) IsMeta() bool {
65	return IsMetaPackage(m.pattern)
66}
67
68// IsMetaPackage checks if name is a reserved package name that expands to multiple packages.
69func IsMetaPackage(name string) bool {
70	return name == "std" || name == "cmd" || name == "all"
71}
72
73// A MatchError indicates an error that occurred while attempting to match a
74// pattern.
75type MatchError struct {
76	Match *Match
77	Err   error
78}
79
80func (e *MatchError) Error() string {
81	if e.Match.IsLiteral() {
82		return fmt.Sprintf("%s: %v", e.Match.Pattern(), e.Err)
83	}
84	return fmt.Sprintf("pattern %s: %v", e.Match.Pattern(), e.Err)
85}
86
87func (e *MatchError) Unwrap() error {
88	return e.Err
89}
90
91// MatchPackages sets m.Pkgs to a non-nil slice containing all the packages that
92// can be found under the $GOPATH directories and $GOROOT that match the
93// pattern. The pattern must be either "all" (all packages), "std" (standard
94// packages), "cmd" (standard commands), or a path including "...".
95//
96// If any errors may have caused the set of packages to be incomplete,
97// MatchPackages appends those errors to m.Errs.
98func (m *Match) MatchPackages() {
99	m.Pkgs = []string{}
100	if m.IsLocal() {
101		m.AddError(fmt.Errorf("internal error: MatchPackages: %s is not a valid package pattern", m.pattern))
102		return
103	}
104
105	if m.IsLiteral() {
106		m.Pkgs = []string{m.pattern}
107		return
108	}
109
110	match := func(string) bool { return true }
111	treeCanMatch := func(string) bool { return true }
112	if !m.IsMeta() {
113		match = pkgpattern.MatchPattern(m.pattern)
114		treeCanMatch = pkgpattern.TreeCanMatchPattern(m.pattern)
115	}
116
117	have := map[string]bool{
118		"builtin": true, // ignore pseudo-package that exists only for documentation
119	}
120	if !cfg.BuildContext.CgoEnabled {
121		have["runtime/cgo"] = true // ignore during walk
122	}
123
124	for _, src := range cfg.BuildContext.SrcDirs() {
125		if (m.pattern == "std" || m.pattern == "cmd") && src != cfg.GOROOTsrc {
126			continue
127		}
128
129		// If the root itself is a symlink to a directory,
130		// we want to follow it (see https://go.dev/issue/50807).
131		// Add a trailing separator to force that to happen.
132		src = str.WithFilePathSeparator(filepath.Clean(src))
133		root := src
134		if m.pattern == "cmd" {
135			root += "cmd" + string(filepath.Separator)
136		}
137
138		err := fsys.Walk(root, func(path string, fi fs.FileInfo, err error) error {
139			if err != nil {
140				return err // Likely a permission error, which could interfere with matching.
141			}
142			if path == src {
143				return nil // GOROOT/src and GOPATH/src cannot contain packages.
144			}
145
146			want := true
147			// Avoid .foo, _foo, and testdata directory trees.
148			_, elem := filepath.Split(path)
149			if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" {
150				want = false
151			}
152
153			name := filepath.ToSlash(path[len(src):])
154			if m.pattern == "std" && (!IsStandardImportPath(name) || name == "cmd") {
155				// The name "std" is only the standard library.
156				// If the name is cmd, it's the root of the command tree.
157				want = false
158			}
159			if !treeCanMatch(name) {
160				want = false
161			}
162
163			if !fi.IsDir() {
164				if fi.Mode()&fs.ModeSymlink != 0 && want && strings.Contains(m.pattern, "...") {
165					if target, err := fsys.Stat(path); err == nil && target.IsDir() {
166						fmt.Fprintf(os.Stderr, "warning: ignoring symlink %s\n", path)
167					}
168				}
169				return nil
170			}
171			if !want {
172				return filepath.SkipDir
173			}
174
175			if have[name] {
176				return nil
177			}
178			have[name] = true
179			if !match(name) {
180				return nil
181			}
182			pkg, err := cfg.BuildContext.ImportDir(path, 0)
183			if err != nil {
184				if _, noGo := err.(*build.NoGoError); noGo {
185					// The package does not actually exist, so record neither the package
186					// nor the error.
187					return nil
188				}
189				// There was an error importing path, but not matching it,
190				// which is all that Match promises to do.
191				// Ignore the import error.
192			}
193
194			// If we are expanding "cmd", skip main
195			// packages under cmd/vendor. At least as of
196			// March, 2017, there is one there for the
197			// vendored pprof tool.
198			if m.pattern == "cmd" && pkg != nil && strings.HasPrefix(pkg.ImportPath, "cmd/vendor") && pkg.Name == "main" {
199				return nil
200			}
201
202			m.Pkgs = append(m.Pkgs, name)
203			return nil
204		})
205		if err != nil {
206			m.AddError(err)
207		}
208	}
209}
210
211// MatchDirs sets m.Dirs to a non-nil slice containing all directories that
212// potentially match a local pattern. The pattern must begin with an absolute
213// path, or "./", or "../". On Windows, the pattern may use slash or backslash
214// separators or a mix of both.
215//
216// If any errors may have caused the set of directories to be incomplete,
217// MatchDirs appends those errors to m.Errs.
218func (m *Match) MatchDirs(modRoots []string) {
219	m.Dirs = []string{}
220	if !m.IsLocal() {
221		m.AddError(fmt.Errorf("internal error: MatchDirs: %s is not a valid filesystem pattern", m.pattern))
222		return
223	}
224
225	if m.IsLiteral() {
226		m.Dirs = []string{m.pattern}
227		return
228	}
229
230	// Clean the path and create a matching predicate.
231	// filepath.Clean removes "./" prefixes (and ".\" on Windows). We need to
232	// preserve these, since they are meaningful in MatchPattern and in
233	// returned import paths.
234	cleanPattern := filepath.Clean(m.pattern)
235	isLocal := strings.HasPrefix(m.pattern, "./") || (os.PathSeparator == '\\' && strings.HasPrefix(m.pattern, `.\`))
236	prefix := ""
237	if cleanPattern != "." && isLocal {
238		prefix = "./"
239		cleanPattern = "." + string(os.PathSeparator) + cleanPattern
240	}
241	slashPattern := filepath.ToSlash(cleanPattern)
242	match := pkgpattern.MatchPattern(slashPattern)
243
244	// Find directory to begin the scan.
245	// Could be smarter but this one optimization
246	// is enough for now, since ... is usually at the
247	// end of a path.
248	i := strings.Index(cleanPattern, "...")
249	dir, _ := filepath.Split(cleanPattern[:i])
250
251	// pattern begins with ./ or ../.
252	// path.Clean will discard the ./ but not the ../.
253	// We need to preserve the ./ for pattern matching
254	// and in the returned import paths.
255
256	if len(modRoots) > 1 {
257		abs, err := filepath.Abs(dir)
258		if err != nil {
259			m.AddError(err)
260			return
261		}
262		var found bool
263		for _, modRoot := range modRoots {
264			if modRoot != "" && str.HasFilePathPrefix(abs, modRoot) {
265				found = true
266			}
267		}
268		if !found {
269			plural := ""
270			if len(modRoots) > 1 {
271				plural = "s"
272			}
273			m.AddError(fmt.Errorf("directory %s is outside module root%s (%s)", abs, plural, strings.Join(modRoots, ", ")))
274		}
275	}
276
277	// If dir is actually a symlink to a directory,
278	// we want to follow it (see https://go.dev/issue/50807).
279	// Add a trailing separator to force that to happen.
280	dir = str.WithFilePathSeparator(dir)
281	err := fsys.Walk(dir, func(path string, fi fs.FileInfo, err error) error {
282		if err != nil {
283			return err // Likely a permission error, which could interfere with matching.
284		}
285		if !fi.IsDir() {
286			return nil
287		}
288		top := false
289		if path == dir {
290			// Walk starts at dir and recurses. For the recursive case,
291			// the path is the result of filepath.Join, which calls filepath.Clean.
292			// The initial case is not Cleaned, though, so we do this explicitly.
293			//
294			// This converts a path like "./io/" to "io". Without this step, running
295			// "cd $GOROOT/src; go list ./io/..." would incorrectly skip the io
296			// package, because prepending the prefix "./" to the unclean path would
297			// result in "././io", and match("././io") returns false.
298			top = true
299			path = filepath.Clean(path)
300		}
301
302		// Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..".
303		_, elem := filepath.Split(path)
304		dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".."
305		if dot || strings.HasPrefix(elem, "_") || elem == "testdata" {
306			return filepath.SkipDir
307		}
308
309		if !top && cfg.ModulesEnabled {
310			// Ignore other modules found in subdirectories.
311			if fi, err := fsys.Stat(filepath.Join(path, "go.mod")); err == nil && !fi.IsDir() {
312				return filepath.SkipDir
313			}
314		}
315
316		name := prefix + filepath.ToSlash(path)
317		if !match(name) {
318			return nil
319		}
320
321		// We keep the directory if we can import it, or if we can't import it
322		// due to invalid Go source files. This means that directories containing
323		// parse errors will be built (and fail) instead of being silently skipped
324		// as not matching the pattern. Go 1.5 and earlier skipped, but that
325		// behavior means people miss serious mistakes.
326		// See golang.org/issue/11407.
327		if p, err := cfg.BuildContext.ImportDir(path, 0); err != nil && (p == nil || len(p.InvalidGoFiles) == 0) {
328			if _, noGo := err.(*build.NoGoError); noGo {
329				// The package does not actually exist, so record neither the package
330				// nor the error.
331				return nil
332			}
333			// There was an error importing path, but not matching it,
334			// which is all that Match promises to do.
335			// Ignore the import error.
336		}
337		m.Dirs = append(m.Dirs, name)
338		return nil
339	})
340	if err != nil {
341		m.AddError(err)
342	}
343}
344
345// WarnUnmatched warns about patterns that didn't match any packages.
346func WarnUnmatched(matches []*Match) {
347	for _, m := range matches {
348		if len(m.Pkgs) == 0 && len(m.Errs) == 0 {
349			fmt.Fprintf(os.Stderr, "go: warning: %q matched no packages\n", m.pattern)
350		}
351	}
352}
353
354// ImportPaths returns the matching paths to use for the given command line.
355// It calls ImportPathsQuiet and then WarnUnmatched.
356func ImportPaths(patterns, modRoots []string) []*Match {
357	matches := ImportPathsQuiet(patterns, modRoots)
358	WarnUnmatched(matches)
359	return matches
360}
361
362// ImportPathsQuiet is like ImportPaths but does not warn about patterns with no matches.
363func ImportPathsQuiet(patterns, modRoots []string) []*Match {
364	var out []*Match
365	for _, a := range CleanPatterns(patterns) {
366		m := NewMatch(a)
367		if m.IsLocal() {
368			m.MatchDirs(modRoots)
369
370			// Change the file import path to a regular import path if the package
371			// is in GOPATH or GOROOT. We don't report errors here; LoadImport
372			// (or something similar) will report them later.
373			m.Pkgs = make([]string, len(m.Dirs))
374			for i, dir := range m.Dirs {
375				absDir := dir
376				if !filepath.IsAbs(dir) {
377					absDir = filepath.Join(base.Cwd(), dir)
378				}
379				if bp, _ := cfg.BuildContext.ImportDir(absDir, build.FindOnly); bp.ImportPath != "" && bp.ImportPath != "." {
380					m.Pkgs[i] = bp.ImportPath
381				} else {
382					m.Pkgs[i] = dir
383				}
384			}
385		} else {
386			m.MatchPackages()
387		}
388
389		out = append(out, m)
390	}
391	return out
392}
393
394// CleanPatterns returns the patterns to use for the given command line. It
395// canonicalizes the patterns but does not evaluate any matches. For patterns
396// that are not local or absolute paths, it preserves text after '@' to avoid
397// modifying version queries.
398func CleanPatterns(patterns []string) []string {
399	if len(patterns) == 0 {
400		return []string{"."}
401	}
402	var out []string
403	for _, a := range patterns {
404		var p, v string
405		if build.IsLocalImport(a) || filepath.IsAbs(a) {
406			p = a
407		} else if i := strings.IndexByte(a, '@'); i < 0 {
408			p = a
409		} else {
410			p = a[:i]
411			v = a[i:]
412		}
413
414		// Arguments may be either file paths or import paths.
415		// As a courtesy to Windows developers, rewrite \ to /
416		// in arguments that look like import paths.
417		// Don't replace slashes in absolute paths.
418		if filepath.IsAbs(p) {
419			p = filepath.Clean(p)
420		} else {
421			if filepath.Separator == '\\' {
422				p = strings.ReplaceAll(p, `\`, `/`)
423			}
424
425			// Put argument in canonical form, but preserve leading ./.
426			if strings.HasPrefix(p, "./") {
427				p = "./" + path.Clean(p)
428				if p == "./." {
429					p = "."
430				}
431			} else {
432				p = path.Clean(p)
433			}
434		}
435
436		out = append(out, p+v)
437	}
438	return out
439}
440
441// IsStandardImportPath reports whether $GOROOT/src/path should be considered
442// part of the standard distribution. For historical reasons we allow people to add
443// their own code to $GOROOT instead of using $GOPATH, but we assume that
444// code will start with a domain name (dot in the first element).
445//
446// Note that this function is meant to evaluate whether a directory found in GOROOT
447// should be treated as part of the standard library. It should not be used to decide
448// that a directory found in GOPATH should be rejected: directories in GOPATH
449// need not have dots in the first element, and they just take their chances
450// with future collisions in the standard library.
451func IsStandardImportPath(path string) bool {
452	i := strings.Index(path, "/")
453	if i < 0 {
454		i = len(path)
455	}
456	elem := path[:i]
457	return !strings.Contains(elem, ".")
458}
459
460// IsRelativePath reports whether pattern should be interpreted as a directory
461// path relative to the current directory, as opposed to a pattern matching
462// import paths.
463func IsRelativePath(pattern string) bool {
464	return strings.HasPrefix(pattern, "./") || strings.HasPrefix(pattern, "../") || pattern == "." || pattern == ".."
465}
466
467// InDir checks whether path is in the file tree rooted at dir.
468// If so, InDir returns an equivalent path relative to dir.
469// If not, InDir returns an empty string.
470// InDir makes some effort to succeed even in the presence of symbolic links.
471func InDir(path, dir string) string {
472	// inDirLex reports whether path is lexically in dir,
473	// without considering symbolic or hard links.
474	inDirLex := func(path, dir string) (string, bool) {
475		if dir == "" {
476			return path, true
477		}
478		rel := str.TrimFilePathPrefix(path, dir)
479		if rel == path {
480			return "", false
481		}
482		if rel == "" {
483			return ".", true
484		}
485		return rel, true
486	}
487
488	if rel, ok := inDirLex(path, dir); ok {
489		return rel
490	}
491	xpath, err := filepath.EvalSymlinks(path)
492	if err != nil || xpath == path {
493		xpath = ""
494	} else {
495		if rel, ok := inDirLex(xpath, dir); ok {
496			return rel
497		}
498	}
499
500	xdir, err := filepath.EvalSymlinks(dir)
501	if err == nil && xdir != dir {
502		if rel, ok := inDirLex(path, xdir); ok {
503			return rel
504		}
505		if xpath != "" {
506			if rel, ok := inDirLex(xpath, xdir); ok {
507				return rel
508			}
509		}
510	}
511	return ""
512}
513