1package dots
2
3import (
4	"go/build"
5	"log"
6	"os"
7	"path"
8	"path/filepath"
9	"regexp"
10	"runtime"
11	"strings"
12)
13
14var (
15	buildContext = build.Default
16	goroot       = filepath.Clean(runtime.GOROOT())
17	gorootSrc    = filepath.Join(goroot, "src")
18)
19
20func flatten(arr [][]string) []string {
21	var res []string
22	for _, e := range arr {
23		res = append(res, e...)
24	}
25	return res
26}
27
28// Resolve accepts a slice of paths with optional "..." placeholder and a slice with paths to be skipped.
29// The final result is the set of all files from the selected directories subtracted with
30// the files in the skip slice.
31func Resolve(includePatterns, skipPatterns []string) ([]string, error) {
32	skip, err := resolvePatterns(skipPatterns)
33	filter := newPathFilter(flatten(skip))
34	if err != nil {
35		return nil, err
36	}
37
38	pathSet := map[string]bool{}
39	includePackages, err := resolvePatterns(includePatterns)
40	include := flatten(includePackages)
41	if err != nil {
42		return nil, err
43	}
44
45	var result []string
46	for _, i := range include {
47		if _, ok := pathSet[i]; !ok && !filter(i) {
48			pathSet[i] = true
49			result = append(result, i)
50		}
51	}
52	return result, err
53}
54
55// ResolvePackages accepts a slice of paths with optional "..." placeholder and a slice with paths to be skipped.
56// The final result is the set of all files from the selected directories subtracted with
57// the files in the skip slice. The difference between `Resolve` and `ResolvePackages`
58// is that `ResolvePackages` preserves the package structure in the nested slices.
59func ResolvePackages(includePatterns, skipPatterns []string) ([][]string, error) {
60	skip, err := resolvePatterns(skipPatterns)
61	filter := newPathFilter(flatten(skip))
62	if err != nil {
63		return nil, err
64	}
65
66	pathSet := map[string]bool{}
67	include, err := resolvePatterns(includePatterns)
68	if err != nil {
69		return nil, err
70	}
71
72	var result [][]string
73	for _, p := range include {
74		var packageFiles []string
75		for _, f := range p {
76			if _, ok := pathSet[f]; !ok && !filter(f) {
77				pathSet[f] = true
78				packageFiles = append(packageFiles, f)
79			}
80		}
81		result = append(result, packageFiles)
82	}
83	return result, err
84}
85
86func isDir(filename string) bool {
87	fi, err := os.Stat(filename)
88	return err == nil && fi.IsDir()
89}
90
91func exists(filename string) bool {
92	_, err := os.Stat(filename)
93	return err == nil
94}
95
96func resolveDir(dirname string) ([]string, error) {
97	pkg, err := build.ImportDir(dirname, 0)
98	return resolveImportedPackage(pkg, err)
99}
100
101func resolvePackage(pkgname string) ([]string, error) {
102	pkg, err := build.Import(pkgname, ".", 0)
103	return resolveImportedPackage(pkg, err)
104}
105
106func resolveImportedPackage(pkg *build.Package, err error) ([]string, error) {
107	if err != nil {
108		if _, nogo := err.(*build.NoGoError); nogo {
109			// Don't complain if the failure is due to no Go source files.
110			return nil, nil
111		}
112		return nil, err
113	}
114
115	var files []string
116	files = append(files, pkg.GoFiles...)
117	files = append(files, pkg.CgoFiles...)
118	files = append(files, pkg.TestGoFiles...)
119	if pkg.Dir != "." {
120		for i, f := range files {
121			files[i] = filepath.Join(pkg.Dir, f)
122		}
123	}
124	return files, nil
125}
126
127func resolvePatterns(patterns []string) ([][]string, error) {
128	var files [][]string
129	for _, pattern := range patterns {
130		f, err := resolvePattern(pattern)
131		if err != nil {
132			return nil, err
133		}
134		files = append(files, f...)
135	}
136	return files, nil
137}
138
139func resolvePattern(pattern string) ([][]string, error) {
140	// dirsRun, filesRun, and pkgsRun indicate whether golint is applied to
141	// directory, file or package targets. The distinction affects which
142	// checks are run. It is no valid to mix target types.
143	var dirsRun, filesRun, pkgsRun int
144	var matches []string
145
146	if strings.HasSuffix(pattern, "/...") && isDir(pattern[:len(pattern)-len("/...")]) {
147		dirsRun = 1
148		for _, dirname := range matchPackagesInFS(pattern) {
149			matches = append(matches, dirname)
150		}
151	} else if isDir(pattern) {
152		dirsRun = 1
153		matches = append(matches, pattern)
154	} else if exists(pattern) {
155		filesRun = 1
156		matches = append(matches, pattern)
157	} else {
158		pkgsRun = 1
159		matches = append(matches, pattern)
160	}
161
162	result := [][]string{}
163	switch {
164	case dirsRun == 1:
165		for _, dir := range matches {
166			res, err := resolveDir(dir)
167			if err != nil {
168				return nil, err
169			}
170			result = append(result, res)
171		}
172	case filesRun == 1:
173		return [][]string{matches}, nil
174	case pkgsRun == 1:
175		for _, pkg := range importPaths(matches) {
176			res, err := resolvePackage(pkg)
177			if err != nil {
178				return nil, err
179			}
180			result = append(result, res)
181		}
182	}
183	return result, nil
184}
185
186func newPathFilter(skip []string) func(string) bool {
187	filter := map[string]bool{}
188	for _, name := range skip {
189		filter[name] = true
190	}
191
192	return func(path string) bool {
193		base := filepath.Base(path)
194		if filter[base] || filter[path] {
195			return true
196		}
197		return base != "." && base != ".." && strings.ContainsAny(base[0:1], "_.")
198	}
199}
200
201// importPathsNoDotExpansion returns the import paths to use for the given
202// command line, but it does no ... expansion.
203func importPathsNoDotExpansion(args []string) []string {
204	if len(args) == 0 {
205		return []string{"."}
206	}
207	var out []string
208	for _, a := range args {
209		// Arguments are supposed to be import paths, but
210		// as a courtesy to Windows developers, rewrite \ to /
211		// in command-line arguments.  Handles .\... and so on.
212		if filepath.Separator == '\\' {
213			a = strings.Replace(a, `\`, `/`, -1)
214		}
215
216		// Put argument in canonical form, but preserve leading ./.
217		if strings.HasPrefix(a, "./") {
218			a = "./" + path.Clean(a)
219			if a == "./." {
220				a = "."
221			}
222		} else {
223			a = path.Clean(a)
224		}
225		if a == "all" || a == "std" {
226			out = append(out, matchPackages(a)...)
227			continue
228		}
229		out = append(out, a)
230	}
231	return out
232}
233
234// importPaths returns the import paths to use for the given command line.
235func importPaths(args []string) []string {
236	args = importPathsNoDotExpansion(args)
237	var out []string
238	for _, a := range args {
239		if strings.Contains(a, "...") {
240			if build.IsLocalImport(a) {
241				out = append(out, matchPackagesInFS(a)...)
242			} else {
243				out = append(out, matchPackages(a)...)
244			}
245			continue
246		}
247		out = append(out, a)
248	}
249	return out
250}
251
252// matchPattern(pattern)(name) reports whether
253// name matches pattern.  Pattern is a limited glob
254// pattern in which '...' means 'any string' and there
255// is no other special syntax.
256func matchPattern(pattern string) func(name string) bool {
257	re := regexp.QuoteMeta(pattern)
258	re = strings.Replace(re, `\.\.\.`, `.*`, -1)
259	// Special case: foo/... matches foo too.
260	if strings.HasSuffix(re, `/.*`) {
261		re = re[:len(re)-len(`/.*`)] + `(/.*)?`
262	}
263	reg := regexp.MustCompile(`^` + re + `$`)
264	return func(name string) bool {
265		return reg.MatchString(name)
266	}
267}
268
269// hasPathPrefix reports whether the path s begins with the
270// elements in prefix.
271func hasPathPrefix(s, prefix string) bool {
272	switch {
273	default:
274		return false
275	case len(s) == len(prefix):
276		return s == prefix
277	case len(s) > len(prefix):
278		if prefix != "" && prefix[len(prefix)-1] == '/' {
279			return strings.HasPrefix(s, prefix)
280		}
281		return s[len(prefix)] == '/' && s[:len(prefix)] == prefix
282	}
283}
284
285// treeCanMatchPattern(pattern)(name) reports whether
286// name or children of name can possibly match pattern.
287// Pattern is the same limited glob accepted by matchPattern.
288func treeCanMatchPattern(pattern string) func(name string) bool {
289	wildCard := false
290	if i := strings.Index(pattern, "..."); i >= 0 {
291		wildCard = true
292		pattern = pattern[:i]
293	}
294	return func(name string) bool {
295		return len(name) <= len(pattern) && hasPathPrefix(pattern, name) ||
296			wildCard && strings.HasPrefix(name, pattern)
297	}
298}
299
300func matchPackages(pattern string) []string {
301	match := func(string) bool { return true }
302	treeCanMatch := func(string) bool { return true }
303	if pattern != "all" && pattern != "std" {
304		match = matchPattern(pattern)
305		treeCanMatch = treeCanMatchPattern(pattern)
306	}
307
308	have := map[string]bool{
309		"builtin": true, // ignore pseudo-package that exists only for documentation
310	}
311	if !buildContext.CgoEnabled {
312		have["runtime/cgo"] = true // ignore during walk
313	}
314	var pkgs []string
315
316	// Commands
317	cmd := filepath.Join(goroot, "src/cmd") + string(filepath.Separator)
318	filepath.Walk(cmd, func(path string, fi os.FileInfo, err error) error {
319		if err != nil || !fi.IsDir() || path == cmd {
320			return nil
321		}
322		name := path[len(cmd):]
323		if !treeCanMatch(name) {
324			return filepath.SkipDir
325		}
326		// Commands are all in cmd/, not in subdirectories.
327		if strings.Contains(name, string(filepath.Separator)) {
328			return filepath.SkipDir
329		}
330
331		// We use, e.g., cmd/gofmt as the pseudo import path for gofmt.
332		name = "cmd/" + name
333		if have[name] {
334			return nil
335		}
336		have[name] = true
337		if !match(name) {
338			return nil
339		}
340		_, err = buildContext.ImportDir(path, 0)
341		if err != nil {
342			if _, noGo := err.(*build.NoGoError); !noGo {
343				log.Print(err)
344			}
345			return nil
346		}
347		pkgs = append(pkgs, name)
348		return nil
349	})
350
351	for _, src := range buildContext.SrcDirs() {
352		if (pattern == "std" || pattern == "cmd") && src != gorootSrc {
353			continue
354		}
355		src = filepath.Clean(src) + string(filepath.Separator)
356		root := src
357		if pattern == "cmd" {
358			root += "cmd" + string(filepath.Separator)
359		}
360		filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
361			if err != nil || !fi.IsDir() || path == src {
362				return nil
363			}
364
365			// Avoid .foo, _foo, and testdata directory trees.
366			_, elem := filepath.Split(path)
367			if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" {
368				return filepath.SkipDir
369			}
370
371			name := filepath.ToSlash(path[len(src):])
372			if pattern == "std" && (strings.Contains(name, ".") || name == "cmd") {
373				// The name "std" is only the standard library.
374				// If the name is cmd, it's the root of the command tree.
375				return filepath.SkipDir
376			}
377			if !treeCanMatch(name) {
378				return filepath.SkipDir
379			}
380			if have[name] {
381				return nil
382			}
383			have[name] = true
384			if !match(name) {
385				return nil
386			}
387			_, err = buildContext.ImportDir(path, 0)
388			if err != nil {
389				if _, noGo := err.(*build.NoGoError); noGo {
390					return nil
391				}
392			}
393			pkgs = append(pkgs, name)
394			return nil
395		})
396	}
397	return pkgs
398}
399
400func matchPackagesInFS(pattern string) []string {
401	// Find directory to begin the scan.
402	// Could be smarter but this one optimization
403	// is enough for now, since ... is usually at the
404	// end of a path.
405	i := strings.Index(pattern, "...")
406	dir, _ := path.Split(pattern[:i])
407
408	// pattern begins with ./ or ../.
409	// path.Clean will discard the ./ but not the ../.
410	// We need to preserve the ./ for pattern matching
411	// and in the returned import paths.
412	prefix := ""
413	if strings.HasPrefix(pattern, "./") {
414		prefix = "./"
415	}
416	match := matchPattern(pattern)
417
418	var pkgs []string
419	filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
420		if err != nil || !fi.IsDir() {
421			return nil
422		}
423		if path == dir {
424			// filepath.Walk starts at dir and recurses. For the recursive case,
425			// the path is the result of filepath.Join, which calls filepath.Clean.
426			// The initial case is not Cleaned, though, so we do this explicitly.
427			//
428			// This converts a path like "./io/" to "io". Without this step, running
429			// "cd $GOROOT/src/pkg; go list ./io/..." would incorrectly skip the io
430			// package, because prepending the prefix "./" to the unclean path would
431			// result in "././io", and match("././io") returns false.
432			path = filepath.Clean(path)
433		}
434
435		// Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..".
436		_, elem := filepath.Split(path)
437		dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".."
438		if dot || strings.HasPrefix(elem, "_") || elem == "testdata" {
439			return filepath.SkipDir
440		}
441
442		name := prefix + filepath.ToSlash(path)
443		if !match(name) {
444			return nil
445		}
446		if _, err = build.ImportDir(path, 0); err != nil {
447			if _, noGo := err.(*build.NoGoError); !noGo {
448				log.Print(err)
449			}
450			return nil
451		}
452		pkgs = append(pkgs, name)
453		return nil
454	})
455	return pkgs
456}
457