1// Copyright 2018 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 modload
6
7import (
8	"context"
9	"fmt"
10	"io/fs"
11	"os"
12	"path/filepath"
13	"strings"
14
15	"cmd/go/internal/cfg"
16	"cmd/go/internal/fsys"
17	"cmd/go/internal/imports"
18	"cmd/go/internal/search"
19
20	"golang.org/x/mod/module"
21)
22
23type stdFilter int8
24
25const (
26	omitStd = stdFilter(iota)
27	includeStd
28)
29
30// matchPackages is like m.MatchPackages, but uses a local variable (rather than
31// a global) for tags, can include or exclude packages in the standard library,
32// and is restricted to the given list of modules.
33func matchPackages(ctx context.Context, m *search.Match, tags map[string]bool, filter stdFilter, modules []module.Version) {
34	m.Pkgs = []string{}
35
36	isMatch := func(string) bool { return true }
37	treeCanMatch := func(string) bool { return true }
38	if !m.IsMeta() {
39		isMatch = search.MatchPattern(m.Pattern())
40		treeCanMatch = search.TreeCanMatchPattern(m.Pattern())
41	}
42
43	have := map[string]bool{
44		"builtin": true, // ignore pseudo-package that exists only for documentation
45	}
46	if !cfg.BuildContext.CgoEnabled {
47		have["runtime/cgo"] = true // ignore during walk
48	}
49
50	type pruning int8
51	const (
52		pruneVendor = pruning(1 << iota)
53		pruneGoMod
54	)
55
56	walkPkgs := func(root, importPathRoot string, prune pruning) {
57		root = filepath.Clean(root)
58		err := fsys.Walk(root, func(path string, fi fs.FileInfo, err error) error {
59			if err != nil {
60				m.AddError(err)
61				return nil
62			}
63
64			want := true
65			elem := ""
66
67			// Don't use GOROOT/src but do walk down into it.
68			if path == root {
69				if importPathRoot == "" {
70					return nil
71				}
72			} else {
73				// Avoid .foo, _foo, and testdata subdirectory trees.
74				_, elem = filepath.Split(path)
75				if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" {
76					want = false
77				}
78			}
79
80			name := importPathRoot + filepath.ToSlash(path[len(root):])
81			if importPathRoot == "" {
82				name = name[1:] // cut leading slash
83			}
84			if !treeCanMatch(name) {
85				want = false
86			}
87
88			if !fi.IsDir() {
89				if fi.Mode()&fs.ModeSymlink != 0 && want && strings.Contains(m.Pattern(), "...") {
90					if target, err := fsys.Stat(path); err == nil && target.IsDir() {
91						fmt.Fprintf(os.Stderr, "warning: ignoring symlink %s\n", path)
92					}
93				}
94				return nil
95			}
96
97			if !want {
98				return filepath.SkipDir
99			}
100			// Stop at module boundaries.
101			if (prune&pruneGoMod != 0) && path != root {
102				if fi, err := os.Stat(filepath.Join(path, "go.mod")); err == nil && !fi.IsDir() {
103					return filepath.SkipDir
104				}
105			}
106
107			if !have[name] {
108				have[name] = true
109				if isMatch(name) {
110					if _, _, err := scanDir(path, tags); err != imports.ErrNoGo {
111						m.Pkgs = append(m.Pkgs, name)
112					}
113				}
114			}
115
116			if elem == "vendor" && (prune&pruneVendor != 0) {
117				return filepath.SkipDir
118			}
119			return nil
120		})
121		if err != nil {
122			m.AddError(err)
123		}
124	}
125
126	if filter == includeStd {
127		walkPkgs(cfg.GOROOTsrc, "", pruneGoMod)
128		if treeCanMatch("cmd") {
129			walkPkgs(filepath.Join(cfg.GOROOTsrc, "cmd"), "cmd", pruneGoMod)
130		}
131	}
132
133	if cfg.BuildMod == "vendor" {
134		mod := MainModules.mustGetSingleMainModule()
135		if modRoot := MainModules.ModRoot(mod); modRoot != "" {
136			walkPkgs(modRoot, MainModules.PathPrefix(mod), pruneGoMod|pruneVendor)
137			walkPkgs(filepath.Join(modRoot, "vendor"), "", pruneVendor)
138		}
139		return
140	}
141
142	for _, mod := range modules {
143		if !treeCanMatch(mod.Path) {
144			continue
145		}
146
147		var (
148			root, modPrefix string
149			isLocal         bool
150		)
151		if MainModules.Contains(mod.Path) {
152			if MainModules.ModRoot(mod) == "" {
153				continue // If there is no main module, we can't search in it.
154			}
155			root = MainModules.ModRoot(mod)
156			modPrefix = MainModules.PathPrefix(mod)
157			isLocal = true
158		} else {
159			var err error
160			const needSum = true
161			root, isLocal, err = fetch(ctx, mod, needSum)
162			if err != nil {
163				m.AddError(err)
164				continue
165			}
166			modPrefix = mod.Path
167		}
168
169		prune := pruneVendor
170		if isLocal {
171			prune |= pruneGoMod
172		}
173		walkPkgs(root, modPrefix, prune)
174	}
175
176	return
177}
178
179// MatchInModule identifies the packages matching the given pattern within the
180// given module version, which does not need to be in the build list or module
181// requirement graph.
182//
183// If m is the zero module.Version, MatchInModule matches the pattern
184// against the standard library (std and cmd) in GOROOT/src.
185func MatchInModule(ctx context.Context, pattern string, m module.Version, tags map[string]bool) *search.Match {
186	match := search.NewMatch(pattern)
187	if m == (module.Version{}) {
188		matchPackages(ctx, match, tags, includeStd, nil)
189	}
190
191	LoadModFile(ctx) // Sets Target, needed by fetch and matchPackages.
192
193	if !match.IsLiteral() {
194		matchPackages(ctx, match, tags, omitStd, []module.Version{m})
195		return match
196	}
197
198	const needSum = true
199	root, isLocal, err := fetch(ctx, m, needSum)
200	if err != nil {
201		match.Errs = []error{err}
202		return match
203	}
204
205	dir, haveGoFiles, err := dirInModule(pattern, m.Path, root, isLocal)
206	if err != nil {
207		match.Errs = []error{err}
208		return match
209	}
210	if haveGoFiles {
211		if _, _, err := scanDir(dir, tags); err != imports.ErrNoGo {
212			// ErrNoGo indicates that the directory is not actually a Go package,
213			// perhaps due to the tags in use. Any other non-nil error indicates a
214			// problem with one or more of the Go source files, but such an error does
215			// not stop the package from existing, so it has no impact on matching.
216			match.Pkgs = []string{pattern}
217		}
218	}
219	return match
220}
221