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.

473 lines
15 KiB

  1. package packages
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "go/parser"
  6. "go/token"
  7. "log"
  8. "os"
  9. "path/filepath"
  10. "sort"
  11. "strconv"
  12. "strings"
  13. )
  14. // processGolistOverlay provides rudimentary support for adding
  15. // files that don't exist on disk to an overlay. The results can be
  16. // sometimes incorrect.
  17. // TODO(matloob): Handle unsupported cases, including the following:
  18. // - determining the correct package to add given a new import path
  19. func (state *golistState) processGolistOverlay(response *responseDeduper) (modifiedPkgs, needPkgs []string, err error) {
  20. havePkgs := make(map[string]string) // importPath -> non-test package ID
  21. needPkgsSet := make(map[string]bool)
  22. modifiedPkgsSet := make(map[string]bool)
  23. pkgOfDir := make(map[string][]*Package)
  24. for _, pkg := range response.dr.Packages {
  25. // This is an approximation of import path to id. This can be
  26. // wrong for tests, vendored packages, and a number of other cases.
  27. havePkgs[pkg.PkgPath] = pkg.ID
  28. x := commonDir(pkg.GoFiles)
  29. if x != "" {
  30. pkgOfDir[x] = append(pkgOfDir[x], pkg)
  31. }
  32. }
  33. // If no new imports are added, it is safe to avoid loading any needPkgs.
  34. // Otherwise, it's hard to tell which package is actually being loaded
  35. // (due to vendoring) and whether any modified package will show up
  36. // in the transitive set of dependencies (because new imports are added,
  37. // potentially modifying the transitive set of dependencies).
  38. var overlayAddsImports bool
  39. // If both a package and its test package are created by the overlay, we
  40. // need the real package first. Process all non-test files before test
  41. // files, and make the whole process deterministic while we're at it.
  42. var overlayFiles []string
  43. for opath := range state.cfg.Overlay {
  44. overlayFiles = append(overlayFiles, opath)
  45. }
  46. sort.Slice(overlayFiles, func(i, j int) bool {
  47. iTest := strings.HasSuffix(overlayFiles[i], "_test.go")
  48. jTest := strings.HasSuffix(overlayFiles[j], "_test.go")
  49. if iTest != jTest {
  50. return !iTest // non-tests are before tests.
  51. }
  52. return overlayFiles[i] < overlayFiles[j]
  53. })
  54. for _, opath := range overlayFiles {
  55. contents := state.cfg.Overlay[opath]
  56. base := filepath.Base(opath)
  57. dir := filepath.Dir(opath)
  58. var pkg *Package // if opath belongs to both a package and its test variant, this will be the test variant
  59. var testVariantOf *Package // if opath is a test file, this is the package it is testing
  60. var fileExists bool
  61. isTestFile := strings.HasSuffix(opath, "_test.go")
  62. pkgName, ok := extractPackageName(opath, contents)
  63. if !ok {
  64. // Don't bother adding a file that doesn't even have a parsable package statement
  65. // to the overlay.
  66. continue
  67. }
  68. // If all the overlay files belong to a different package, change the
  69. // package name to that package.
  70. maybeFixPackageName(pkgName, isTestFile, pkgOfDir[dir])
  71. nextPackage:
  72. for _, p := range response.dr.Packages {
  73. if pkgName != p.Name && p.ID != "command-line-arguments" {
  74. continue
  75. }
  76. for _, f := range p.GoFiles {
  77. if !sameFile(filepath.Dir(f), dir) {
  78. continue
  79. }
  80. // Make sure to capture information on the package's test variant, if needed.
  81. if isTestFile && !hasTestFiles(p) {
  82. // TODO(matloob): Are there packages other than the 'production' variant
  83. // of a package that this can match? This shouldn't match the test main package
  84. // because the file is generated in another directory.
  85. testVariantOf = p
  86. continue nextPackage
  87. }
  88. // We must have already seen the package of which this is a test variant.
  89. if pkg != nil && p != pkg && pkg.PkgPath == p.PkgPath {
  90. if hasTestFiles(p) {
  91. testVariantOf = pkg
  92. }
  93. }
  94. pkg = p
  95. if filepath.Base(f) == base {
  96. fileExists = true
  97. }
  98. }
  99. }
  100. // The overlay could have included an entirely new package or an
  101. // ad-hoc package. An ad-hoc package is one that we have manually
  102. // constructed from inadequate `go list` results for a file= query.
  103. // It will have the ID command-line-arguments.
  104. if pkg == nil || pkg.ID == "command-line-arguments" {
  105. // Try to find the module or gopath dir the file is contained in.
  106. // Then for modules, add the module opath to the beginning.
  107. pkgPath, ok, err := state.getPkgPath(dir)
  108. if err != nil {
  109. return nil, nil, err
  110. }
  111. if !ok {
  112. break
  113. }
  114. var forTest string // only set for x tests
  115. isXTest := strings.HasSuffix(pkgName, "_test")
  116. if isXTest {
  117. forTest = pkgPath
  118. pkgPath += "_test"
  119. }
  120. id := pkgPath
  121. if isTestFile {
  122. if isXTest {
  123. id = fmt.Sprintf("%s [%s.test]", pkgPath, forTest)
  124. } else {
  125. id = fmt.Sprintf("%s [%s.test]", pkgPath, pkgPath)
  126. }
  127. }
  128. if pkg != nil {
  129. // TODO(rstambler): We should change the package's path and ID
  130. // here. The only issue is that this messes with the roots.
  131. } else {
  132. // Try to reclaim a package with the same ID, if it exists in the response.
  133. for _, p := range response.dr.Packages {
  134. if reclaimPackage(p, id, opath, contents) {
  135. pkg = p
  136. break
  137. }
  138. }
  139. // Otherwise, create a new package.
  140. if pkg == nil {
  141. pkg = &Package{
  142. PkgPath: pkgPath,
  143. ID: id,
  144. Name: pkgName,
  145. Imports: make(map[string]*Package),
  146. }
  147. response.addPackage(pkg)
  148. havePkgs[pkg.PkgPath] = id
  149. // Add the production package's sources for a test variant.
  150. if isTestFile && !isXTest && testVariantOf != nil {
  151. pkg.GoFiles = append(pkg.GoFiles, testVariantOf.GoFiles...)
  152. pkg.CompiledGoFiles = append(pkg.CompiledGoFiles, testVariantOf.CompiledGoFiles...)
  153. // Add the package under test and its imports to the test variant.
  154. pkg.forTest = testVariantOf.PkgPath
  155. for k, v := range testVariantOf.Imports {
  156. pkg.Imports[k] = &Package{ID: v.ID}
  157. }
  158. }
  159. if isXTest {
  160. pkg.forTest = forTest
  161. }
  162. }
  163. }
  164. }
  165. if !fileExists {
  166. pkg.GoFiles = append(pkg.GoFiles, opath)
  167. // TODO(matloob): Adding the file to CompiledGoFiles can exhibit the wrong behavior
  168. // if the file will be ignored due to its build tags.
  169. pkg.CompiledGoFiles = append(pkg.CompiledGoFiles, opath)
  170. modifiedPkgsSet[pkg.ID] = true
  171. }
  172. imports, err := extractImports(opath, contents)
  173. if err != nil {
  174. // Let the parser or type checker report errors later.
  175. continue
  176. }
  177. for _, imp := range imports {
  178. // TODO(rstambler): If the package is an x test and the import has
  179. // a test variant, make sure to replace it.
  180. if _, found := pkg.Imports[imp]; found {
  181. continue
  182. }
  183. overlayAddsImports = true
  184. id, ok := havePkgs[imp]
  185. if !ok {
  186. var err error
  187. id, err = state.resolveImport(dir, imp)
  188. if err != nil {
  189. return nil, nil, err
  190. }
  191. }
  192. pkg.Imports[imp] = &Package{ID: id}
  193. // Add dependencies to the non-test variant version of this package as well.
  194. if testVariantOf != nil {
  195. testVariantOf.Imports[imp] = &Package{ID: id}
  196. }
  197. }
  198. }
  199. // toPkgPath guesses the package path given the id.
  200. toPkgPath := func(sourceDir, id string) (string, error) {
  201. if i := strings.IndexByte(id, ' '); i >= 0 {
  202. return state.resolveImport(sourceDir, id[:i])
  203. }
  204. return state.resolveImport(sourceDir, id)
  205. }
  206. // Now that new packages have been created, do another pass to determine
  207. // the new set of missing packages.
  208. for _, pkg := range response.dr.Packages {
  209. for _, imp := range pkg.Imports {
  210. if len(pkg.GoFiles) == 0 {
  211. return nil, nil, fmt.Errorf("cannot resolve imports for package %q with no Go files", pkg.PkgPath)
  212. }
  213. pkgPath, err := toPkgPath(filepath.Dir(pkg.GoFiles[0]), imp.ID)
  214. if err != nil {
  215. return nil, nil, err
  216. }
  217. if _, ok := havePkgs[pkgPath]; !ok {
  218. needPkgsSet[pkgPath] = true
  219. }
  220. }
  221. }
  222. if overlayAddsImports {
  223. needPkgs = make([]string, 0, len(needPkgsSet))
  224. for pkg := range needPkgsSet {
  225. needPkgs = append(needPkgs, pkg)
  226. }
  227. }
  228. modifiedPkgs = make([]string, 0, len(modifiedPkgsSet))
  229. for pkg := range modifiedPkgsSet {
  230. modifiedPkgs = append(modifiedPkgs, pkg)
  231. }
  232. return modifiedPkgs, needPkgs, err
  233. }
  234. // resolveImport finds the the ID of a package given its import path.
  235. // In particular, it will find the right vendored copy when in GOPATH mode.
  236. func (state *golistState) resolveImport(sourceDir, importPath string) (string, error) {
  237. env, err := state.getEnv()
  238. if err != nil {
  239. return "", err
  240. }
  241. if env["GOMOD"] != "" {
  242. return importPath, nil
  243. }
  244. searchDir := sourceDir
  245. for {
  246. vendorDir := filepath.Join(searchDir, "vendor")
  247. exists, ok := state.vendorDirs[vendorDir]
  248. if !ok {
  249. info, err := os.Stat(vendorDir)
  250. exists = err == nil && info.IsDir()
  251. state.vendorDirs[vendorDir] = exists
  252. }
  253. if exists {
  254. vendoredPath := filepath.Join(vendorDir, importPath)
  255. if info, err := os.Stat(vendoredPath); err == nil && info.IsDir() {
  256. // We should probably check for .go files here, but shame on anyone who fools us.
  257. path, ok, err := state.getPkgPath(vendoredPath)
  258. if err != nil {
  259. return "", err
  260. }
  261. if ok {
  262. return path, nil
  263. }
  264. }
  265. }
  266. // We know we've hit the top of the filesystem when we Dir / and get /,
  267. // or C:\ and get C:\, etc.
  268. next := filepath.Dir(searchDir)
  269. if next == searchDir {
  270. break
  271. }
  272. searchDir = next
  273. }
  274. return importPath, nil
  275. }
  276. func hasTestFiles(p *Package) bool {
  277. for _, f := range p.GoFiles {
  278. if strings.HasSuffix(f, "_test.go") {
  279. return true
  280. }
  281. }
  282. return false
  283. }
  284. // determineRootDirs returns a mapping from absolute directories that could
  285. // contain code to their corresponding import path prefixes.
  286. func (state *golistState) determineRootDirs() (map[string]string, error) {
  287. env, err := state.getEnv()
  288. if err != nil {
  289. return nil, err
  290. }
  291. if env["GOMOD"] != "" {
  292. state.rootsOnce.Do(func() {
  293. state.rootDirs, state.rootDirsError = state.determineRootDirsModules()
  294. })
  295. } else {
  296. state.rootsOnce.Do(func() {
  297. state.rootDirs, state.rootDirsError = state.determineRootDirsGOPATH()
  298. })
  299. }
  300. return state.rootDirs, state.rootDirsError
  301. }
  302. func (state *golistState) determineRootDirsModules() (map[string]string, error) {
  303. // This will only return the root directory for the main module.
  304. // For now we only support overlays in main modules.
  305. // Editing files in the module cache isn't a great idea, so we don't
  306. // plan to ever support that, but editing files in replaced modules
  307. // is something we may want to support. To do that, we'll want to
  308. // do a go list -m to determine the replaced module's module path and
  309. // directory, and then a go list -m {{with .Replace}}{{.Dir}}{{end}} <replaced module's path>
  310. // from the main module to determine if that module is actually a replacement.
  311. // See bcmills's comment here: https://github.com/golang/go/issues/37629#issuecomment-594179751
  312. // for more information.
  313. out, err := state.invokeGo("list", "-m", "-json")
  314. if err != nil {
  315. return nil, err
  316. }
  317. m := map[string]string{}
  318. type jsonMod struct{ Path, Dir string }
  319. for dec := json.NewDecoder(out); dec.More(); {
  320. mod := new(jsonMod)
  321. if err := dec.Decode(mod); err != nil {
  322. return nil, err
  323. }
  324. if mod.Dir != "" && mod.Path != "" {
  325. // This is a valid module; add it to the map.
  326. absDir, err := filepath.Abs(mod.Dir)
  327. if err != nil {
  328. return nil, err
  329. }
  330. m[absDir] = mod.Path
  331. }
  332. }
  333. return m, nil
  334. }
  335. func (state *golistState) determineRootDirsGOPATH() (map[string]string, error) {
  336. m := map[string]string{}
  337. for _, dir := range filepath.SplitList(state.mustGetEnv()["GOPATH"]) {
  338. absDir, err := filepath.Abs(dir)
  339. if err != nil {
  340. return nil, err
  341. }
  342. m[filepath.Join(absDir, "src")] = ""
  343. }
  344. return m, nil
  345. }
  346. func extractImports(filename string, contents []byte) ([]string, error) {
  347. f, err := parser.ParseFile(token.NewFileSet(), filename, contents, parser.ImportsOnly) // TODO(matloob): reuse fileset?
  348. if err != nil {
  349. return nil, err
  350. }
  351. var res []string
  352. for _, imp := range f.Imports {
  353. quotedPath := imp.Path.Value
  354. path, err := strconv.Unquote(quotedPath)
  355. if err != nil {
  356. return nil, err
  357. }
  358. res = append(res, path)
  359. }
  360. return res, nil
  361. }
  362. // reclaimPackage attempts to reuse a package that failed to load in an overlay.
  363. //
  364. // If the package has errors and has no Name, GoFiles, or Imports,
  365. // then it's possible that it doesn't yet exist on disk.
  366. func reclaimPackage(pkg *Package, id string, filename string, contents []byte) bool {
  367. // TODO(rstambler): Check the message of the actual error?
  368. // It differs between $GOPATH and module mode.
  369. if pkg.ID != id {
  370. return false
  371. }
  372. if len(pkg.Errors) != 1 {
  373. return false
  374. }
  375. if pkg.Name != "" || pkg.ExportFile != "" {
  376. return false
  377. }
  378. if len(pkg.GoFiles) > 0 || len(pkg.CompiledGoFiles) > 0 || len(pkg.OtherFiles) > 0 {
  379. return false
  380. }
  381. if len(pkg.Imports) > 0 {
  382. return false
  383. }
  384. pkgName, ok := extractPackageName(filename, contents)
  385. if !ok {
  386. return false
  387. }
  388. pkg.Name = pkgName
  389. pkg.Errors = nil
  390. return true
  391. }
  392. func extractPackageName(filename string, contents []byte) (string, bool) {
  393. // TODO(rstambler): Check the message of the actual error?
  394. // It differs between $GOPATH and module mode.
  395. f, err := parser.ParseFile(token.NewFileSet(), filename, contents, parser.PackageClauseOnly) // TODO(matloob): reuse fileset?
  396. if err != nil {
  397. return "", false
  398. }
  399. return f.Name.Name, true
  400. }
  401. func commonDir(a []string) string {
  402. seen := make(map[string]bool)
  403. x := append([]string{}, a...)
  404. for _, f := range x {
  405. seen[filepath.Dir(f)] = true
  406. }
  407. if len(seen) > 1 {
  408. log.Fatalf("commonDir saw %v for %v", seen, x)
  409. }
  410. for k := range seen {
  411. // len(seen) == 1
  412. return k
  413. }
  414. return "" // no files
  415. }
  416. // It is possible that the files in the disk directory dir have a different package
  417. // name from newName, which is deduced from the overlays. If they all have a different
  418. // package name, and they all have the same package name, then that name becomes
  419. // the package name.
  420. // It returns true if it changes the package name, false otherwise.
  421. func maybeFixPackageName(newName string, isTestFile bool, pkgsOfDir []*Package) {
  422. names := make(map[string]int)
  423. for _, p := range pkgsOfDir {
  424. names[p.Name]++
  425. }
  426. if len(names) != 1 {
  427. // some files are in different packages
  428. return
  429. }
  430. var oldName string
  431. for k := range names {
  432. oldName = k
  433. }
  434. if newName == oldName {
  435. return
  436. }
  437. // We might have a case where all of the package names in the directory are
  438. // the same, but the overlay file is for an x test, which belongs to its
  439. // own package. If the x test does not yet exist on disk, we may not yet
  440. // have its package name on disk, but we should not rename the packages.
  441. //
  442. // We use a heuristic to determine if this file belongs to an x test:
  443. // The test file should have a package name whose package name has a _test
  444. // suffix or looks like "newName_test".
  445. maybeXTest := strings.HasPrefix(oldName+"_test", newName) || strings.HasSuffix(newName, "_test")
  446. if isTestFile && maybeXTest {
  447. return
  448. }
  449. for _, p := range pkgsOfDir {
  450. p.Name = newName
  451. }
  452. }