github.com/jd-ly/tools@v0.5.7/internal/lsp/cache/load.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package cache 6 7 import ( 8 "context" 9 "fmt" 10 "go/types" 11 "io/ioutil" 12 "os" 13 "path/filepath" 14 "sort" 15 "strings" 16 "time" 17 18 "github.com/jd-ly/tools/go/packages" 19 "github.com/jd-ly/tools/internal/event" 20 "github.com/jd-ly/tools/internal/gocommand" 21 "github.com/jd-ly/tools/internal/lsp/debug/tag" 22 "github.com/jd-ly/tools/internal/lsp/protocol" 23 "github.com/jd-ly/tools/internal/lsp/source" 24 "github.com/jd-ly/tools/internal/memoize" 25 "github.com/jd-ly/tools/internal/packagesinternal" 26 "github.com/jd-ly/tools/internal/span" 27 errors "golang.org/x/xerrors" 28 ) 29 30 // metadata holds package metadata extracted from a call to packages.Load. 31 type metadata struct { 32 id packageID 33 pkgPath packagePath 34 name packageName 35 goFiles []span.URI 36 compiledGoFiles []span.URI 37 forTest packagePath 38 typesSizes types.Sizes 39 errors []packages.Error 40 deps []packageID 41 missingDeps map[packagePath]struct{} 42 module *packages.Module 43 44 // config is the *packages.Config associated with the loaded package. 45 config *packages.Config 46 } 47 48 // load calls packages.Load for the given scopes, updating package metadata, 49 // import graph, and mapped files with the result. 50 func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interface{}) error { 51 var query []string 52 var containsDir bool // for logging 53 for _, scope := range scopes { 54 switch scope := scope.(type) { 55 case packagePath: 56 if scope == "command-line-arguments" { 57 panic("attempted to load command-line-arguments") 58 } 59 // The only time we pass package paths is when we're doing a 60 // partial workspace load. In those cases, the paths came back from 61 // go list and should already be GOPATH-vendorized when appropriate. 62 query = append(query, string(scope)) 63 case fileURI: 64 uri := span.URI(scope) 65 // Don't try to load a file that doesn't exist. 66 fh := s.FindFile(uri) 67 if fh == nil || fh.Kind() != source.Go { 68 continue 69 } 70 query = append(query, fmt.Sprintf("file=%s", uri.Filename())) 71 case moduleLoadScope: 72 query = append(query, fmt.Sprintf("%s/...", scope)) 73 case viewLoadScope: 74 // If we are outside of GOPATH, a module, or some other known 75 // build system, don't load subdirectories. 76 if !s.ValidBuildConfiguration() { 77 query = append(query, "./") 78 } else { 79 query = append(query, "./...") 80 } 81 default: 82 panic(fmt.Sprintf("unknown scope type %T", scope)) 83 } 84 switch scope.(type) { 85 case viewLoadScope, moduleLoadScope: 86 containsDir = true 87 } 88 } 89 if len(query) == 0 { 90 return nil 91 } 92 sort.Strings(query) // for determinism 93 94 ctx, done := event.Start(ctx, "cache.view.load", tag.Query.Of(query)) 95 defer done() 96 97 flags := source.LoadWorkspace 98 if allowNetwork { 99 flags |= source.AllowNetwork 100 } 101 _, inv, cleanup, err := s.goCommandInvocation(ctx, flags, &gocommand.Invocation{ 102 WorkingDir: s.view.rootURI.Filename(), 103 }) 104 if err != nil { 105 return err 106 } 107 108 // Set a last resort deadline on packages.Load since it calls the go 109 // command, which may hang indefinitely if it has a bug. golang/go#42132 110 // and golang/go#42255 have more context. 111 ctx, cancel := context.WithTimeout(ctx, 15*time.Minute) 112 defer cancel() 113 114 cfg := s.config(ctx, inv) 115 pkgs, err := packages.Load(cfg, query...) 116 cleanup() 117 118 // If the context was canceled, return early. Otherwise, we might be 119 // type-checking an incomplete result. Check the context directly, 120 // because go/packages adds extra information to the error. 121 if ctx.Err() != nil { 122 return ctx.Err() 123 } 124 if err != nil { 125 event.Error(ctx, "go/packages.Load", err, tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs))) 126 } else { 127 event.Log(ctx, "go/packages.Load", tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs))) 128 } 129 if len(pkgs) == 0 { 130 if err != nil { 131 // Try to extract the load error into a structured error with 132 // diagnostics. 133 if criticalErr := s.parseLoadError(ctx, err); criticalErr != nil { 134 return criticalErr 135 } 136 } else { 137 err = fmt.Errorf("no packages returned") 138 } 139 return errors.Errorf("%v: %w", err, source.PackagesLoadError) 140 } 141 for _, pkg := range pkgs { 142 if !containsDir || s.view.Options().VerboseOutput { 143 event.Log(ctx, "go/packages.Load", tag.Snapshot.Of(s.ID()), tag.PackagePath.Of(pkg.PkgPath), tag.Files.Of(pkg.CompiledGoFiles)) 144 } 145 // Ignore packages with no sources, since we will never be able to 146 // correctly invalidate that metadata. 147 if len(pkg.GoFiles) == 0 && len(pkg.CompiledGoFiles) == 0 { 148 continue 149 } 150 // Special case for the builtin package, as it has no dependencies. 151 if pkg.PkgPath == "builtin" { 152 if err := s.buildBuiltinPackage(ctx, pkg.GoFiles); err != nil { 153 return err 154 } 155 continue 156 } 157 // Skip test main packages. 158 if isTestMain(pkg, s.view.gocache) { 159 continue 160 } 161 // Set the metadata for this package. 162 m, err := s.setMetadata(ctx, packagePath(pkg.PkgPath), pkg, cfg, map[packageID]struct{}{}) 163 if err != nil { 164 return err 165 } 166 if _, err := s.buildPackageHandle(ctx, m.id, s.workspaceParseMode(m.id)); err != nil { 167 return err 168 } 169 } 170 // Rebuild the import graph when the metadata is updated. 171 s.clearAndRebuildImportGraph() 172 173 return nil 174 } 175 176 func (s *snapshot) parseLoadError(ctx context.Context, loadErr error) *source.CriticalError { 177 // The error may be a result of the user's workspace layout. Check for 178 // a valid workspace configuration first. 179 if criticalErr := s.workspaceLayoutErrors(ctx, loadErr); criticalErr != nil { 180 return criticalErr 181 } 182 criticalErr := &source.CriticalError{ 183 MainError: loadErr, 184 } 185 // Attempt to place diagnostics in the relevant go.mod files, if any. 186 for _, uri := range s.ModFiles() { 187 fh, err := s.GetFile(ctx, uri) 188 if err != nil { 189 continue 190 } 191 srcErr := s.extractGoCommandError(ctx, s, fh, loadErr.Error()) 192 if srcErr == nil { 193 continue 194 } 195 criticalErr.ErrorList = append(criticalErr.ErrorList, srcErr) 196 } 197 return criticalErr 198 } 199 200 // workspaceLayoutErrors returns a diagnostic for every open file, as well as 201 // an error message if there are no open files. 202 func (s *snapshot) workspaceLayoutErrors(ctx context.Context, err error) *source.CriticalError { 203 // Assume the workspace is misconfigured only if we've detected an invalid 204 // build configuration. Currently, a valid build configuration is either a 205 // module at the root of the view or a GOPATH workspace. 206 if s.ValidBuildConfiguration() { 207 return nil 208 } 209 if len(s.workspace.getKnownModFiles()) == 0 { 210 return nil 211 } 212 // TODO(rstambler): Handle GO111MODULE=auto. 213 if s.view.userGo111Module != on { 214 return nil 215 } 216 if s.workspace.moduleSource != legacyWorkspace { 217 return nil 218 } 219 // The user's workspace contains go.mod files and they have 220 // GO111MODULE=on, so we should guide them to create a 221 // workspace folder for each module. 222 223 // Add a diagnostic to every open file, or return a general error if 224 // there aren't any. 225 var open []source.VersionedFileHandle 226 s.mu.Lock() 227 for _, fh := range s.files { 228 if s.isOpenLocked(fh.URI()) { 229 open = append(open, fh) 230 } 231 } 232 s.mu.Unlock() 233 234 msg := `gopls requires a module at the root of your workspace. 235 You can work with multiple modules by opening each one as a workspace folder. 236 Improvements to this workflow will be coming soon (https://github.com/golang/go/issues/32394), 237 and you can learn more here: https://github.com/golang/go/issues/36899.` 238 239 criticalError := &source.CriticalError{ 240 MainError: errors.New(msg), 241 } 242 if len(open) == 0 { 243 return criticalError 244 } 245 for _, fh := range open { 246 // Place the diagnostics on the package or module declarations. 247 var rng protocol.Range 248 switch fh.Kind() { 249 case source.Go: 250 if pgf, err := s.ParseGo(ctx, fh, source.ParseHeader); err == nil { 251 pkgDecl := span.NewRange(s.FileSet(), pgf.File.Package, pgf.File.Name.End()) 252 if spn, err := pkgDecl.Span(); err == nil { 253 rng, _ = pgf.Mapper.Range(spn) 254 } 255 } 256 case source.Mod: 257 if pmf, err := s.ParseMod(ctx, fh); err == nil { 258 if pmf.File.Module != nil && pmf.File.Module.Syntax != nil { 259 rng, _ = rangeFromPositions(pmf.Mapper, pmf.File.Module.Syntax.Start, pmf.File.Module.Syntax.End) 260 } 261 } 262 } 263 criticalError.ErrorList = append(criticalError.ErrorList, &source.Error{ 264 URI: fh.URI(), 265 Range: rng, 266 Kind: source.ListError, 267 Message: msg, 268 }) 269 } 270 return criticalError 271 } 272 273 type workspaceDirKey string 274 275 type workspaceDirData struct { 276 dir string 277 err error 278 } 279 280 // getWorkspaceDir gets the URI for the workspace directory associated with 281 // this snapshot. The workspace directory is a temp directory containing the 282 // go.mod file computed from all active modules. 283 func (s *snapshot) getWorkspaceDir(ctx context.Context) (span.URI, error) { 284 s.mu.Lock() 285 h := s.workspaceDirHandle 286 s.mu.Unlock() 287 if h != nil { 288 return getWorkspaceDir(ctx, h, s.generation) 289 } 290 file, err := s.workspace.modFile(ctx, s) 291 if err != nil { 292 return "", err 293 } 294 content, err := file.Format() 295 if err != nil { 296 return "", err 297 } 298 key := workspaceDirKey(hashContents(content)) 299 s.mu.Lock() 300 h = s.generation.Bind(key, func(context.Context, memoize.Arg) interface{} { 301 tmpdir, err := ioutil.TempDir("", "gopls-workspace-mod") 302 if err != nil { 303 return &workspaceDirData{err: err} 304 } 305 filename := filepath.Join(tmpdir, "go.mod") 306 if err := ioutil.WriteFile(filename, content, 0644); err != nil { 307 os.RemoveAll(tmpdir) 308 return &workspaceDirData{err: err} 309 } 310 return &workspaceDirData{dir: tmpdir} 311 }, func(v interface{}) { 312 d := v.(*workspaceDirData) 313 if d.dir != "" { 314 if err := os.RemoveAll(d.dir); err != nil { 315 event.Error(context.Background(), "cleaning workspace dir", err) 316 } 317 } 318 }) 319 s.workspaceDirHandle = h 320 s.mu.Unlock() 321 return getWorkspaceDir(ctx, h, s.generation) 322 } 323 324 func getWorkspaceDir(ctx context.Context, h *memoize.Handle, g *memoize.Generation) (span.URI, error) { 325 v, err := h.Get(ctx, g, nil) 326 if err != nil { 327 return "", err 328 } 329 return span.URIFromPath(v.(*workspaceDirData).dir), nil 330 } 331 332 // setMetadata extracts metadata from pkg and records it in s. It 333 // recurses through pkg.Imports to ensure that metadata exists for all 334 // dependencies. 335 func (s *snapshot) setMetadata(ctx context.Context, pkgPath packagePath, pkg *packages.Package, cfg *packages.Config, seen map[packageID]struct{}) (*metadata, error) { 336 id := packageID(pkg.ID) 337 if _, ok := seen[id]; ok { 338 return nil, errors.Errorf("import cycle detected: %q", id) 339 } 340 // Recreate the metadata rather than reusing it to avoid locking. 341 m := &metadata{ 342 id: id, 343 pkgPath: pkgPath, 344 name: packageName(pkg.Name), 345 forTest: packagePath(packagesinternal.GetForTest(pkg)), 346 typesSizes: pkg.TypesSizes, 347 errors: pkg.Errors, 348 config: cfg, 349 module: pkg.Module, 350 } 351 352 for _, filename := range pkg.CompiledGoFiles { 353 uri := span.URIFromPath(filename) 354 m.compiledGoFiles = append(m.compiledGoFiles, uri) 355 s.addID(uri, m.id) 356 } 357 for _, filename := range pkg.GoFiles { 358 uri := span.URIFromPath(filename) 359 m.goFiles = append(m.goFiles, uri) 360 s.addID(uri, m.id) 361 } 362 363 // TODO(rstambler): is this still necessary? 364 copied := map[packageID]struct{}{ 365 id: {}, 366 } 367 for k, v := range seen { 368 copied[k] = v 369 } 370 for importPath, importPkg := range pkg.Imports { 371 importPkgPath := packagePath(importPath) 372 importID := packageID(importPkg.ID) 373 374 m.deps = append(m.deps, importID) 375 376 // Don't remember any imports with significant errors. 377 if importPkgPath != "unsafe" && len(importPkg.CompiledGoFiles) == 0 { 378 if m.missingDeps == nil { 379 m.missingDeps = make(map[packagePath]struct{}) 380 } 381 m.missingDeps[importPkgPath] = struct{}{} 382 continue 383 } 384 if s.getMetadata(importID) == nil { 385 if _, err := s.setMetadata(ctx, importPkgPath, importPkg, cfg, copied); err != nil { 386 event.Error(ctx, "error in dependency", err) 387 } 388 } 389 } 390 391 // Add the metadata to the cache. 392 s.mu.Lock() 393 defer s.mu.Unlock() 394 395 // TODO: We should make sure not to set duplicate metadata, 396 // and instead panic here. This can be done by making sure not to 397 // reset metadata information for packages we've already seen. 398 if original, ok := s.metadata[m.id]; ok { 399 m = original 400 } else { 401 s.metadata[m.id] = m 402 } 403 404 // Set the workspace packages. If any of the package's files belong to the 405 // view, then the package may be a workspace package. 406 for _, uri := range append(m.compiledGoFiles, m.goFiles...) { 407 if !s.view.contains(uri) { 408 continue 409 } 410 411 // The package's files are in this view. It may be a workspace package. 412 if strings.Contains(string(uri), "/vendor/") { 413 // Vendored packages are not likely to be interesting to the user. 414 continue 415 } 416 417 switch { 418 case m.forTest == "": 419 // A normal package. 420 s.workspacePackages[m.id] = pkgPath 421 case m.forTest == m.pkgPath, m.forTest+"_test" == m.pkgPath: 422 // The test variant of some workspace package or its x_test. 423 // To load it, we need to load the non-test variant with -test. 424 s.workspacePackages[m.id] = m.forTest 425 default: 426 // A test variant of some intermediate package. We don't care about it. 427 } 428 } 429 return m, nil 430 } 431 432 func isTestMain(pkg *packages.Package, gocache string) bool { 433 // Test mains must have an import path that ends with ".test". 434 if !strings.HasSuffix(pkg.PkgPath, ".test") { 435 return false 436 } 437 // Test main packages are always named "main". 438 if pkg.Name != "main" { 439 return false 440 } 441 // Test mains always have exactly one GoFile that is in the build cache. 442 if len(pkg.GoFiles) > 1 { 443 return false 444 } 445 if !strings.HasPrefix(pkg.GoFiles[0], gocache) { 446 return false 447 } 448 return true 449 }