internal/lsp: move the missing imports handling into the metadata
This change uses the missing imports detection to return diagnostics and
warning messages to the user.
Updates golang/go#34484
Change-Id: If7bb7e702b8bdbd7e1ad5e26f93acc50d16209b0
Reviewed-on: https://go-review.googlesource.com/c/tools/+/196985
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go
index f1b94f1..2f80208 100644
--- a/internal/lsp/cache/check.go
+++ b/internal/lsp/cache/check.go
@@ -158,6 +158,14 @@
return string(cph.m.id)
}
+func (cph *checkPackageHandle) MissingDependencies() []string {
+ var md []string
+ for i := range cph.m.missingDeps {
+ md = append(md, string(i))
+ }
+ return md
+}
+
func (cph *checkPackageHandle) Cached(ctx context.Context) (source.Package, error) {
return cph.cached(ctx)
}
diff --git a/internal/lsp/cache/gofile.go b/internal/lsp/cache/gofile.go
index a08cb19..1102cec 100644
--- a/internal/lsp/cache/gofile.go
+++ b/internal/lsp/cache/gofile.go
@@ -23,10 +23,6 @@
// which can be modified during type-checking.
mu sync.Mutex
- // missingImports is the set of unresolved imports for this package.
- // It contains any packages with `go list` errors.
- missingImports map[packagePath]struct{}
-
imports []*ast.ImportSpec
}
@@ -35,21 +31,47 @@
mode source.ParseMode
}
-func (f *goFile) CheckPackageHandles(ctx context.Context) (cphs []source.CheckPackageHandle, err error) {
+func (f *goFile) CheckPackageHandles(ctx context.Context) (results []source.CheckPackageHandle, err error) {
ctx = telemetry.File.With(ctx, f.URI())
fh := f.Handle(ctx)
- cphs = f.isDirty(ctx, fh)
- if len(cphs) == 0 {
- cphs, err = f.view.loadParseTypecheck(ctx, f, fh)
+ var dirty bool
+ meta, pkgs := f.view.getSnapshot(f.URI())
+ if len(meta) == 0 {
+ dirty = true
+ }
+ for _, m := range meta {
+ if len(m.missingDeps) != 0 {
+ dirty = true
+ }
+ }
+ for _, cph := range pkgs {
+ // If we're explicitly checking if a file needs to be type-checked,
+ // we need it to be fully parsed.
+ if cph.mode() != source.ParseFull {
+ continue
+ }
+ // Check if there is a fully-parsed package to which this file belongs.
+ for _, file := range cph.Files() {
+ if file.File().Identity() == fh.Identity() {
+ results = append(results, cph)
+ }
+ }
+ }
+ if dirty || len(results) == 0 {
+ cphs, err := f.view.loadParseTypecheck(ctx, f, fh)
if err != nil {
return nil, err
}
+ if cphs == nil {
+ return results, nil
+ }
+ results = cphs
}
- if len(cphs) == 0 {
+ if len(results) == 0 {
return nil, errors.Errorf("no CheckPackageHandles for %s", f.URI())
}
- return cphs, nil
+ return results, nil
}
func (v *view) GetActiveReverseDeps(ctx context.Context, uri span.URI) (results []source.CheckPackageHandle) {
@@ -78,27 +100,3 @@
}
return results
}
-
-// isDirty is true if the file needs to be type-checked.
-// It assumes that the file's view's mutex is held by the caller.
-func (f *goFile) isDirty(ctx context.Context, fh source.FileHandle) []source.CheckPackageHandle {
- meta, cphs := f.view.getSnapshot(f.URI())
- if len(meta) == 0 {
- return nil
- }
- var results []source.CheckPackageHandle
- for key, cph := range cphs {
- // If we're explicitly checking if a file needs to be type-checked,
- // we need it to be fully parsed.
- if key.mode != source.ParseFull {
- continue
- }
- // Check if there is a fully-parsed package to which this file belongs.
- for _, file := range cph.Files() {
- if file.File().Identity() == fh.Identity() {
- results = append(results, cph)
- }
- }
- }
- return results
-}
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index d70e091..9d99eff 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -27,6 +27,11 @@
if err != nil {
return nil, err
}
+ // If load has explicitly returns nil metadata and no error,
+ // it means that we should not re-typecheck the packages.
+ if meta == nil {
+ return nil, nil
+ }
var (
cphs []*checkPackageHandle
results []source.CheckPackageHandle
@@ -129,7 +134,15 @@
// Return this error as a diagnostic to the user.
return nil, err
}
- return v.updateMetadata(ctx, f.URI(), pkgs)
+ m, prevMissingImports, err := v.updateMetadata(ctx, f.URI(), pkgs)
+ if err != nil {
+ return nil, err
+ }
+ meta, err := validateMetadata(ctx, m, prevMissingImports)
+ if err != nil {
+ return nil, err
+ }
+ return meta, nil
}
// shouldRunGopackages reparses a file's package and import declarations to
@@ -161,3 +174,36 @@
}
return false
}
+
+func validateMetadata(ctx context.Context, metadata []*metadata, prevMissingImports map[packageID]map[packagePath]struct{}) ([]*metadata, error) {
+ // If we saw incorrect metadata for this package previously, don't both rechecking it.
+ for _, m := range metadata {
+ if len(m.missingDeps) > 0 {
+ prev, ok := prevMissingImports[m.id]
+ // There are missing imports that we previously hadn't seen before.
+ if !ok {
+ return metadata, nil
+ }
+ // The set of missing imports has changed.
+ if !sameSet(prev, m.missingDeps) {
+ return metadata, nil
+ }
+ } else {
+ // There are no missing imports.
+ return metadata, nil
+ }
+ }
+ return nil, nil
+}
+
+func sameSet(x, y map[packagePath]struct{}) bool {
+ if len(x) != len(y) {
+ return false
+ }
+ for k := range x {
+ if _, ok := y[k]; !ok {
+ return false
+ }
+ }
+ return true
+}
diff --git a/internal/lsp/cache/pkg.go b/internal/lsp/cache/pkg.go
index 5cf1660..e0fc01a 100644
--- a/internal/lsp/cache/pkg.go
+++ b/internal/lsp/cache/pkg.go
@@ -24,9 +24,8 @@
view *view
// ID and package path have their own types to avoid being used interchangeably.
- id packageID
- pkgPath packagePath
-
+ id packageID
+ pkgPath packagePath
files []source.ParseGoHandle
errors []packages.Error
imports map[packagePath]*pkg
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 38312ec..a998fa7 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -20,17 +20,18 @@
}
type metadata struct {
- id packageID
- pkgPath packagePath
- name string
- files []span.URI
- typesSizes types.Sizes
- parents map[packageID]bool
- children map[packageID]*metadata
- errors []packages.Error
+ id packageID
+ pkgPath packagePath
+ name string
+ files []span.URI
+ typesSizes types.Sizes
+ parents map[packageID]bool
+ children map[packageID]*metadata
+ errors []packages.Error
+ missingDeps map[packagePath]struct{}
}
-func (v *view) getSnapshot(uri span.URI) ([]*metadata, map[packageKey]*checkPackageHandle) {
+func (v *view) getSnapshot(uri span.URI) ([]*metadata, []*checkPackageHandle) {
v.snapshotMu.Lock()
defer v.snapshotMu.Unlock()
@@ -38,7 +39,11 @@
for _, id := range v.snapshot.ids[uri] {
m = append(m, v.snapshot.metadata[id])
}
- return m, v.snapshot.packages[uri]
+ var cphs []*checkPackageHandle
+ for _, cph := range v.snapshot.packages[uri] {
+ cphs = append(cphs, cph)
+ }
+ return m, cphs
}
func (v *view) getMetadata(uri span.URI) []*metadata {
@@ -59,11 +64,17 @@
return v.snapshot.packages[uri]
}
-func (v *view) updateMetadata(ctx context.Context, uri span.URI, pkgs []*packages.Package) ([]*metadata, error) {
+func (v *view) updateMetadata(ctx context.Context, uri span.URI, pkgs []*packages.Package) ([]*metadata, map[packageID]map[packagePath]struct{}, error) {
v.snapshotMu.Lock()
defer v.snapshotMu.Unlock()
// Clear metadata since we are re-running go/packages.
+ prevMissingImports := make(map[packageID]map[packagePath]struct{})
+ for _, id := range v.snapshot.ids[uri] {
+ if m, ok := v.snapshot.metadata[id]; ok && len(m.missingDeps) > 0 {
+ prevMissingImports[id] = m.missingDeps
+ }
+ }
without := make(map[span.URI]struct{})
for _, id := range v.snapshot.ids[uri] {
v.remove(id, without, map[packageID]struct{}{})
@@ -80,11 +91,14 @@
pkg: pkg,
parent: nil,
}); err != nil {
- return nil, err
+ return nil, nil, err
}
results = append(results, v.snapshot.metadata[packageID(pkg.ID)])
}
- return results, nil
+ if len(results) == 0 {
+ return nil, nil, errors.Errorf("no metadata for %s", uri)
+ }
+ return results, prevMissingImports, nil
}
type importGraph struct {
@@ -134,6 +148,10 @@
}
// Don't remember any imports with significant errors.
if importPkgPath != "unsafe" && len(importPkg.CompiledGoFiles) == 0 {
+ if m.missingDeps == nil {
+ m.missingDeps = make(map[packagePath]struct{})
+ }
+ m.missingDeps[importPkgPath] = struct{}{}
continue
}
if _, ok := m.children[packageID(importPkg.ID)]; !ok {
diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go
index f6a42b7..5176ca5 100644
--- a/internal/lsp/source/diagnostics.go
+++ b/internal/lsp/source/diagnostics.go
@@ -47,6 +47,16 @@
return nil, "", err
}
cph := WidestCheckPackageHandle(cphs)
+
+ // If we are missing dependencies, it may because the user's workspace is
+ // not correctly configured. Report errors, if possible.
+ var warningMsg string
+ if len(cph.MissingDependencies()) > 0 {
+ warningMsg, err = checkCommonErrors(ctx, view, f.URI())
+ if err != nil {
+ log.Error(ctx, "error checking common errors", err, telemetry.File.Of(f.URI))
+ }
+ }
pkg, err := cph.Check(ctx)
if err != nil {
log.Error(ctx, "no package for file", err)
@@ -59,15 +69,6 @@
clearReports(view, reports, fh.File().Identity().URI)
}
- // If we have `go list` errors, we may want to offer a warning message to the user.
- var warningMsg string
- if hasListErrors(pkg.GetErrors()) {
- warningMsg, err = checkCommonErrors(ctx, view, f.URI())
- if err != nil {
- log.Error(ctx, "error checking common errors", err, telemetry.File.Of(f.URI))
- }
- }
-
// Prepare any additional reports for the errors in this package.
for _, err := range pkg.GetErrors() {
if err.Kind != packages.ListError {
diff --git a/internal/lsp/source/errors.go b/internal/lsp/source/errors.go
index 9b740fe..634e287 100644
--- a/internal/lsp/source/errors.go
+++ b/internal/lsp/source/errors.go
@@ -12,7 +12,20 @@
"golang.org/x/tools/internal/span"
)
+const (
+ // TODO(rstambler): We should really be able to point to a link on the website.
+ modulesWiki = "https://github.com/golang/go/wiki/Modules"
+)
+
func checkCommonErrors(ctx context.Context, view View, uri span.URI) (string, error) {
+ // Unfortunately, we probably can't have go/packages expose a function like this.
+ // Since we only really understand the `go` command, check the user's GOPACKAGESDRIVER
+ // and, if they are using `go list`, consider the possible error cases.
+ gopackagesdriver := os.Getenv("GOPACKAGESDRIVER")
+ if gopackagesdriver != "" && gopackagesdriver != "off" {
+ return "", nil
+ }
+
// Some cases we should be able to detect:
//
// 1. The user is in GOPATH mode and is working outside their GOPATH
@@ -49,7 +62,7 @@
msg = "You are in module mode, but you are not inside of a module. Please create a module."
}
} else {
- msg = "You are neither in a module nor in your GOPATH. Please see X for information on how to set up your Go project."
+ msg = fmt.Sprintf("You are neither in a module nor in your GOPATH. Please see %s for information on how to set up your Go project.", modulesWiki)
}
return msg, nil
}
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 2019186..77556af 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -122,6 +122,8 @@
// Cached returns the Package for the CheckPackageHandle if it has already been stored.
Cached(ctx context.Context) (Package, error)
+
+ MissingDependencies() []string
}
// Cache abstracts the core logic of dealing with the environment from the