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.

resolve.go 12KB

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