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 }