git.sr.ht/~sircmpwn/gqlgen@v0.0.0-20200522192042-c84d29a1c940/internal/code/packages.go (about)

     1  package code
     2  
     3  import (
     4  	"bytes"
     5  	"path/filepath"
     6  
     7  	"github.com/pkg/errors"
     8  	"golang.org/x/tools/go/packages"
     9  )
    10  
    11  var mode = packages.NeedName |
    12  	packages.NeedFiles |
    13  	packages.NeedImports |
    14  	packages.NeedTypes |
    15  	packages.NeedSyntax |
    16  	packages.NeedTypesInfo
    17  
    18  // Packages is a wrapper around x/tools/go/packages that maintains a (hopefully prewarmed) cache of packages
    19  // that can be invalidated as writes are made and packages are known to change.
    20  type Packages struct {
    21  	packages     map[string]*packages.Package
    22  	importToName map[string]string
    23  	loadErrors   []error
    24  
    25  	numLoadCalls int // stupid test steam. ignore.
    26  	numNameCalls int // stupid test steam. ignore.
    27  }
    28  
    29  // LoadAll will call packages.Load and return the package data for the given packages,
    30  // but if the package already have been loaded it will return cached values instead.
    31  func (p *Packages) LoadAll(importPaths ...string) []*packages.Package {
    32  	if p.packages == nil {
    33  		p.packages = map[string]*packages.Package{}
    34  	}
    35  
    36  	missing := make([]string, 0, len(importPaths))
    37  	for _, path := range importPaths {
    38  		if _, ok := p.packages[path]; ok {
    39  			continue
    40  		}
    41  		missing = append(missing, path)
    42  	}
    43  
    44  	if len(missing) > 0 {
    45  		p.numLoadCalls++
    46  		pkgs, err := packages.Load(&packages.Config{Mode: mode}, missing...)
    47  		if err != nil {
    48  			p.loadErrors = append(p.loadErrors, err)
    49  		}
    50  
    51  		for _, pkg := range pkgs {
    52  			p.addToCache(pkg)
    53  		}
    54  	}
    55  
    56  	res := make([]*packages.Package, 0, len(importPaths))
    57  	for _, path := range importPaths {
    58  		res = append(res, p.packages[NormalizeVendor(path)])
    59  	}
    60  	return res
    61  }
    62  
    63  func (p *Packages) addToCache(pkg *packages.Package) {
    64  	imp := NormalizeVendor(pkg.PkgPath)
    65  	p.packages[imp] = pkg
    66  	for _, imp := range pkg.Imports {
    67  		if _, found := p.packages[NormalizeVendor(imp.PkgPath)]; !found {
    68  			p.addToCache(imp)
    69  		}
    70  	}
    71  }
    72  
    73  // Load works the same as LoadAll, except a single package at a time.
    74  func (p *Packages) Load(importPath string) *packages.Package {
    75  	pkgs := p.LoadAll(importPath)
    76  	if len(pkgs) == 0 {
    77  		return nil
    78  	}
    79  	return pkgs[0]
    80  }
    81  
    82  // LoadWithTypes tries a standard load, which may not have enough type info (TypesInfo== nil) available if the imported package is a
    83  // second order dependency. Fortunately this doesnt happen very often, so we can just issue a load when we detect it.
    84  func (p *Packages) LoadWithTypes(importPath string) *packages.Package {
    85  	pkg := p.Load(importPath)
    86  	if pkg == nil || pkg.TypesInfo == nil {
    87  		p.numLoadCalls++
    88  		pkgs, err := packages.Load(&packages.Config{Mode: mode}, importPath)
    89  		if err != nil {
    90  			p.loadErrors = append(p.loadErrors, err)
    91  			return nil
    92  		}
    93  		p.addToCache(pkgs[0])
    94  		pkg = pkgs[0]
    95  	}
    96  	return pkg
    97  }
    98  
    99  // NameForPackage looks up the package name from the package stanza in the go files at the given import path.
   100  func (p *Packages) NameForPackage(importPath string) string {
   101  	if importPath == "" {
   102  		panic(errors.New("import path can not be empty"))
   103  	}
   104  	if p.importToName == nil {
   105  		p.importToName = map[string]string{}
   106  	}
   107  
   108  	importPath = NormalizeVendor(importPath)
   109  
   110  	// if its in the name cache use it
   111  	if name := p.importToName[importPath]; name != "" {
   112  		return name
   113  	}
   114  
   115  	// otherwise we might have already loaded the full package data for it cached
   116  	pkg := p.packages[importPath]
   117  
   118  	if pkg == nil {
   119  		// otherwise do a name only lookup for it but dont put it in the package cache.
   120  		p.numNameCalls++
   121  		pkgs, err := packages.Load(&packages.Config{Mode: packages.NeedName}, importPath)
   122  		if err != nil {
   123  			p.loadErrors = append(p.loadErrors, err)
   124  		} else {
   125  			pkg = pkgs[0]
   126  		}
   127  	}
   128  
   129  	if pkg == nil || pkg.Name == "" {
   130  		return SanitizePackageName(filepath.Base(importPath))
   131  	}
   132  
   133  	p.importToName[importPath] = pkg.Name
   134  
   135  	return pkg.Name
   136  }
   137  
   138  // Evict removes a given package import path from the cache, along with any packages that depend on it. Further calls
   139  // to Load will fetch it from disk.
   140  func (p *Packages) Evict(importPath string) {
   141  	delete(p.packages, importPath)
   142  
   143  	for _, pkg := range p.packages {
   144  		for _, imported := range pkg.Imports {
   145  			if imported.PkgPath == importPath {
   146  				p.Evict(pkg.PkgPath)
   147  			}
   148  		}
   149  	}
   150  }
   151  
   152  // Errors returns any errors that were returned by Load, either from the call itself or any of the loaded packages.
   153  func (p *Packages) Errors() PkgErrors {
   154  	var res []error //nolint:prealloc
   155  	res = append(res, p.loadErrors...)
   156  	for _, pkg := range p.packages {
   157  		for _, err := range pkg.Errors {
   158  			res = append(res, err)
   159  		}
   160  	}
   161  	return res
   162  }
   163  
   164  type PkgErrors []error
   165  
   166  func (p PkgErrors) Error() string {
   167  	var b bytes.Buffer
   168  	b.WriteString("packages.Load: ")
   169  	for _, e := range p {
   170  		b.WriteString(e.Error() + "\n")
   171  	}
   172  	return b.String()
   173  }