Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
456 lines
12 KiB
456 lines
12 KiB
package dots |
|
|
|
import ( |
|
"go/build" |
|
"log" |
|
"os" |
|
"path" |
|
"path/filepath" |
|
"regexp" |
|
"runtime" |
|
"strings" |
|
) |
|
|
|
var ( |
|
buildContext = build.Default |
|
goroot = filepath.Clean(runtime.GOROOT()) |
|
gorootSrc = filepath.Join(goroot, "src") |
|
) |
|
|
|
func flatten(arr [][]string) []string { |
|
var res []string |
|
for _, e := range arr { |
|
res = append(res, e...) |
|
} |
|
return res |
|
} |
|
|
|
// Resolve accepts a slice of paths with optional "..." placeholder and a slice with paths to be skipped. |
|
// The final result is the set of all files from the selected directories subtracted with |
|
// the files in the skip slice. |
|
func Resolve(includePatterns, skipPatterns []string) ([]string, error) { |
|
skip, err := resolvePatterns(skipPatterns) |
|
filter := newPathFilter(flatten(skip)) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
pathSet := map[string]bool{} |
|
includePackages, err := resolvePatterns(includePatterns) |
|
include := flatten(includePackages) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
var result []string |
|
for _, i := range include { |
|
if _, ok := pathSet[i]; !ok && !filter(i) { |
|
pathSet[i] = true |
|
result = append(result, i) |
|
} |
|
} |
|
return result, err |
|
} |
|
|
|
// ResolvePackages accepts a slice of paths with optional "..." placeholder and a slice with paths to be skipped. |
|
// The final result is the set of all files from the selected directories subtracted with |
|
// the files in the skip slice. The difference between `Resolve` and `ResolvePackages` |
|
// is that `ResolvePackages` preserves the package structure in the nested slices. |
|
func ResolvePackages(includePatterns, skipPatterns []string) ([][]string, error) { |
|
skip, err := resolvePatterns(skipPatterns) |
|
filter := newPathFilter(flatten(skip)) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
pathSet := map[string]bool{} |
|
include, err := resolvePatterns(includePatterns) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
var result [][]string |
|
for _, p := range include { |
|
var packageFiles []string |
|
for _, f := range p { |
|
if _, ok := pathSet[f]; !ok && !filter(f) { |
|
pathSet[f] = true |
|
packageFiles = append(packageFiles, f) |
|
} |
|
} |
|
result = append(result, packageFiles) |
|
} |
|
return result, err |
|
} |
|
|
|
func isDir(filename string) bool { |
|
fi, err := os.Stat(filename) |
|
return err == nil && fi.IsDir() |
|
} |
|
|
|
func exists(filename string) bool { |
|
_, err := os.Stat(filename) |
|
return err == nil |
|
} |
|
|
|
func resolveDir(dirname string) ([]string, error) { |
|
pkg, err := build.ImportDir(dirname, 0) |
|
return resolveImportedPackage(pkg, err) |
|
} |
|
|
|
func resolvePackage(pkgname string) ([]string, error) { |
|
pkg, err := build.Import(pkgname, ".", 0) |
|
return resolveImportedPackage(pkg, err) |
|
} |
|
|
|
func resolveImportedPackage(pkg *build.Package, err error) ([]string, error) { |
|
if err != nil { |
|
if _, nogo := err.(*build.NoGoError); nogo { |
|
// Don't complain if the failure is due to no Go source files. |
|
return nil, nil |
|
} |
|
return nil, err |
|
} |
|
|
|
var files []string |
|
files = append(files, pkg.GoFiles...) |
|
files = append(files, pkg.CgoFiles...) |
|
files = append(files, pkg.TestGoFiles...) |
|
if pkg.Dir != "." { |
|
for i, f := range files { |
|
files[i] = filepath.Join(pkg.Dir, f) |
|
} |
|
} |
|
return files, nil |
|
} |
|
|
|
func resolvePatterns(patterns []string) ([][]string, error) { |
|
var files [][]string |
|
for _, pattern := range patterns { |
|
f, err := resolvePattern(pattern) |
|
if err != nil { |
|
return nil, err |
|
} |
|
files = append(files, f...) |
|
} |
|
return files, nil |
|
} |
|
|
|
func resolvePattern(pattern string) ([][]string, error) { |
|
// dirsRun, filesRun, and pkgsRun indicate whether golint is applied to |
|
// directory, file or package targets. The distinction affects which |
|
// checks are run. It is no valid to mix target types. |
|
var dirsRun, filesRun, pkgsRun int |
|
var matches []string |
|
|
|
if strings.HasSuffix(pattern, "/...") && isDir(pattern[:len(pattern)-len("/...")]) { |
|
dirsRun = 1 |
|
for _, dirname := range matchPackagesInFS(pattern) { |
|
matches = append(matches, dirname) |
|
} |
|
} else if isDir(pattern) { |
|
dirsRun = 1 |
|
matches = append(matches, pattern) |
|
} else if exists(pattern) { |
|
filesRun = 1 |
|
matches = append(matches, pattern) |
|
} else { |
|
pkgsRun = 1 |
|
matches = append(matches, pattern) |
|
} |
|
|
|
result := [][]string{} |
|
switch { |
|
case dirsRun == 1: |
|
for _, dir := range matches { |
|
res, err := resolveDir(dir) |
|
if err != nil { |
|
return nil, err |
|
} |
|
result = append(result, res) |
|
} |
|
case filesRun == 1: |
|
return [][]string{matches}, nil |
|
case pkgsRun == 1: |
|
for _, pkg := range importPaths(matches) { |
|
res, err := resolvePackage(pkg) |
|
if err != nil { |
|
return nil, err |
|
} |
|
result = append(result, res) |
|
} |
|
} |
|
return result, nil |
|
} |
|
|
|
func newPathFilter(skip []string) func(string) bool { |
|
filter := map[string]bool{} |
|
for _, name := range skip { |
|
filter[name] = true |
|
} |
|
|
|
return func(path string) bool { |
|
base := filepath.Base(path) |
|
if filter[base] || filter[path] { |
|
return true |
|
} |
|
return base != "." && base != ".." && strings.ContainsAny(base[0:1], "_.") |
|
} |
|
} |
|
|
|
// importPathsNoDotExpansion returns the import paths to use for the given |
|
// command line, but it does no ... expansion. |
|
func importPathsNoDotExpansion(args []string) []string { |
|
if len(args) == 0 { |
|
return []string{"."} |
|
} |
|
var out []string |
|
for _, a := range args { |
|
// Arguments are supposed to be import paths, but |
|
// as a courtesy to Windows developers, rewrite \ to / |
|
// in command-line arguments. Handles .\... and so on. |
|
if filepath.Separator == '\\' { |
|
a = strings.Replace(a, `\`, `/`, -1) |
|
} |
|
|
|
// Put argument in canonical form, but preserve leading ./. |
|
if strings.HasPrefix(a, "./") { |
|
a = "./" + path.Clean(a) |
|
if a == "./." { |
|
a = "." |
|
} |
|
} else { |
|
a = path.Clean(a) |
|
} |
|
if a == "all" || a == "std" { |
|
out = append(out, matchPackages(a)...) |
|
continue |
|
} |
|
out = append(out, a) |
|
} |
|
return out |
|
} |
|
|
|
// importPaths returns the import paths to use for the given command line. |
|
func importPaths(args []string) []string { |
|
args = importPathsNoDotExpansion(args) |
|
var out []string |
|
for _, a := range args { |
|
if strings.Contains(a, "...") { |
|
if build.IsLocalImport(a) { |
|
out = append(out, matchPackagesInFS(a)...) |
|
} else { |
|
out = append(out, matchPackages(a)...) |
|
} |
|
continue |
|
} |
|
out = append(out, a) |
|
} |
|
return out |
|
} |
|
|
|
// 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. |
|
func matchPattern(pattern string) func(name string) bool { |
|
re := regexp.QuoteMeta(pattern) |
|
re = strings.Replace(re, `\.\.\.`, `.*`, -1) |
|
// Special case: foo/... matches foo too. |
|
if strings.HasSuffix(re, `/.*`) { |
|
re = re[:len(re)-len(`/.*`)] + `(/.*)?` |
|
} |
|
reg := regexp.MustCompile(`^` + re + `$`) |
|
return func(name string) bool { |
|
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 |
|
} |
|
} |
|
|
|
// 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) |
|
} |
|
} |
|
|
|
func matchPackages(pattern string) []string { |
|
match := func(string) bool { return true } |
|
treeCanMatch := func(string) bool { return true } |
|
if pattern != "all" && pattern != "std" { |
|
match = matchPattern(pattern) |
|
treeCanMatch = treeCanMatchPattern(pattern) |
|
} |
|
|
|
have := map[string]bool{ |
|
"builtin": true, // ignore pseudo-package that exists only for documentation |
|
} |
|
if !buildContext.CgoEnabled { |
|
have["runtime/cgo"] = true // ignore during walk |
|
} |
|
var pkgs []string |
|
|
|
// Commands |
|
cmd := filepath.Join(goroot, "src/cmd") + string(filepath.Separator) |
|
filepath.Walk(cmd, func(path string, fi os.FileInfo, err error) error { |
|
if err != nil || !fi.IsDir() || path == cmd { |
|
return nil |
|
} |
|
name := path[len(cmd):] |
|
if !treeCanMatch(name) { |
|
return filepath.SkipDir |
|
} |
|
// Commands are all in cmd/, not in subdirectories. |
|
if strings.Contains(name, string(filepath.Separator)) { |
|
return filepath.SkipDir |
|
} |
|
|
|
// We use, e.g., cmd/gofmt as the pseudo import path for gofmt. |
|
name = "cmd/" + name |
|
if have[name] { |
|
return nil |
|
} |
|
have[name] = true |
|
if !match(name) { |
|
return nil |
|
} |
|
_, err = buildContext.ImportDir(path, 0) |
|
if err != nil { |
|
if _, noGo := err.(*build.NoGoError); !noGo { |
|
log.Print(err) |
|
} |
|
return nil |
|
} |
|
pkgs = append(pkgs, name) |
|
return nil |
|
}) |
|
|
|
for _, src := range buildContext.SrcDirs() { |
|
if (pattern == "std" || pattern == "cmd") && src != gorootSrc { |
|
continue |
|
} |
|
src = filepath.Clean(src) + string(filepath.Separator) |
|
root := src |
|
if pattern == "cmd" { |
|
root += "cmd" + string(filepath.Separator) |
|
} |
|
filepath.Walk(root, func(path string, fi os.FileInfo, err error) error { |
|
if err != nil || !fi.IsDir() || path == src { |
|
return nil |
|
} |
|
|
|
// Avoid .foo, _foo, and testdata directory trees. |
|
_, elem := filepath.Split(path) |
|
if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" { |
|
return filepath.SkipDir |
|
} |
|
|
|
name := filepath.ToSlash(path[len(src):]) |
|
if pattern == "std" && (strings.Contains(name, ".") || name == "cmd") { |
|
// The name "std" is only the standard library. |
|
// If the name is cmd, it's the root of the command tree. |
|
return filepath.SkipDir |
|
} |
|
if !treeCanMatch(name) { |
|
return filepath.SkipDir |
|
} |
|
if have[name] { |
|
return nil |
|
} |
|
have[name] = true |
|
if !match(name) { |
|
return nil |
|
} |
|
_, err = buildContext.ImportDir(path, 0) |
|
if err != nil { |
|
if _, noGo := err.(*build.NoGoError); noGo { |
|
return nil |
|
} |
|
} |
|
pkgs = append(pkgs, name) |
|
return nil |
|
}) |
|
} |
|
return pkgs |
|
} |
|
|
|
func matchPackagesInFS(pattern string) []string { |
|
// Find directory to begin the scan. |
|
// Could be smarter but this one optimization |
|
// is enough for now, since ... is usually at the |
|
// end of a path. |
|
i := strings.Index(pattern, "...") |
|
dir, _ := path.Split(pattern[:i]) |
|
|
|
// pattern begins with ./ or ../. |
|
// path.Clean will discard the ./ but not the ../. |
|
// We need to preserve the ./ for pattern matching |
|
// and in the returned import paths. |
|
prefix := "" |
|
if strings.HasPrefix(pattern, "./") { |
|
prefix = "./" |
|
} |
|
match := matchPattern(pattern) |
|
|
|
var pkgs []string |
|
filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { |
|
if err != nil || !fi.IsDir() { |
|
return nil |
|
} |
|
if path == dir { |
|
// filepath.Walk starts at dir and recurses. For the recursive case, |
|
// the path is the result of filepath.Join, which calls filepath.Clean. |
|
// The initial case is not Cleaned, though, so we do this explicitly. |
|
// |
|
// This converts a path like "./io/" to "io". Without this step, running |
|
// "cd $GOROOT/src/pkg; go list ./io/..." would incorrectly skip the io |
|
// package, because prepending the prefix "./" to the unclean path would |
|
// result in "././io", and match("././io") returns false. |
|
path = filepath.Clean(path) |
|
} |
|
|
|
// Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..". |
|
_, elem := filepath.Split(path) |
|
dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".." |
|
if dot || strings.HasPrefix(elem, "_") || elem == "testdata" { |
|
return filepath.SkipDir |
|
} |
|
|
|
name := prefix + filepath.ToSlash(path) |
|
if !match(name) { |
|
return nil |
|
} |
|
if _, err = build.ImportDir(path, 0); err != nil { |
|
if _, noGo := err.(*build.NoGoError); !noGo { |
|
log.Print(err) |
|
} |
|
return nil |
|
} |
|
pkgs = append(pkgs, name) |
|
return nil |
|
}) |
|
return pkgs |
|
}
|
|
|