1
0
mirror of https://github.com/golang/go synced 2024-10-01 08:18:32 -06:00
go/internal/lsp/cache/load.go
Muir Manders d73e1c7e25 internal/lsp: fix errors when adding new file to existing package
Previously when you added a new file to an existing package, the new
file would get stuck with the "no package for file" error until you
saved the file and then made changed a different file in the
package. There were two changes required to fix the errors:

First, we need to invalidate the package cache when a new file is
added to a package so that the package will actually re-parse and
re-type check. We now notice if file names changed when updating a
package's metadata and invalidate the package cache accordingly.

Second, when dealing with overlay (unsaved) files, we need to map
the *goFile to the package even if we fail to parse the
file (e.g. the new file fails to parse when it is empty). If we don't
map it to the package, the package won't get refreshed as the file is
changed.

Fixes golang/go#32341

Change-Id: I1a728fbedc79da7d5fe69554a5893efcd1e1d902
GitHub-Last-Rev: e7c3d4c1f8f73b12c87ee76d868cc04893e55808
GitHub-Pull-Request: golang/tools#111
Reviewed-on: https://go-review.googlesource.com/c/tools/+/181417
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2019-06-11 22:22:05 +00:00

217 lines
6.0 KiB
Go

package cache
import (
"context"
"fmt"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
)
func (v *view) loadParseTypecheck(ctx context.Context, f *goFile) ([]packages.Error, error) {
v.mcache.mu.Lock()
defer v.mcache.mu.Unlock()
// If the AST for this file is trimmed, and we are explicitly type-checking it,
// don't ignore function bodies.
if f.astIsTrimmed() {
f.invalidateAST()
}
// Save the metadata's current missing imports, if any.
var originalMissingImports map[packagePath]struct{}
if f.meta != nil {
originalMissingImports = f.meta.missingImports
}
// Check if we need to run go/packages.Load for this file's package.
if errs, err := v.checkMetadata(ctx, f); err != nil {
return errs, err
}
// If `go list` failed to get data for the file in question (this should never happen).
if f.meta == nil {
return nil, fmt.Errorf("loadParseTypecheck: no metadata found for %v", f.filename())
}
// If we have already seen these missing imports before, and we still have type information,
// there is no need to continue.
if sameSet(originalMissingImports, f.meta.missingImports) && f.pkg != nil {
return nil, nil
}
imp := &importer{
view: v,
seen: make(map[packagePath]struct{}),
ctx: ctx,
fset: f.FileSet(),
topLevelPkgID: f.meta.id,
}
// Start prefetching direct imports.
for importPath := range f.meta.children {
go imp.Import(string(importPath))
}
// Type-check package.
pkg, err := imp.getPkg(f.meta.pkgPath)
if err != nil {
return nil, err
}
if pkg == nil || pkg.IsIllTyped() {
return nil, fmt.Errorf("loadParseTypecheck: %s is ill typed", f.meta.pkgPath)
}
// If we still have not found the package for the file, something is wrong.
if f.pkg == nil {
return nil, fmt.Errorf("loadParseTypeCheck: no package found for %v", f.filename())
}
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
}
// checkMetadata determines if we should run go/packages.Load for this file.
// If yes, update the metadata for the file and its package.
func (v *view) checkMetadata(ctx context.Context, f *goFile) ([]packages.Error, error) {
if !v.parseImports(ctx, f) {
return nil, nil
}
pkgs, err := packages.Load(v.buildConfig(), fmt.Sprintf("file=%s", f.filename()))
if len(pkgs) == 0 {
if err == nil {
err = fmt.Errorf("no packages found for %s", f.filename())
}
// Return this error as a diagnostic to the user.
return []packages.Error{
{
Msg: err.Error(),
Kind: packages.ListError,
},
}, err
}
for _, pkg := range pkgs {
// If the package comes back with errors from `go list`,
// don't bother type-checking it.
if len(pkg.Errors) > 0 {
return pkg.Errors, fmt.Errorf("package %s has errors, skipping type-checking", pkg.PkgPath)
}
// Build the import graph for this package.
v.link(ctx, packagePath(pkg.PkgPath), pkg, nil)
}
return nil, nil
}
// reparseImports reparses a file's package and import declarations to
// determine if they have changed.
func (v *view) parseImports(ctx context.Context, f *goFile) bool {
if f.meta == nil || len(f.meta.missingImports) > 0 {
return true
}
// Get file content in case we don't already have it.
parsed, _ := v.session.cache.ParseGo(f.Handle(ctx), source.ParseHeader).Parse(ctx)
if parsed == nil {
return true
}
// If the package name has changed, re-run `go list`.
if f.meta.name != parsed.Name.Name {
return true
}
// If the package's imports have changed, re-run `go list`.
if len(f.imports) != len(parsed.Imports) {
return true
}
for i, importSpec := range f.imports {
if importSpec.Path.Value != parsed.Imports[i].Path.Value {
return true
}
}
return false
}
func (v *view) link(ctx context.Context, pkgPath packagePath, pkg *packages.Package, parent *metadata) *metadata {
m, ok := v.mcache.packages[pkgPath]
// If a file was added or deleted we need to invalidate the package cache
// so relevant packages get parsed and type checked again.
if ok && !filenamesIdentical(m.files, pkg.CompiledGoFiles) {
v.invalidatePackage(pkgPath)
}
if !ok {
m = &metadata{
pkgPath: pkgPath,
id: packageID(pkg.ID),
typesSizes: pkg.TypesSizes,
parents: make(map[packagePath]bool),
children: make(map[packagePath]bool),
missingImports: make(map[packagePath]struct{}),
}
v.mcache.packages[pkgPath] = m
}
// Reset any field that could have changed across calls to packages.Load.
m.name = pkg.Name
m.files = pkg.CompiledGoFiles
for _, filename := range m.files {
if f, _ := v.getFile(span.FileURI(filename)); f != nil {
if gof, ok := f.(*goFile); ok {
gof.meta = m
} else {
v.Session().Logger().Errorf(ctx, "not a Go file: %s", f.URI())
}
}
}
// Connect the import graph.
if parent != nil {
m.parents[parent.pkgPath] = true
parent.children[pkgPath] = true
}
for importPath, importPkg := range pkg.Imports {
if len(importPkg.Errors) > 0 {
m.missingImports[pkgPath] = struct{}{}
}
importPkgPath := packagePath(importPath)
if _, ok := m.children[importPkgPath]; !ok {
v.link(ctx, importPkgPath, importPkg, m)
}
}
// Clear out any imports that have been removed.
for importPath := range m.children {
if _, ok := pkg.Imports[string(importPath)]; !ok {
delete(m.children, importPath)
if child, ok := v.mcache.packages[importPath]; ok {
delete(child.parents, pkgPath)
}
}
}
return m
}
// filenamesIdentical reports whether two sets of file names are identical.
func filenamesIdentical(oldFiles, newFiles []string) bool {
if len(oldFiles) != len(newFiles) {
return false
}
oldByName := make(map[string]struct{}, len(oldFiles))
for _, filename := range oldFiles {
oldByName[filename] = struct{}{}
}
for _, newFilename := range newFiles {
if _, found := oldByName[newFilename]; !found {
return false
}
delete(oldByName, newFilename)
}
return len(oldByName) == 0
}