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