github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/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 "crypto/sha256" 10 "fmt" 11 "io/ioutil" 12 "os" 13 "path/filepath" 14 "sort" 15 "strings" 16 "time" 17 18 "github.com/jhump/golang-x-tools/go/packages" 19 "github.com/jhump/golang-x-tools/internal/event" 20 "github.com/jhump/golang-x-tools/internal/gocommand" 21 "github.com/jhump/golang-x-tools/internal/lsp/debug/tag" 22 "github.com/jhump/golang-x-tools/internal/lsp/protocol" 23 "github.com/jhump/golang-x-tools/internal/lsp/source" 24 "github.com/jhump/golang-x-tools/internal/memoize" 25 "github.com/jhump/golang-x-tools/internal/packagesinternal" 26 "github.com/jhump/golang-x-tools/internal/span" 27 errors "golang.org/x/xerrors" 28 ) 29 30 // load calls packages.Load for the given scopes, updating package metadata, 31 // import graph, and mapped files with the result. 32 func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interface{}) (err error) { 33 var query []string 34 var containsDir bool // for logging 35 for _, scope := range scopes { 36 if !s.shouldLoad(scope) { 37 continue 38 } 39 // Unless the context was canceled, set "shouldLoad" to false for all 40 // of the metadata we attempted to load. 41 defer func() { 42 if errors.Is(err, context.Canceled) { 43 return 44 } 45 s.clearShouldLoad(scope) 46 }() 47 switch scope := scope.(type) { 48 case PackagePath: 49 if source.IsCommandLineArguments(string(scope)) { 50 panic("attempted to load command-line-arguments") 51 } 52 // The only time we pass package paths is when we're doing a 53 // partial workspace load. In those cases, the paths came back from 54 // go list and should already be GOPATH-vendorized when appropriate. 55 query = append(query, string(scope)) 56 case fileURI: 57 uri := span.URI(scope) 58 // Don't try to load a file that doesn't exist. 59 fh := s.FindFile(uri) 60 if fh == nil || s.View().FileKind(fh) != source.Go { 61 continue 62 } 63 query = append(query, fmt.Sprintf("file=%s", uri.Filename())) 64 case moduleLoadScope: 65 switch scope { 66 case "std", "cmd": 67 query = append(query, string(scope)) 68 default: 69 query = append(query, fmt.Sprintf("%s/...", scope)) 70 } 71 case viewLoadScope: 72 // If we are outside of GOPATH, a module, or some other known 73 // build system, don't load subdirectories. 74 if !s.ValidBuildConfiguration() { 75 query = append(query, "./") 76 } else { 77 query = append(query, "./...") 78 } 79 default: 80 panic(fmt.Sprintf("unknown scope type %T", scope)) 81 } 82 switch scope.(type) { 83 case viewLoadScope, moduleLoadScope: 84 containsDir = true 85 } 86 } 87 if len(query) == 0 { 88 return nil 89 } 90 sort.Strings(query) // for determinism 91 92 if s.view.Options().VerboseWorkDoneProgress { 93 work := s.view.session.progress.Start(ctx, "Load", fmt.Sprintf("Loading query=%s", query), nil, nil) 94 defer func() { 95 work.End("Done.") 96 }() 97 } 98 99 ctx, done := event.Start(ctx, "cache.view.load", tag.Query.Of(query)) 100 defer done() 101 102 flags := source.LoadWorkspace 103 if allowNetwork { 104 flags |= source.AllowNetwork 105 } 106 _, inv, cleanup, err := s.goCommandInvocation(ctx, flags, &gocommand.Invocation{ 107 WorkingDir: s.view.rootURI.Filename(), 108 }) 109 if err != nil { 110 return err 111 } 112 113 // Set a last resort deadline on packages.Load since it calls the go 114 // command, which may hang indefinitely if it has a bug. golang/go#42132 115 // and golang/go#42255 have more context. 116 ctx, cancel := context.WithTimeout(ctx, 10*time.Minute) 117 defer cancel() 118 119 cfg := s.config(ctx, inv) 120 pkgs, err := packages.Load(cfg, query...) 121 cleanup() 122 123 // If the context was canceled, return early. Otherwise, we might be 124 // type-checking an incomplete result. Check the context directly, 125 // because go/packages adds extra information to the error. 126 if ctx.Err() != nil { 127 return ctx.Err() 128 } 129 if err != nil { 130 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))) 131 } else { 132 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))) 133 } 134 if len(pkgs) == 0 { 135 if err == nil { 136 err = fmt.Errorf("no packages returned") 137 } 138 return errors.Errorf("%v: %w", err, source.PackagesLoadError) 139 } 140 for _, pkg := range pkgs { 141 if !containsDir || s.view.Options().VerboseOutput { 142 event.Log(ctx, "go/packages.Load", 143 tag.Snapshot.Of(s.ID()), 144 tag.Package.Of(pkg.ID), 145 tag.Files.Of(pkg.CompiledGoFiles)) 146 } 147 // Ignore packages with no sources, since we will never be able to 148 // correctly invalidate that metadata. 149 if len(pkg.GoFiles) == 0 && len(pkg.CompiledGoFiles) == 0 { 150 continue 151 } 152 // Special case for the builtin package, as it has no dependencies. 153 if pkg.PkgPath == "builtin" { 154 if len(pkg.GoFiles) != 1 { 155 return errors.Errorf("only expected 1 file for builtin, got %v", len(pkg.GoFiles)) 156 } 157 s.setBuiltin(pkg.GoFiles[0]) 158 continue 159 } 160 // Skip test main packages. 161 if isTestMain(pkg, s.view.gocache) { 162 continue 163 } 164 // Skip filtered packages. They may be added anyway if they're 165 // dependencies of non-filtered packages. 166 if s.view.allFilesExcluded(pkg) { 167 continue 168 } 169 // Set the metadata for this package. 170 s.mu.Lock() 171 m, err := s.setMetadataLocked(ctx, PackagePath(pkg.PkgPath), pkg, cfg, query, map[PackageID]struct{}{}) 172 s.mu.Unlock() 173 if err != nil { 174 return err 175 } 176 if _, err := s.buildPackageHandle(ctx, m.ID, s.workspaceParseMode(m.ID)); err != nil { 177 return err 178 } 179 } 180 // Rebuild the import graph when the metadata is updated. 181 s.clearAndRebuildImportGraph() 182 183 return nil 184 } 185 186 // workspaceLayoutErrors returns a diagnostic for every open file, as well as 187 // an error message if there are no open files. 188 func (s *snapshot) workspaceLayoutError(ctx context.Context) *source.CriticalError { 189 if len(s.workspace.getKnownModFiles()) == 0 { 190 return nil 191 } 192 if s.view.userGo111Module == off { 193 return nil 194 } 195 if s.workspace.moduleSource != legacyWorkspace { 196 return nil 197 } 198 // If the user has one module per view, there is nothing to warn about. 199 if s.ValidBuildConfiguration() && len(s.workspace.getKnownModFiles()) == 1 { 200 return nil 201 } 202 203 // Apply diagnostics about the workspace configuration to relevant open 204 // files. 205 openFiles := s.openFiles() 206 207 // If the snapshot does not have a valid build configuration, it may be 208 // that the user has opened a directory that contains multiple modules. 209 // Check for that an warn about it. 210 if !s.ValidBuildConfiguration() { 211 msg := `gopls requires a module at the root of your workspace. 212 You can work with multiple modules by opening each one as a workspace folder. 213 Improvements to this workflow will be coming soon, and you can learn more here: 214 https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.` 215 return &source.CriticalError{ 216 MainError: errors.Errorf(msg), 217 DiagList: s.applyCriticalErrorToFiles(ctx, msg, openFiles), 218 } 219 } 220 221 // If the user has one active go.mod file, they may still be editing files 222 // in nested modules. Check the module of each open file and add warnings 223 // that the nested module must be opened as a workspace folder. 224 if len(s.workspace.getActiveModFiles()) == 1 { 225 // Get the active root go.mod file to compare against. 226 var rootModURI span.URI 227 for uri := range s.workspace.getActiveModFiles() { 228 rootModURI = uri 229 } 230 nestedModules := map[string][]source.VersionedFileHandle{} 231 for _, fh := range openFiles { 232 modURI := moduleForURI(s.workspace.knownModFiles, fh.URI()) 233 if modURI != rootModURI { 234 modDir := filepath.Dir(modURI.Filename()) 235 nestedModules[modDir] = append(nestedModules[modDir], fh) 236 } 237 } 238 // Add a diagnostic to each file in a nested module to mark it as 239 // "orphaned". Don't show a general diagnostic in the progress bar, 240 // because the user may still want to edit a file in a nested module. 241 var srcDiags []*source.Diagnostic 242 for modDir, uris := range nestedModules { 243 msg := fmt.Sprintf(`This file is in %s, which is a nested module in the %s module. 244 gopls currently requires one module per workspace folder. 245 Please open %s as a separate workspace folder. 246 You can learn more here: https://github.com/golang/tools/blob/master/gopls/doc/workspace.md. 247 `, modDir, filepath.Dir(rootModURI.Filename()), modDir) 248 srcDiags = append(srcDiags, s.applyCriticalErrorToFiles(ctx, msg, uris)...) 249 } 250 if len(srcDiags) != 0 { 251 return &source.CriticalError{ 252 MainError: errors.Errorf(`You are working in a nested module. 253 Please open it as a separate workspace folder. Learn more: 254 https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`), 255 DiagList: srcDiags, 256 } 257 } 258 } 259 return nil 260 } 261 262 func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, files []source.VersionedFileHandle) []*source.Diagnostic { 263 var srcDiags []*source.Diagnostic 264 for _, fh := range files { 265 // Place the diagnostics on the package or module declarations. 266 var rng protocol.Range 267 switch s.view.FileKind(fh) { 268 case source.Go: 269 if pgf, err := s.ParseGo(ctx, fh, source.ParseHeader); err == nil { 270 pkgDecl := span.NewRange(s.FileSet(), pgf.File.Package, pgf.File.Name.End()) 271 if spn, err := pkgDecl.Span(); err == nil { 272 rng, _ = pgf.Mapper.Range(spn) 273 } 274 } 275 case source.Mod: 276 if pmf, err := s.ParseMod(ctx, fh); err == nil { 277 if pmf.File.Module != nil && pmf.File.Module.Syntax != nil { 278 rng, _ = rangeFromPositions(pmf.Mapper, pmf.File.Module.Syntax.Start, pmf.File.Module.Syntax.End) 279 } 280 } 281 } 282 srcDiags = append(srcDiags, &source.Diagnostic{ 283 URI: fh.URI(), 284 Range: rng, 285 Severity: protocol.SeverityError, 286 Source: source.ListError, 287 Message: msg, 288 }) 289 } 290 return srcDiags 291 } 292 293 type workspaceDirKey string 294 295 type workspaceDirData struct { 296 dir string 297 err error 298 } 299 300 // getWorkspaceDir gets the URI for the workspace directory associated with 301 // this snapshot. The workspace directory is a temp directory containing the 302 // go.mod file computed from all active modules. 303 func (s *snapshot) getWorkspaceDir(ctx context.Context) (span.URI, error) { 304 s.mu.Lock() 305 h := s.workspaceDirHandle 306 s.mu.Unlock() 307 if h != nil { 308 return getWorkspaceDir(ctx, h, s.generation) 309 } 310 file, err := s.workspace.modFile(ctx, s) 311 if err != nil { 312 return "", err 313 } 314 hash := sha256.New() 315 modContent, err := file.Format() 316 if err != nil { 317 return "", err 318 } 319 sumContent, err := s.workspace.sumFile(ctx, s) 320 if err != nil { 321 return "", err 322 } 323 hash.Write(modContent) 324 hash.Write(sumContent) 325 key := workspaceDirKey(hash.Sum(nil)) 326 s.mu.Lock() 327 h = s.generation.Bind(key, func(context.Context, memoize.Arg) interface{} { 328 tmpdir, err := ioutil.TempDir("", "gopls-workspace-mod") 329 if err != nil { 330 return &workspaceDirData{err: err} 331 } 332 333 for name, content := range map[string][]byte{ 334 "go.mod": modContent, 335 "go.sum": sumContent, 336 } { 337 filename := filepath.Join(tmpdir, name) 338 if err := ioutil.WriteFile(filename, content, 0644); err != nil { 339 os.RemoveAll(tmpdir) 340 return &workspaceDirData{err: err} 341 } 342 } 343 344 return &workspaceDirData{dir: tmpdir} 345 }, func(v interface{}) { 346 d := v.(*workspaceDirData) 347 if d.dir != "" { 348 if err := os.RemoveAll(d.dir); err != nil { 349 event.Error(context.Background(), "cleaning workspace dir", err) 350 } 351 } 352 }) 353 s.workspaceDirHandle = h 354 s.mu.Unlock() 355 return getWorkspaceDir(ctx, h, s.generation) 356 } 357 358 func getWorkspaceDir(ctx context.Context, h *memoize.Handle, g *memoize.Generation) (span.URI, error) { 359 v, err := h.Get(ctx, g, nil) 360 if err != nil { 361 return "", err 362 } 363 return span.URIFromPath(v.(*workspaceDirData).dir), nil 364 } 365 366 // setMetadataLocked extracts metadata from pkg and records it in s. It 367 // recurses through pkg.Imports to ensure that metadata exists for all 368 // dependencies. 369 func (s *snapshot) setMetadataLocked(ctx context.Context, pkgPath PackagePath, pkg *packages.Package, cfg *packages.Config, query []string, seen map[PackageID]struct{}) (*Metadata, error) { 370 id := PackageID(pkg.ID) 371 if source.IsCommandLineArguments(pkg.ID) { 372 suffix := ":" + strings.Join(query, ",") 373 id = PackageID(string(id) + suffix) 374 pkgPath = PackagePath(string(pkgPath) + suffix) 375 } 376 if _, ok := seen[id]; ok { 377 return nil, errors.Errorf("import cycle detected: %q", id) 378 } 379 // Recreate the metadata rather than reusing it to avoid locking. 380 m := &Metadata{ 381 ID: id, 382 PkgPath: pkgPath, 383 Name: PackageName(pkg.Name), 384 ForTest: PackagePath(packagesinternal.GetForTest(pkg)), 385 TypesSizes: pkg.TypesSizes, 386 Config: cfg, 387 Module: pkg.Module, 388 depsErrors: packagesinternal.GetDepsErrors(pkg), 389 } 390 391 for _, err := range pkg.Errors { 392 // Filter out parse errors from go list. We'll get them when we 393 // actually parse, and buggy overlay support may generate spurious 394 // errors. (See TestNewModule_Issue38207.) 395 if strings.Contains(err.Msg, "expected '") { 396 continue 397 } 398 m.Errors = append(m.Errors, err) 399 } 400 401 uris := map[span.URI]struct{}{} 402 for _, filename := range pkg.CompiledGoFiles { 403 uri := span.URIFromPath(filename) 404 m.CompiledGoFiles = append(m.CompiledGoFiles, uri) 405 uris[uri] = struct{}{} 406 } 407 for _, filename := range pkg.GoFiles { 408 uri := span.URIFromPath(filename) 409 m.GoFiles = append(m.GoFiles, uri) 410 uris[uri] = struct{}{} 411 } 412 s.updateIDForURIsLocked(id, uris) 413 414 // TODO(rstambler): is this still necessary? 415 copied := map[PackageID]struct{}{ 416 id: {}, 417 } 418 for k, v := range seen { 419 copied[k] = v 420 } 421 for importPath, importPkg := range pkg.Imports { 422 importPkgPath := PackagePath(importPath) 423 importID := PackageID(importPkg.ID) 424 425 m.Deps = append(m.Deps, importID) 426 427 // Don't remember any imports with significant errors. 428 if importPkgPath != "unsafe" && len(importPkg.CompiledGoFiles) == 0 { 429 if m.MissingDeps == nil { 430 m.MissingDeps = make(map[PackagePath]struct{}) 431 } 432 m.MissingDeps[importPkgPath] = struct{}{} 433 continue 434 } 435 if s.noValidMetadataForIDLocked(importID) { 436 if _, err := s.setMetadataLocked(ctx, importPkgPath, importPkg, cfg, query, copied); err != nil { 437 event.Error(ctx, "error in dependency", err) 438 } 439 } 440 } 441 442 // Add the metadata to the cache. 443 444 // If we've already set the metadata for this snapshot, reuse it. 445 if original, ok := s.metadata[m.ID]; ok && original.Valid { 446 // Since we've just reloaded, clear out shouldLoad. 447 original.ShouldLoad = false 448 m = original.Metadata 449 } else { 450 s.metadata[m.ID] = &KnownMetadata{ 451 Metadata: m, 452 Valid: true, 453 } 454 // Invalidate any packages we may have associated with this metadata. 455 for _, mode := range []source.ParseMode{source.ParseHeader, source.ParseExported, source.ParseFull} { 456 key := packageKey{mode, m.ID} 457 delete(s.packages, key) 458 } 459 } 460 461 // Set the workspace packages. If any of the package's files belong to the 462 // view, then the package may be a workspace package. 463 for _, uri := range append(m.CompiledGoFiles, m.GoFiles...) { 464 if !s.view.contains(uri) { 465 continue 466 } 467 468 // The package's files are in this view. It may be a workspace package. 469 if strings.Contains(string(uri), "/vendor/") { 470 // Vendored packages are not likely to be interesting to the user. 471 continue 472 } 473 474 switch { 475 case m.ForTest == "": 476 // A normal package. 477 s.workspacePackages[m.ID] = pkgPath 478 case m.ForTest == m.PkgPath, m.ForTest+"_test" == m.PkgPath: 479 // The test variant of some workspace package or its x_test. 480 // To load it, we need to load the non-test variant with -test. 481 s.workspacePackages[m.ID] = m.ForTest 482 default: 483 // A test variant of some intermediate package. We don't care about it. 484 m.IsIntermediateTestVariant = true 485 } 486 } 487 return m, nil 488 } 489 490 func isTestMain(pkg *packages.Package, gocache string) bool { 491 // Test mains must have an import path that ends with ".test". 492 if !strings.HasSuffix(pkg.PkgPath, ".test") { 493 return false 494 } 495 // Test main packages are always named "main". 496 if pkg.Name != "main" { 497 return false 498 } 499 // Test mains always have exactly one GoFile that is in the build cache. 500 if len(pkg.GoFiles) > 1 { 501 return false 502 } 503 if !source.InDir(gocache, pkg.GoFiles[0]) { 504 return false 505 } 506 return true 507 }