| // Copyright 2019 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package cache |
| |
| import ( |
| "bytes" |
| "context" |
| "errors" |
| "fmt" |
| "go/ast" |
| "go/types" |
| "path/filepath" |
| "regexp" |
| "strings" |
| "sync" |
| |
| "golang.org/x/mod/module" |
| "golang.org/x/sync/errgroup" |
| "golang.org/x/tools/go/ast/astutil" |
| "golang.org/x/tools/go/packages" |
| "golang.org/x/tools/gopls/internal/lsp/protocol" |
| "golang.org/x/tools/gopls/internal/lsp/source" |
| "golang.org/x/tools/gopls/internal/span" |
| "golang.org/x/tools/internal/bug" |
| "golang.org/x/tools/internal/event" |
| "golang.org/x/tools/internal/event/tag" |
| "golang.org/x/tools/internal/memoize" |
| "golang.org/x/tools/internal/packagesinternal" |
| "golang.org/x/tools/internal/typeparams" |
| "golang.org/x/tools/internal/typesinternal" |
| ) |
| |
| // A packageKey identifies a packageHandle in the snapshot.packages map. |
| type packageKey struct { |
| mode source.ParseMode |
| id PackageID |
| } |
| |
| type packageHandleKey source.Hash |
| |
| // A packageHandle is a handle to the future result of type-checking a package. |
| // The resulting package is obtained from the await() method. |
| type packageHandle struct { |
| promise *memoize.Promise // [typeCheckResult] |
| |
| // m is the metadata associated with the package. |
| m *source.Metadata |
| |
| // key is the hashed key for the package. |
| // |
| // It includes the all bits of the transitive closure of |
| // dependencies's sources. This is more than type checking |
| // really depends on: export data of direct deps should be |
| // enough. (The key for analysis actions could similarly |
| // hash only Facts of direct dependencies.) |
| key packageHandleKey |
| } |
| |
| // typeCheckResult contains the result of a call to |
| // typeCheckImpl, which type-checks a package. |
| type typeCheckResult struct { |
| pkg *pkg |
| err error |
| } |
| |
| // buildPackageHandle returns a handle for the future results of |
| // type-checking the package identified by id in the given mode. |
| // It assumes that the given ID already has metadata available, so it does not |
| // attempt to reload missing or invalid metadata. The caller must reload |
| // metadata if needed. |
| func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode source.ParseMode) (*packageHandle, error) { |
| packageKey := packageKey{id: id, mode: mode} |
| |
| s.mu.Lock() |
| entry, hit := s.packages.Get(packageKey) |
| m := s.meta.metadata[id] |
| s.mu.Unlock() |
| |
| if m == nil { |
| return nil, fmt.Errorf("no metadata for %s", id) |
| } |
| |
| if hit { |
| return entry.(*packageHandle), nil |
| } |
| |
| // Begin computing the key by getting the depKeys for all dependencies. |
| // This requires reading the transitive closure of dependencies' source files. |
| // |
| // It is tempting to parallelize the recursion here, but |
| // without de-duplication of subtasks this would lead to an |
| // exponential amount of work, and computing the key is |
| // expensive as it reads all the source files transitively. |
| // Notably, we don't update the s.packages cache until the |
| // entire key has been computed. |
| // TODO(adonovan): use a promise cache to ensure that the key |
| // for each package is computed by at most one thread, then do |
| // the recursive key building of dependencies in parallel. |
| deps := make(map[PackageID]*packageHandle) |
| var depKey source.Hash // XOR of all unique deps |
| for _, depID := range m.DepsByPkgPath { |
| depHandle, err := s.buildPackageHandle(ctx, depID, s.workspaceParseMode(depID)) |
| // Don't use invalid metadata for dependencies if the top-level |
| // metadata is valid. We only load top-level packages, so if the |
| // top-level is valid, all of its dependencies should be as well. |
| if err != nil { |
| event.Error(ctx, fmt.Sprintf("%s: no dep handle for %s", id, depID), err, source.SnapshotLabels(s)...) |
| |
| // This check ensures we break out of the slow |
| // buildPackageHandle recursion quickly when |
| // context cancelation is detected within GetFile. |
| if ctx.Err() != nil { |
| return nil, ctx.Err() // cancelled |
| } |
| |
| // One bad dependency should not prevent us from |
| // checking the entire package. Leave depKeys[i] unset. |
| continue |
| } |
| |
| depKey.XORWith(source.Hash(depHandle.key)) |
| |
| deps[depID] = depHandle |
| } |
| |
| // Read both lists of files of this package, in parallel. |
| // |
| // goFiles aren't presented to the type checker--nor |
| // are they included in the key, unsoundly--but their |
| // syntax trees are available from (*pkg).File(URI). |
| // TODO(adonovan): consider parsing them on demand? |
| // The need should be rare. |
| goFiles, compiledGoFiles, err := readGoFiles(ctx, s, m) |
| if err != nil { |
| return nil, err |
| } |
| |
| // All the file reading has now been done. |
| // Create a handle for the result of type checking. |
| experimentalKey := s.View().Options().ExperimentalPackageCacheKey |
| phKey := computePackageKey(m.ID, compiledGoFiles, m, depKey, mode, experimentalKey) |
| promise, release := s.store.Promise(phKey, func(ctx context.Context, arg interface{}) interface{} { |
| pkg, err := typeCheckImpl(ctx, arg.(*snapshot), goFiles, compiledGoFiles, m, mode, deps) |
| return typeCheckResult{pkg, err} |
| }) |
| |
| ph := &packageHandle{ |
| promise: promise, |
| m: m, |
| key: phKey, |
| } |
| |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| |
| // Check that the metadata has not changed |
| // (which should invalidate this handle). |
| // |
| // (In future, handles should form a graph with edges from a |
| // packageHandle to the handles for parsing its files and the |
| // handles for type-checking its immediate deps, at which |
| // point there will be no need to even access s.meta.) |
| if s.meta.metadata[ph.m.ID] != ph.m { |
| return nil, fmt.Errorf("stale metadata for %s", ph.m.ID) |
| } |
| |
| // Check cache again in case another thread got there first. |
| if prev, ok := s.packages.Get(packageKey); ok { |
| prevPH := prev.(*packageHandle) |
| release() |
| if prevPH.m != ph.m { |
| return nil, bug.Errorf("existing package handle does not match for %s", ph.m.ID) |
| } |
| return prevPH, nil |
| } |
| |
| // Update the map. |
| s.packages.Set(packageKey, ph, func(_, _ interface{}) { release() }) |
| |
| return ph, nil |
| } |
| |
| // readGoFiles reads the content of Metadata.GoFiles and |
| // Metadata.CompiledGoFiles, in parallel. |
| func readGoFiles(ctx context.Context, s *snapshot, m *source.Metadata) (goFiles, compiledGoFiles []source.FileHandle, err error) { |
| var group errgroup.Group |
| getFileHandles := func(files []span.URI) []source.FileHandle { |
| fhs := make([]source.FileHandle, len(files)) |
| for i, uri := range files { |
| i, uri := i, uri |
| group.Go(func() (err error) { |
| fhs[i], err = s.GetFile(ctx, uri) // ~25us |
| return |
| }) |
| } |
| return fhs |
| } |
| return getFileHandles(m.GoFiles), |
| getFileHandles(m.CompiledGoFiles), |
| group.Wait() |
| } |
| |
| func (s *snapshot) workspaceParseMode(id PackageID) source.ParseMode { |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| _, ws := s.workspacePackages[id] |
| if !ws { |
| return source.ParseExported |
| } |
| if s.view.Options().MemoryMode == source.ModeNormal { |
| return source.ParseFull |
| } |
| if s.isActiveLocked(id) { |
| return source.ParseFull |
| } |
| return source.ParseExported |
| } |
| |
| // computePackageKey returns a key representing the act of type checking |
| // a package named id containing the specified files, metadata, and |
| // combined dependency hash. |
| func computePackageKey(id PackageID, files []source.FileHandle, m *source.Metadata, depsKey source.Hash, mode source.ParseMode, experimentalKey bool) packageHandleKey { |
| // TODO(adonovan): opt: no need to materalize the bytes; hash them directly. |
| // Also, use field separators to avoid spurious collisions. |
| b := bytes.NewBuffer(nil) |
| b.WriteString(string(id)) |
| if m.Module != nil { |
| b.WriteString(m.Module.GoVersion) // go version affects type check errors. |
| } |
| if !experimentalKey { |
| // cfg was used to produce the other hashed inputs (package ID, parsed Go |
| // files, and deps). It should not otherwise affect the inputs to the type |
| // checker, so this experiment omits it. This should increase cache hits on |
| // the daemon as cfg contains the environment and working directory. |
| hc := hashConfig(m.Config) |
| b.Write(hc[:]) |
| } |
| b.WriteByte(byte(mode)) |
| b.Write(depsKey[:]) |
| for _, file := range files { |
| b.WriteString(file.FileIdentity().String()) |
| } |
| // Metadata errors are interpreted and memoized on the computed package, so |
| // we must hash them into the key here. |
| // |
| // TODO(rfindley): handle metadata diagnostics independently from |
| // type-checking diagnostics. |
| for _, err := range m.Errors { |
| b.WriteString(err.Msg) |
| b.WriteString(err.Pos) |
| b.WriteRune(rune(err.Kind)) |
| } |
| return packageHandleKey(source.HashOf(b.Bytes())) |
| } |
| |
| // hashConfig returns the hash for the *packages.Config. |
| func hashConfig(config *packages.Config) source.Hash { |
| // TODO(adonovan): opt: don't materialize the bytes; hash them directly. |
| // Also, use sound field separators to avoid collisions. |
| var b bytes.Buffer |
| |
| // Dir, Mode, Env, BuildFlags are the parts of the config that can change. |
| b.WriteString(config.Dir) |
| b.WriteRune(rune(config.Mode)) |
| |
| for _, e := range config.Env { |
| b.WriteString(e) |
| } |
| for _, f := range config.BuildFlags { |
| b.WriteString(f) |
| } |
| return source.HashOf(b.Bytes()) |
| } |
| |
| // await waits for typeCheckImpl to complete and returns its result. |
| func (ph *packageHandle) await(ctx context.Context, s *snapshot) (*pkg, error) { |
| v, err := s.awaitPromise(ctx, ph.promise) |
| if err != nil { |
| return nil, err |
| } |
| data := v.(typeCheckResult) |
| return data.pkg, data.err |
| } |
| |
| func (ph *packageHandle) CompiledGoFiles() []span.URI { |
| return ph.m.CompiledGoFiles |
| } |
| |
| func (ph *packageHandle) ID() string { |
| return string(ph.m.ID) |
| } |
| |
| func (ph *packageHandle) cached() (*pkg, error) { |
| v := ph.promise.Cached() |
| if v == nil { |
| return nil, fmt.Errorf("no cached type information for %s", ph.m.PkgPath) |
| } |
| data := v.(typeCheckResult) |
| return data.pkg, data.err |
| } |
| |
| // typeCheckImpl type checks the parsed source files in compiledGoFiles. |
| // (The resulting pkg also holds the parsed but not type-checked goFiles.) |
| // deps holds the future results of type-checking the direct dependencies. |
| func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *source.Metadata, mode source.ParseMode, deps map[PackageID]*packageHandle) (*pkg, error) { |
| // Start type checking of direct dependencies, |
| // in parallel and asynchronously. |
| // As the type checker imports each of these |
| // packages, it will wait for its completion. |
| var wg sync.WaitGroup |
| for _, dep := range deps { |
| wg.Add(1) |
| go func(dep *packageHandle) { |
| dep.await(ctx, snapshot) // ignore result |
| wg.Done() |
| }(dep) |
| } |
| // The 'defer' below is unusual but intentional: |
| // it is not necessary that each call to dep.check |
| // complete before type checking begins, as the type |
| // checker will wait for those it needs. But they do |
| // need to complete before this function returns and |
| // the snapshot is possibly destroyed. |
| defer wg.Wait() |
| |
| var filter *unexportedFilter |
| if mode == source.ParseExported { |
| filter = &unexportedFilter{uses: map[string]bool{}} |
| } |
| pkg, err := doTypeCheck(ctx, snapshot, goFiles, compiledGoFiles, m, mode, deps, filter) |
| if err != nil { |
| return nil, err |
| } |
| |
| if mode == source.ParseExported { |
| // The AST filtering is a little buggy and may remove things it |
| // shouldn't. If we only got undeclared name errors, try one more |
| // time keeping those names. |
| missing, unexpected := filter.ProcessErrors(pkg.typeErrors) |
| if len(unexpected) == 0 && len(missing) != 0 { |
| pkg, err = doTypeCheck(ctx, snapshot, goFiles, compiledGoFiles, m, mode, deps, filter) |
| if err != nil { |
| return nil, err |
| } |
| missing, unexpected = filter.ProcessErrors(pkg.typeErrors) |
| } |
| if len(unexpected) != 0 || len(missing) != 0 { |
| pkg, err = doTypeCheck(ctx, snapshot, goFiles, compiledGoFiles, m, mode, deps, nil) |
| if err != nil { |
| return nil, err |
| } |
| } |
| } |
| // If this is a replaced module in the workspace, the version is |
| // meaningless, and we don't want clients to access it. |
| if m.Module != nil { |
| version := m.Module.Version |
| if source.IsWorkspaceModuleVersion(version) { |
| version = "" |
| } |
| pkg.version = &module.Version{ |
| Path: m.Module.Path, |
| Version: version, |
| } |
| } |
| |
| // We don't care about a package's errors unless we have parsed it in full. |
| if mode != source.ParseFull { |
| return pkg, nil |
| } |
| |
| for _, e := range m.Errors { |
| diags, err := goPackagesErrorDiagnostics(snapshot, pkg, e) |
| if err != nil { |
| event.Error(ctx, "unable to compute positions for list errors", err, tag.Package.Of(string(pkg.ID()))) |
| continue |
| } |
| pkg.diagnostics = append(pkg.diagnostics, diags...) |
| } |
| |
| // Our heuristic for whether to show type checking errors is: |
| // + If any file was 'fixed', don't show type checking errors as we |
| // can't guarantee that they reference accurate locations in the source. |
| // + If there is a parse error _in the current file_, suppress type |
| // errors in that file. |
| // + Otherwise, show type errors even in the presence of parse errors in |
| // other package files. go/types attempts to suppress follow-on errors |
| // due to bad syntax, so on balance type checking errors still provide |
| // a decent signal/noise ratio as long as the file in question parses. |
| |
| // Track URIs with parse errors so that we can suppress type errors for these |
| // files. |
| unparseable := map[span.URI]bool{} |
| for _, e := range pkg.parseErrors { |
| diags, err := parseErrorDiagnostics(snapshot, pkg, e) |
| if err != nil { |
| event.Error(ctx, "unable to compute positions for parse errors", err, tag.Package.Of(string(pkg.ID()))) |
| continue |
| } |
| for _, diag := range diags { |
| unparseable[diag.URI] = true |
| pkg.diagnostics = append(pkg.diagnostics, diag) |
| } |
| } |
| |
| if pkg.hasFixedFiles { |
| return pkg, nil |
| } |
| |
| unexpanded := pkg.typeErrors |
| pkg.typeErrors = nil |
| for _, e := range expandErrors(unexpanded, snapshot.View().Options().RelatedInformationSupported) { |
| diags, err := typeErrorDiagnostics(snapshot, pkg, e) |
| if err != nil { |
| event.Error(ctx, "unable to compute positions for type errors", err, tag.Package.Of(string(pkg.ID()))) |
| continue |
| } |
| pkg.typeErrors = append(pkg.typeErrors, e.primary) |
| for _, diag := range diags { |
| // If the file didn't parse cleanly, it is highly likely that type |
| // checking errors will be confusing or redundant. But otherwise, type |
| // checking usually provides a good enough signal to include. |
| if !unparseable[diag.URI] { |
| pkg.diagnostics = append(pkg.diagnostics, diag) |
| } |
| } |
| } |
| |
| depsErrors, err := snapshot.depsErrors(ctx, pkg) |
| if err != nil { |
| return nil, err |
| } |
| pkg.diagnostics = append(pkg.diagnostics, depsErrors...) |
| |
| return pkg, nil |
| } |
| |
| var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`) |
| |
| func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *source.Metadata, mode source.ParseMode, deps map[PackageID]*packageHandle, astFilter *unexportedFilter) (*pkg, error) { |
| ctx, done := event.Start(ctx, "cache.typeCheck", tag.Package.Of(string(m.ID))) |
| defer done() |
| |
| pkg := &pkg{ |
| m: m, |
| mode: mode, |
| fset: snapshot.FileSet(), // must match parse call below (snapshot.ParseGo for now) |
| deps: make(map[PackageID]*pkg), |
| types: types.NewPackage(string(m.PkgPath), string(m.Name)), |
| typesInfo: &types.Info{ |
| Types: make(map[ast.Expr]types.TypeAndValue), |
| Defs: make(map[*ast.Ident]types.Object), |
| Uses: make(map[*ast.Ident]types.Object), |
| Implicits: make(map[ast.Node]types.Object), |
| Selections: make(map[*ast.SelectorExpr]*types.Selection), |
| Scopes: make(map[ast.Node]*types.Scope), |
| }, |
| } |
| typeparams.InitInstanceInfo(pkg.typesInfo) |
| |
| // In the presence of line directives, we may need to report errors in |
| // non-compiled Go files, so we need to register them on the package. |
| // However, we only need to really parse them in ParseFull mode, when |
| // the user might actually be looking at the file. |
| goMode := source.ParseFull |
| if mode != source.ParseFull { |
| goMode = source.ParseHeader |
| } |
| |
| // Parse the GoFiles. (These aren't presented to the type |
| // checker but are part of the returned pkg.) |
| // TODO(adonovan): opt: parallelize parsing. |
| for _, fh := range goFiles { |
| pgf, err := snapshot.ParseGo(ctx, fh, goMode) |
| if err != nil { |
| return nil, err |
| } |
| pkg.goFiles = append(pkg.goFiles, pgf) |
| } |
| |
| // Parse the CompiledGoFiles: those seen by the compiler/typechecker. |
| if err := parseCompiledGoFiles(ctx, compiledGoFiles, snapshot, mode, pkg, astFilter); err != nil { |
| return nil, err |
| } |
| |
| // Use the default type information for the unsafe package. |
| if m.PkgPath == "unsafe" { |
| // Don't type check Unsafe: it's unnecessary, and doing so exposes a data |
| // race to Unsafe.completed. |
| pkg.types = types.Unsafe |
| return pkg, nil |
| } |
| |
| if len(m.CompiledGoFiles) == 0 { |
| // No files most likely means go/packages failed. Try to attach error |
| // messages to the file as much as possible. |
| var found bool |
| for _, e := range m.Errors { |
| srcDiags, err := goPackagesErrorDiagnostics(snapshot, pkg, e) |
| if err != nil { |
| continue |
| } |
| found = true |
| pkg.diagnostics = append(pkg.diagnostics, srcDiags...) |
| } |
| if found { |
| return pkg, nil |
| } |
| return nil, fmt.Errorf("no parsed files for package %s, expected: %v, errors: %v", pkg.m.PkgPath, pkg.compiledGoFiles, m.Errors) |
| } |
| |
| cfg := &types.Config{ |
| Error: func(e error) { |
| pkg.typeErrors = append(pkg.typeErrors, e.(types.Error)) |
| }, |
| Importer: importerFunc(func(path string) (*types.Package, error) { |
| // While all of the import errors could be reported |
| // based on the metadata before we start type checking, |
| // reporting them via types.Importer places the errors |
| // at the correct source location. |
| id, ok := pkg.m.DepsByImpPath[ImportPath(path)] |
| if !ok { |
| // If the import declaration is broken, |
| // go list may fail to report metadata about it. |
| // See TestFixImportDecl for an example. |
| return nil, fmt.Errorf("missing metadata for import of %q", path) |
| } |
| dep, ok := deps[id] // id may be "" |
| if !ok { |
| return nil, snapshot.missingPkgError(path) |
| } |
| if !source.IsValidImport(m.PkgPath, dep.m.PkgPath) { |
| return nil, fmt.Errorf("invalid use of internal package %s", path) |
| } |
| depPkg, err := dep.await(ctx, snapshot) |
| if err != nil { |
| return nil, err |
| } |
| pkg.deps[depPkg.m.ID] = depPkg |
| return depPkg.types, nil |
| }), |
| } |
| if pkg.m.Module != nil && pkg.m.Module.GoVersion != "" { |
| goVersion := "go" + pkg.m.Module.GoVersion |
| // types.NewChecker panics if GoVersion is invalid. An unparsable mod |
| // file should probably stop us before we get here, but double check |
| // just in case. |
| if goVersionRx.MatchString(goVersion) { |
| typesinternal.SetGoVersion(cfg, goVersion) |
| } |
| } |
| |
| if mode != source.ParseFull { |
| cfg.DisableUnusedImportCheck = true |
| cfg.IgnoreFuncBodies = true |
| } |
| |
| // We want to type check cgo code if go/types supports it. |
| // We passed typecheckCgo to go/packages when we Loaded. |
| typesinternal.SetUsesCgo(cfg) |
| |
| check := types.NewChecker(cfg, pkg.fset, pkg.types, pkg.typesInfo) |
| |
| var files []*ast.File |
| for _, cgf := range pkg.compiledGoFiles { |
| files = append(files, cgf.File) |
| } |
| |
| // Type checking errors are handled via the config, so ignore them here. |
| _ = check.Files(files) // 50us-15ms, depending on size of package |
| |
| // If the context was cancelled, we may have returned a ton of transient |
| // errors to the type checker. Swallow them. |
| if ctx.Err() != nil { |
| return nil, ctx.Err() |
| } |
| return pkg, nil |
| } |
| |
| func parseCompiledGoFiles(ctx context.Context, compiledGoFiles []source.FileHandle, snapshot *snapshot, mode source.ParseMode, pkg *pkg, astFilter *unexportedFilter) error { |
| // TODO(adonovan): opt: parallelize this loop, which takes 1-25ms. |
| for _, fh := range compiledGoFiles { |
| var pgf *source.ParsedGoFile |
| var err error |
| // Only parse Full through the cache -- we need to own Exported ASTs |
| // to prune them. |
| if mode == source.ParseFull { |
| pgf, err = snapshot.ParseGo(ctx, fh, mode) |
| } else { |
| pgf, err = parseGoImpl(ctx, pkg.fset, fh, mode) // ~20us/KB |
| } |
| if err != nil { |
| return err |
| } |
| pkg.compiledGoFiles = append(pkg.compiledGoFiles, pgf) |
| if pgf.ParseErr != nil { |
| pkg.parseErrors = append(pkg.parseErrors, pgf.ParseErr) |
| } |
| // If we have fixed parse errors in any of the files, we should hide type |
| // errors, as they may be completely nonsensical. |
| pkg.hasFixedFiles = pkg.hasFixedFiles || pgf.Fixed |
| } |
| |
| // Optionally remove parts that don't affect the exported API. |
| if mode == source.ParseExported { |
| if astFilter != nil { |
| // aggressive pruning based on reachability |
| var files []*ast.File |
| for _, cgf := range pkg.compiledGoFiles { |
| files = append(files, cgf.File) |
| } |
| astFilter.Filter(files) |
| } else { |
| // simple trimming of function bodies |
| for _, cgf := range pkg.compiledGoFiles { |
| trimAST(cgf.File) |
| } |
| } |
| } |
| |
| return nil |
| } |
| |
| func (s *snapshot) depsErrors(ctx context.Context, pkg *pkg) ([]*source.Diagnostic, error) { |
| // Select packages that can't be found, and were imported in non-workspace packages. |
| // Workspace packages already show their own errors. |
| var relevantErrors []*packagesinternal.PackageError |
| for _, depsError := range pkg.m.DepsErrors { |
| // Up to Go 1.15, the missing package was included in the stack, which |
| // was presumably a bug. We want the next one up. |
| directImporterIdx := len(depsError.ImportStack) - 1 |
| if s.view.goversion < 15 { |
| directImporterIdx = len(depsError.ImportStack) - 2 |
| } |
| if directImporterIdx < 0 { |
| continue |
| } |
| |
| directImporter := depsError.ImportStack[directImporterIdx] |
| if s.isWorkspacePackage(PackageID(directImporter)) { |
| continue |
| } |
| relevantErrors = append(relevantErrors, depsError) |
| } |
| |
| // Don't build the import index for nothing. |
| if len(relevantErrors) == 0 { |
| return nil, nil |
| } |
| |
| // Build an index of all imports in the package. |
| type fileImport struct { |
| cgf *source.ParsedGoFile |
| imp *ast.ImportSpec |
| } |
| allImports := map[string][]fileImport{} |
| for _, cgf := range pkg.compiledGoFiles { |
| // TODO(adonovan): modify Imports() to accept a single token.File (cgf.Tok). |
| for _, group := range astutil.Imports(s.FileSet(), cgf.File) { |
| for _, imp := range group { |
| if imp.Path == nil { |
| continue |
| } |
| path := strings.Trim(imp.Path.Value, `"`) |
| allImports[path] = append(allImports[path], fileImport{cgf, imp}) |
| } |
| } |
| } |
| |
| // Apply a diagnostic to any import involved in the error, stopping once |
| // we reach the workspace. |
| var errors []*source.Diagnostic |
| for _, depErr := range relevantErrors { |
| for i := len(depErr.ImportStack) - 1; i >= 0; i-- { |
| item := depErr.ImportStack[i] |
| if s.isWorkspacePackage(PackageID(item)) { |
| break |
| } |
| |
| for _, imp := range allImports[item] { |
| rng, err := source.NewMappedRange(imp.cgf.Tok, imp.cgf.Mapper, imp.imp.Pos(), imp.imp.End()).Range() |
| if err != nil { |
| return nil, err |
| } |
| fixes, err := goGetQuickFixes(s, imp.cgf.URI, item) |
| if err != nil { |
| return nil, err |
| } |
| errors = append(errors, &source.Diagnostic{ |
| URI: imp.cgf.URI, |
| Range: rng, |
| Severity: protocol.SeverityError, |
| Source: source.TypeError, |
| Message: fmt.Sprintf("error while importing %v: %v", item, depErr.Err), |
| SuggestedFixes: fixes, |
| }) |
| } |
| } |
| } |
| |
| if len(pkg.compiledGoFiles) == 0 { |
| return errors, nil |
| } |
| mod := s.GoModForFile(pkg.compiledGoFiles[0].URI) |
| if mod == "" { |
| return errors, nil |
| } |
| fh, err := s.GetFile(ctx, mod) |
| if err != nil { |
| return nil, err |
| } |
| pm, err := s.ParseMod(ctx, fh) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Add a diagnostic to the module that contained the lowest-level import of |
| // the missing package. |
| for _, depErr := range relevantErrors { |
| for i := len(depErr.ImportStack) - 1; i >= 0; i-- { |
| item := depErr.ImportStack[i] |
| m := s.getMetadata(PackageID(item)) |
| if m == nil || m.Module == nil { |
| continue |
| } |
| modVer := module.Version{Path: m.Module.Path, Version: m.Module.Version} |
| reference := findModuleReference(pm.File, modVer) |
| if reference == nil { |
| continue |
| } |
| rng, err := pm.Mapper.OffsetRange(reference.Start.Byte, reference.End.Byte) |
| if err != nil { |
| return nil, err |
| } |
| fixes, err := goGetQuickFixes(s, pm.URI, item) |
| if err != nil { |
| return nil, err |
| } |
| errors = append(errors, &source.Diagnostic{ |
| URI: pm.URI, |
| Range: rng, |
| Severity: protocol.SeverityError, |
| Source: source.TypeError, |
| Message: fmt.Sprintf("error while importing %v: %v", item, depErr.Err), |
| SuggestedFixes: fixes, |
| }) |
| break |
| } |
| } |
| return errors, nil |
| } |
| |
| // missingPkgError returns an error message for a missing package that varies |
| // based on the user's workspace mode. |
| func (s *snapshot) missingPkgError(pkgPath string) error { |
| var b strings.Builder |
| if s.workspaceMode()&moduleMode == 0 { |
| gorootSrcPkg := filepath.FromSlash(filepath.Join(s.view.goroot, "src", pkgPath)) |
| fmt.Fprintf(&b, "cannot find package %q in any of \n\t%s (from $GOROOT)", pkgPath, gorootSrcPkg) |
| for _, gopath := range filepath.SplitList(s.view.gopath) { |
| gopathSrcPkg := filepath.FromSlash(filepath.Join(gopath, "src", pkgPath)) |
| fmt.Fprintf(&b, "\n\t%s (from $GOPATH)", gopathSrcPkg) |
| } |
| } else { |
| fmt.Fprintf(&b, "no required module provides package %q", pkgPath) |
| if err := s.getInitializationError(); err != nil { |
| fmt.Fprintf(&b, "\n(workspace configuration error: %s)", err.MainError) |
| } |
| } |
| return errors.New(b.String()) |
| } |
| |
| type extendedError struct { |
| primary types.Error |
| secondaries []types.Error |
| } |
| |
| func (e extendedError) Error() string { |
| return e.primary.Error() |
| } |
| |
| // expandErrors duplicates "secondary" errors by mapping them to their main |
| // error. Some errors returned by the type checker are followed by secondary |
| // errors which give more information about the error. These are errors in |
| // their own right, and they are marked by starting with \t. For instance, when |
| // there is a multiply-defined function, the secondary error points back to the |
| // definition first noticed. |
| // |
| // This function associates the secondary error with its primary error, which can |
| // then be used as RelatedInformation when the error becomes a diagnostic. |
| // |
| // If supportsRelatedInformation is false, the secondary is instead embedded as |
| // additional context in the primary error. |
| func expandErrors(errs []types.Error, supportsRelatedInformation bool) []extendedError { |
| var result []extendedError |
| for i := 0; i < len(errs); { |
| original := extendedError{ |
| primary: errs[i], |
| } |
| for i++; i < len(errs); i++ { |
| spl := errs[i] |
| if len(spl.Msg) == 0 || spl.Msg[0] != '\t' { |
| break |
| } |
| spl.Msg = spl.Msg[1:] |
| original.secondaries = append(original.secondaries, spl) |
| } |
| |
| // Clone the error to all its related locations -- VS Code, at least, |
| // doesn't do it for us. |
| result = append(result, original) |
| for i, mainSecondary := range original.secondaries { |
| // Create the new primary error, with a tweaked message, in the |
| // secondary's location. We need to start from the secondary to |
| // capture its unexported location fields. |
| relocatedSecondary := mainSecondary |
| if supportsRelatedInformation { |
| relocatedSecondary.Msg = fmt.Sprintf("%v (see details)", original.primary.Msg) |
| } else { |
| relocatedSecondary.Msg = fmt.Sprintf("%v (this error: %v)", original.primary.Msg, mainSecondary.Msg) |
| } |
| relocatedSecondary.Soft = original.primary.Soft |
| |
| // Copy over the secondary errors, noting the location of the |
| // current error we're cloning. |
| clonedError := extendedError{primary: relocatedSecondary, secondaries: []types.Error{original.primary}} |
| for j, secondary := range original.secondaries { |
| if i == j { |
| secondary.Msg += " (this error)" |
| } |
| clonedError.secondaries = append(clonedError.secondaries, secondary) |
| } |
| result = append(result, clonedError) |
| } |
| |
| } |
| return result |
| } |
| |
| // An importFunc is an implementation of the single-method |
| // types.Importer interface based on a function value. |
| type importerFunc func(path string) (*types.Package, error) |
| |
| func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) } |