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