github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/go/loader/loader.go (about) 1 package loader 2 3 import ( 4 "errors" 5 "fmt" 6 "go/ast" 7 "go/parser" 8 "go/scanner" 9 "go/token" 10 "go/types" 11 "os" 12 "time" 13 14 "github.com/amarpal/go-tools/config" 15 "github.com/amarpal/go-tools/lintcmd/cache" 16 17 "golang.org/x/tools/go/gcexportdata" 18 "golang.org/x/tools/go/packages" 19 ) 20 21 const MaxFileSize = 50 * 1024 * 1024 // 50 MB 22 23 var errMaxFileSize = errors.New("file exceeds max file size") 24 25 type PackageSpec struct { 26 ID string 27 Name string 28 PkgPath string 29 // Errors that occurred while building the import graph. These will 30 // primarily be parse errors or failure to resolve imports, but 31 // may also be other errors. 32 Errors []packages.Error 33 GoFiles []string 34 CompiledGoFiles []string 35 OtherFiles []string 36 ExportFile string 37 Imports map[string]*PackageSpec 38 TypesSizes types.Sizes 39 Hash cache.ActionID 40 Module *packages.Module 41 42 Config config.Config 43 } 44 45 func (spec *PackageSpec) String() string { 46 return spec.ID 47 } 48 49 type Package struct { 50 *PackageSpec 51 52 // Errors that occurred while loading the package. These will 53 // primarily be parse or type errors, but may also be lower-level 54 // failures such as file-system ones. 55 Errors []packages.Error 56 Types *types.Package 57 Fset *token.FileSet 58 Syntax []*ast.File 59 TypesInfo *types.Info 60 } 61 62 // Graph resolves patterns and returns packages with all the 63 // information required to later load type information, and optionally 64 // syntax trees. 65 // 66 // The provided config can set any setting with the exception of Mode. 67 func Graph(c *cache.Cache, cfg *packages.Config, patterns ...string) ([]*PackageSpec, error) { 68 var dcfg packages.Config 69 if cfg != nil { 70 dcfg = *cfg 71 } 72 dcfg.Mode = packages.NeedName | 73 packages.NeedImports | 74 packages.NeedDeps | 75 packages.NeedExportFile | 76 packages.NeedFiles | 77 packages.NeedCompiledGoFiles | 78 packages.NeedTypesSizes | 79 packages.NeedModule 80 pkgs, err := packages.Load(&dcfg, patterns...) 81 if err != nil { 82 return nil, err 83 } 84 85 m := map[*packages.Package]*PackageSpec{} 86 packages.Visit(pkgs, nil, func(pkg *packages.Package) { 87 spec := &PackageSpec{ 88 ID: pkg.ID, 89 Name: pkg.Name, 90 PkgPath: pkg.PkgPath, 91 Errors: pkg.Errors, 92 GoFiles: pkg.GoFiles, 93 CompiledGoFiles: pkg.CompiledGoFiles, 94 OtherFiles: pkg.OtherFiles, 95 ExportFile: pkg.ExportFile, 96 Imports: map[string]*PackageSpec{}, 97 TypesSizes: pkg.TypesSizes, 98 Module: pkg.Module, 99 } 100 for path, imp := range pkg.Imports { 101 spec.Imports[path] = m[imp] 102 } 103 if cdir := config.Dir(pkg.GoFiles); cdir != "" { 104 cfg, err := config.Load(cdir) 105 if err != nil { 106 spec.Errors = append(spec.Errors, convertError(err)...) 107 } 108 spec.Config = cfg 109 } else { 110 spec.Config = config.DefaultConfig 111 } 112 spec.Hash, err = computeHash(c, spec) 113 if err != nil { 114 spec.Errors = append(spec.Errors, convertError(err)...) 115 } 116 m[pkg] = spec 117 }) 118 out := make([]*PackageSpec, 0, len(pkgs)) 119 for _, pkg := range pkgs { 120 if len(pkg.CompiledGoFiles) == 0 && len(pkg.Errors) == 0 && pkg.PkgPath != "unsafe" { 121 // If a package consists only of test files, then 122 // go/packages incorrectly(?) returns an empty package for 123 // the non-test variant. Get rid of those packages. See 124 // #646. 125 // 126 // Do not, however, skip packages that have errors. Those, 127 // too, may have no files, but we want to print the 128 // errors. 129 continue 130 } 131 out = append(out, m[pkg]) 132 } 133 134 return out, nil 135 } 136 137 type program struct { 138 fset *token.FileSet 139 packages map[string]*types.Package 140 } 141 142 type Stats struct { 143 Source time.Duration 144 Export map[*PackageSpec]time.Duration 145 } 146 147 // Load loads the package described in spec. Imports will be loaded 148 // from export data, while the package itself will be loaded from 149 // source. 150 // 151 // An error will only be returned for system failures, such as failure 152 // to read export data from disk. Syntax and type errors, among 153 // others, will only populate the returned package's Errors field. 154 func Load(spec *PackageSpec) (*Package, Stats, error) { 155 prog := &program{ 156 fset: token.NewFileSet(), 157 packages: map[string]*types.Package{}, 158 } 159 160 stats := Stats{ 161 Export: map[*PackageSpec]time.Duration{}, 162 } 163 for _, imp := range spec.Imports { 164 if imp.PkgPath == "unsafe" { 165 continue 166 } 167 t := time.Now() 168 _, err := prog.loadFromExport(imp) 169 stats.Export[imp] = time.Since(t) 170 if err != nil { 171 return nil, stats, err 172 } 173 } 174 t := time.Now() 175 pkg, err := prog.loadFromSource(spec) 176 if err == errMaxFileSize { 177 pkg, err = prog.loadFromExport(spec) 178 } 179 stats.Source = time.Since(t) 180 return pkg, stats, err 181 } 182 183 // loadFromExport loads a package from export data. 184 func (prog *program) loadFromExport(spec *PackageSpec) (*Package, error) { 185 // log.Printf("Loading package %s from export", spec) 186 if spec.ExportFile == "" { 187 return nil, fmt.Errorf("no export data for %q", spec.ID) 188 } 189 f, err := os.Open(spec.ExportFile) 190 if err != nil { 191 return nil, err 192 } 193 defer f.Close() 194 195 r, err := gcexportdata.NewReader(f) 196 if err != nil { 197 return nil, err 198 } 199 tpkg, err := gcexportdata.Read(r, prog.fset, prog.packages, spec.PkgPath) 200 if err != nil { 201 return nil, err 202 } 203 pkg := &Package{ 204 PackageSpec: spec, 205 Types: tpkg, 206 Fset: prog.fset, 207 } 208 // runtime.SetFinalizer(pkg, func(pkg *Package) { 209 // log.Println("Unloading package", pkg.PkgPath) 210 // }) 211 return pkg, nil 212 } 213 214 // loadFromSource loads a package from source. All of its dependencies 215 // must have been loaded already. 216 func (prog *program) loadFromSource(spec *PackageSpec) (*Package, error) { 217 if len(spec.Errors) > 0 { 218 panic("LoadFromSource called on package with errors") 219 } 220 221 pkg := &Package{ 222 PackageSpec: spec, 223 Types: types.NewPackage(spec.PkgPath, spec.Name), 224 Syntax: make([]*ast.File, len(spec.CompiledGoFiles)), 225 Fset: prog.fset, 226 TypesInfo: &types.Info{ 227 Types: make(map[ast.Expr]types.TypeAndValue), 228 Defs: make(map[*ast.Ident]types.Object), 229 Uses: make(map[*ast.Ident]types.Object), 230 Implicits: make(map[ast.Node]types.Object), 231 Scopes: make(map[ast.Node]*types.Scope), 232 Selections: make(map[*ast.SelectorExpr]*types.Selection), 233 Instances: map[*ast.Ident]types.Instance{}, 234 }, 235 } 236 // runtime.SetFinalizer(pkg, func(pkg *Package) { 237 // log.Println("Unloading package", pkg.PkgPath) 238 // }) 239 240 // OPT(dh): many packages have few files, much fewer than there 241 // are CPU cores. Additionally, parsing each individual file is 242 // very fast. A naive parallel implementation of this loop won't 243 // be faster, and tends to be slower due to extra scheduling, 244 // bookkeeping and potentially false sharing of cache lines. 245 for i, file := range spec.CompiledGoFiles { 246 f, err := os.Open(file) 247 if err != nil { 248 return nil, err 249 } 250 fi, err := f.Stat() 251 if err != nil { 252 return nil, err 253 } 254 if fi.Size() >= MaxFileSize { 255 return nil, errMaxFileSize 256 } 257 af, err := parser.ParseFile(prog.fset, file, f, parser.ParseComments|parser.SkipObjectResolution) 258 f.Close() 259 if err != nil { 260 pkg.Errors = append(pkg.Errors, convertError(err)...) 261 return pkg, nil 262 } 263 pkg.Syntax[i] = af 264 } 265 importer := func(path string) (*types.Package, error) { 266 if path == "unsafe" { 267 return types.Unsafe, nil 268 } 269 if path == "C" { 270 // go/packages doesn't tell us that cgo preprocessing 271 // failed. When we subsequently try to parse the package, 272 // we'll encounter the raw C import. 273 return nil, errors.New("cgo preprocessing failed") 274 } 275 ispecpkg := spec.Imports[path] 276 if ispecpkg == nil { 277 return nil, fmt.Errorf("trying to import %q in the context of %q returned nil PackageSpec", path, spec) 278 } 279 ipkg := prog.packages[ispecpkg.PkgPath] 280 if ipkg == nil { 281 return nil, fmt.Errorf("trying to import %q (%q) in the context of %q returned nil PackageSpec", ispecpkg.PkgPath, path, spec) 282 } 283 return ipkg, nil 284 } 285 tc := &types.Config{ 286 Importer: importerFunc(importer), 287 Error: func(err error) { 288 pkg.Errors = append(pkg.Errors, convertError(err)...) 289 }, 290 } 291 if spec.Module != nil && spec.Module.GoVersion != "" { 292 tc.GoVersion = "go" + spec.Module.GoVersion 293 } 294 types.NewChecker(tc, pkg.Fset, pkg.Types, pkg.TypesInfo).Files(pkg.Syntax) 295 return pkg, nil 296 } 297 298 func convertError(err error) []packages.Error { 299 var errs []packages.Error 300 // taken from go/packages 301 switch err := err.(type) { 302 case packages.Error: 303 // from driver 304 errs = append(errs, err) 305 306 case *os.PathError: 307 // from parser 308 errs = append(errs, packages.Error{ 309 Pos: err.Path + ":1", 310 Msg: err.Err.Error(), 311 Kind: packages.ParseError, 312 }) 313 314 case scanner.ErrorList: 315 // from parser 316 for _, err := range err { 317 errs = append(errs, packages.Error{ 318 Pos: err.Pos.String(), 319 Msg: err.Msg, 320 Kind: packages.ParseError, 321 }) 322 } 323 324 case types.Error: 325 // from type checker 326 errs = append(errs, packages.Error{ 327 Pos: err.Fset.Position(err.Pos).String(), 328 Msg: err.Msg, 329 Kind: packages.TypeError, 330 }) 331 332 case config.ParseError: 333 errs = append(errs, packages.Error{ 334 Pos: fmt.Sprintf("%s:%d", err.Filename, err.Position.Line), 335 Msg: fmt.Sprintf("%s (last key parsed: %q)", err.Message, err.LastKey), 336 Kind: packages.ParseError, 337 }) 338 default: 339 errs = append(errs, packages.Error{ 340 Pos: "-", 341 Msg: err.Error(), 342 Kind: packages.UnknownError, 343 }) 344 } 345 return errs 346 } 347 348 type importerFunc func(path string) (*types.Package, error) 349 350 func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) }