mirror of https://github.com/go-gitea/gitea.git
294 lines
8.9 KiB
Go
294 lines
8.9 KiB
Go
package packages
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"go/parser"
|
|
"go/token"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// processGolistOverlay provides rudimentary support for adding
|
|
// files that don't exist on disk to an overlay. The results can be
|
|
// sometimes incorrect.
|
|
// TODO(matloob): Handle unsupported cases, including the following:
|
|
// - determining the correct package to add given a new import path
|
|
func processGolistOverlay(cfg *Config, response *responseDeduper, rootDirs func() *goInfo) (modifiedPkgs, needPkgs []string, err error) {
|
|
havePkgs := make(map[string]string) // importPath -> non-test package ID
|
|
needPkgsSet := make(map[string]bool)
|
|
modifiedPkgsSet := make(map[string]bool)
|
|
|
|
for _, pkg := range response.dr.Packages {
|
|
// This is an approximation of import path to id. This can be
|
|
// wrong for tests, vendored packages, and a number of other cases.
|
|
havePkgs[pkg.PkgPath] = pkg.ID
|
|
}
|
|
|
|
// If no new imports are added, it is safe to avoid loading any needPkgs.
|
|
// Otherwise, it's hard to tell which package is actually being loaded
|
|
// (due to vendoring) and whether any modified package will show up
|
|
// in the transitive set of dependencies (because new imports are added,
|
|
// potentially modifying the transitive set of dependencies).
|
|
var overlayAddsImports bool
|
|
|
|
for opath, contents := range cfg.Overlay {
|
|
base := filepath.Base(opath)
|
|
dir := filepath.Dir(opath)
|
|
var pkg *Package
|
|
var testVariantOf *Package // if opath is a test file, this is the package it is testing
|
|
var fileExists bool
|
|
isTest := strings.HasSuffix(opath, "_test.go")
|
|
pkgName, ok := extractPackageName(opath, contents)
|
|
if !ok {
|
|
// Don't bother adding a file that doesn't even have a parsable package statement
|
|
// to the overlay.
|
|
continue
|
|
}
|
|
nextPackage:
|
|
for _, p := range response.dr.Packages {
|
|
if pkgName != p.Name && p.ID != "command-line-arguments" {
|
|
continue
|
|
}
|
|
for _, f := range p.GoFiles {
|
|
if !sameFile(filepath.Dir(f), dir) {
|
|
continue
|
|
}
|
|
if isTest && !hasTestFiles(p) {
|
|
// TODO(matloob): Are there packages other than the 'production' variant
|
|
// of a package that this can match? This shouldn't match the test main package
|
|
// because the file is generated in another directory.
|
|
testVariantOf = p
|
|
continue nextPackage
|
|
}
|
|
pkg = p
|
|
if filepath.Base(f) == base {
|
|
fileExists = true
|
|
}
|
|
}
|
|
}
|
|
// The overlay could have included an entirely new package.
|
|
if pkg == nil {
|
|
// Try to find the module or gopath dir the file is contained in.
|
|
// Then for modules, add the module opath to the beginning.
|
|
var pkgPath string
|
|
for rdir, rpath := range rootDirs().rootDirs {
|
|
// TODO(matloob): This doesn't properly handle symlinks.
|
|
r, err := filepath.Rel(rdir, dir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
pkgPath = filepath.ToSlash(r)
|
|
if rpath != "" {
|
|
pkgPath = path.Join(rpath, pkgPath)
|
|
}
|
|
// We only create one new package even it can belong in multiple modules or GOPATH entries.
|
|
// This is okay because tools (such as the LSP) that use overlays will recompute the overlay
|
|
// once the file is saved, and golist will do the right thing.
|
|
// TODO(matloob): Implement module tiebreaking?
|
|
break
|
|
}
|
|
if pkgPath == "" {
|
|
continue
|
|
}
|
|
isXTest := strings.HasSuffix(pkgName, "_test")
|
|
if isXTest {
|
|
pkgPath += "_test"
|
|
}
|
|
id := pkgPath
|
|
if isTest && !isXTest {
|
|
id = fmt.Sprintf("%s [%s.test]", pkgPath, pkgPath)
|
|
}
|
|
// Try to reclaim a package with the same id if it exists in the response.
|
|
for _, p := range response.dr.Packages {
|
|
if reclaimPackage(p, id, opath, contents) {
|
|
pkg = p
|
|
break
|
|
}
|
|
}
|
|
// Otherwise, create a new package
|
|
if pkg == nil {
|
|
pkg = &Package{PkgPath: pkgPath, ID: id, Name: pkgName, Imports: make(map[string]*Package)}
|
|
response.addPackage(pkg)
|
|
havePkgs[pkg.PkgPath] = id
|
|
// Add the production package's sources for a test variant.
|
|
if isTest && !isXTest && testVariantOf != nil {
|
|
pkg.GoFiles = append(pkg.GoFiles, testVariantOf.GoFiles...)
|
|
pkg.CompiledGoFiles = append(pkg.CompiledGoFiles, testVariantOf.CompiledGoFiles...)
|
|
}
|
|
}
|
|
}
|
|
if !fileExists {
|
|
pkg.GoFiles = append(pkg.GoFiles, opath)
|
|
// TODO(matloob): Adding the file to CompiledGoFiles can exhibit the wrong behavior
|
|
// if the file will be ignored due to its build tags.
|
|
pkg.CompiledGoFiles = append(pkg.CompiledGoFiles, opath)
|
|
modifiedPkgsSet[pkg.ID] = true
|
|
}
|
|
imports, err := extractImports(opath, contents)
|
|
if err != nil {
|
|
// Let the parser or type checker report errors later.
|
|
continue
|
|
}
|
|
for _, imp := range imports {
|
|
_, found := pkg.Imports[imp]
|
|
if !found {
|
|
overlayAddsImports = true
|
|
// TODO(matloob): Handle cases when the following block isn't correct.
|
|
// These include imports of test variants, imports of vendored packages, etc.
|
|
id, ok := havePkgs[imp]
|
|
if !ok {
|
|
id = imp
|
|
}
|
|
pkg.Imports[imp] = &Package{ID: id}
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
// toPkgPath tries to guess the package path given the id.
|
|
// This isn't always correct -- it's certainly wrong for
|
|
// vendored packages' paths.
|
|
toPkgPath := func(id string) string {
|
|
// TODO(matloob): Handle vendor paths.
|
|
i := strings.IndexByte(id, ' ')
|
|
if i >= 0 {
|
|
return id[:i]
|
|
}
|
|
return id
|
|
}
|
|
|
|
// Do another pass now that new packages have been created to determine the
|
|
// set of missing packages.
|
|
for _, pkg := range response.dr.Packages {
|
|
for _, imp := range pkg.Imports {
|
|
pkgPath := toPkgPath(imp.ID)
|
|
if _, ok := havePkgs[pkgPath]; !ok {
|
|
needPkgsSet[pkgPath] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if overlayAddsImports {
|
|
needPkgs = make([]string, 0, len(needPkgsSet))
|
|
for pkg := range needPkgsSet {
|
|
needPkgs = append(needPkgs, pkg)
|
|
}
|
|
}
|
|
modifiedPkgs = make([]string, 0, len(modifiedPkgsSet))
|
|
for pkg := range modifiedPkgsSet {
|
|
modifiedPkgs = append(modifiedPkgs, pkg)
|
|
}
|
|
return modifiedPkgs, needPkgs, err
|
|
}
|
|
|
|
func hasTestFiles(p *Package) bool {
|
|
for _, f := range p.GoFiles {
|
|
if strings.HasSuffix(f, "_test.go") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// determineRootDirs returns a mapping from directories code can be contained in to the
|
|
// corresponding import path prefixes of those directories.
|
|
// Its result is used to try to determine the import path for a package containing
|
|
// an overlay file.
|
|
func determineRootDirs(cfg *Config) map[string]string {
|
|
// Assume modules first:
|
|
out, err := invokeGo(cfg, "list", "-m", "-json", "all")
|
|
if err != nil {
|
|
return determineRootDirsGOPATH(cfg)
|
|
}
|
|
m := map[string]string{}
|
|
type jsonMod struct{ Path, Dir string }
|
|
for dec := json.NewDecoder(out); dec.More(); {
|
|
mod := new(jsonMod)
|
|
if err := dec.Decode(mod); err != nil {
|
|
return m // Give up and return an empty map. Package won't be found for overlay.
|
|
}
|
|
if mod.Dir != "" && mod.Path != "" {
|
|
// This is a valid module; add it to the map.
|
|
m[mod.Dir] = mod.Path
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func determineRootDirsGOPATH(cfg *Config) map[string]string {
|
|
m := map[string]string{}
|
|
out, err := invokeGo(cfg, "env", "GOPATH")
|
|
if err != nil {
|
|
// Could not determine root dir mapping. Everything is best-effort, so just return an empty map.
|
|
// When we try to find the import path for a directory, there will be no root-dir match and
|
|
// we'll give up.
|
|
return m
|
|
}
|
|
for _, p := range filepath.SplitList(string(bytes.TrimSpace(out.Bytes()))) {
|
|
m[filepath.Join(p, "src")] = ""
|
|
}
|
|
return m
|
|
}
|
|
|
|
func extractImports(filename string, contents []byte) ([]string, error) {
|
|
f, err := parser.ParseFile(token.NewFileSet(), filename, contents, parser.ImportsOnly) // TODO(matloob): reuse fileset?
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var res []string
|
|
for _, imp := range f.Imports {
|
|
quotedPath := imp.Path.Value
|
|
path, err := strconv.Unquote(quotedPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res = append(res, path)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// reclaimPackage attempts to reuse a package that failed to load in an overlay.
|
|
//
|
|
// If the package has errors and has no Name, GoFiles, or Imports,
|
|
// then it's possible that it doesn't yet exist on disk.
|
|
func reclaimPackage(pkg *Package, id string, filename string, contents []byte) bool {
|
|
// TODO(rstambler): Check the message of the actual error?
|
|
// It differs between $GOPATH and module mode.
|
|
if pkg.ID != id {
|
|
return false
|
|
}
|
|
if len(pkg.Errors) != 1 {
|
|
return false
|
|
}
|
|
if pkg.Name != "" || pkg.ExportFile != "" {
|
|
return false
|
|
}
|
|
if len(pkg.GoFiles) > 0 || len(pkg.CompiledGoFiles) > 0 || len(pkg.OtherFiles) > 0 {
|
|
return false
|
|
}
|
|
if len(pkg.Imports) > 0 {
|
|
return false
|
|
}
|
|
pkgName, ok := extractPackageName(filename, contents)
|
|
if !ok {
|
|
return false
|
|
}
|
|
pkg.Name = pkgName
|
|
pkg.Errors = nil
|
|
return true
|
|
}
|
|
|
|
func extractPackageName(filename string, contents []byte) (string, bool) {
|
|
// TODO(rstambler): Check the message of the actual error?
|
|
// It differs between $GOPATH and module mode.
|
|
f, err := parser.ParseFile(token.NewFileSet(), filename, contents, parser.PackageClauseOnly) // TODO(matloob): reuse fileset?
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
return f.Name.Name, true
|
|
}
|