github.com/spread-ai/gqlgen@v0.0.0-20221124102857-a6c8ef538a1d/internal/code/packages.go (about)

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