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