internal/lsp: propagate diagnostics for reverse dependencies
Prior to this change, if a package was rendered invalid by a change in one of its dependencies, diagnostics would not be propagated until the user typed in one of the package's files. Now, these updated diagnostics are sent along with the diagnostics for the dependency. Fixes golang/go#29817 Change-Id: I4761de31c4bdee820e024005f6112b3b3d2e1da6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/174977 Run-TryBot: Rebecca Stambler <rstambler@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Ian Cottrell <iancottrell@google.com>
This commit is contained in:
parent
a1c92a4ac6
commit
3eedecdc80
|
@ -57,31 +57,6 @@ func (v *View) parse(ctx context.Context, f *File) ([]packages.Error, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *View) cachePackage(ctx context.Context, pkg *Package) {
|
|
||||||
for _, file := range pkg.GetSyntax() {
|
|
||||||
// TODO: If a file is in multiple packages, which package do we store?
|
|
||||||
if !file.Pos().IsValid() {
|
|
||||||
v.Logger().Errorf(ctx, "invalid position for file %v", file.Name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tok := v.Config.Fset.File(file.Pos())
|
|
||||||
if tok == nil {
|
|
||||||
v.Logger().Errorf(ctx, "no token.File for %v", file.Name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fURI := span.FileURI(tok.Name())
|
|
||||||
f, err := v.getFile(fURI)
|
|
||||||
if err != nil {
|
|
||||||
v.Logger().Errorf(ctx, "no file: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
f.token = tok
|
|
||||||
f.ast = file
|
|
||||||
f.imports = f.ast.Imports
|
|
||||||
f.pkg = pkg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *View) checkMetadata(ctx context.Context, f *File) ([]packages.Error, error) {
|
func (v *View) checkMetadata(ctx context.Context, f *File) ([]packages.Error, error) {
|
||||||
if v.reparseImports(ctx, f, f.filename) {
|
if v.reparseImports(ctx, f, f.filename) {
|
||||||
cfg := v.Config
|
cfg := v.Config
|
||||||
|
@ -271,21 +246,56 @@ func (imp *importer) typeCheck(pkgPath string) (*Package, error) {
|
||||||
check.Files(pkg.syntax)
|
check.Files(pkg.syntax)
|
||||||
|
|
||||||
// Add every file in this package to our cache.
|
// Add every file in this package to our cache.
|
||||||
imp.view.cachePackage(imp.ctx, pkg)
|
imp.view.cachePackage(imp.ctx, pkg, meta)
|
||||||
|
|
||||||
|
return pkg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) cachePackage(ctx context.Context, pkg *Package, meta *metadata) {
|
||||||
|
for _, file := range pkg.GetSyntax() {
|
||||||
|
// TODO: If a file is in multiple packages, which package do we store?
|
||||||
|
if !file.Pos().IsValid() {
|
||||||
|
v.Logger().Errorf(ctx, "invalid position for file %v", file.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tok := v.Config.Fset.File(file.Pos())
|
||||||
|
if tok == nil {
|
||||||
|
v.Logger().Errorf(ctx, "no token.File for %v", file.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fURI := span.FileURI(tok.Name())
|
||||||
|
f, err := v.getFile(fURI)
|
||||||
|
if err != nil {
|
||||||
|
v.Logger().Errorf(ctx, "no file: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
f.token = tok
|
||||||
|
f.ast = file
|
||||||
|
f.imports = f.ast.Imports
|
||||||
|
f.pkg = pkg
|
||||||
|
}
|
||||||
|
|
||||||
|
v.pcache.mu.Lock()
|
||||||
|
defer v.pcache.mu.Unlock()
|
||||||
|
|
||||||
|
// Cache the entry for this package.
|
||||||
|
// All dependencies are cached through calls to *imp.Import.
|
||||||
|
e := &entry{
|
||||||
|
pkg: pkg,
|
||||||
|
err: nil,
|
||||||
|
ready: make(chan struct{}),
|
||||||
|
}
|
||||||
|
close(e.ready)
|
||||||
|
v.pcache.packages[pkg.pkgPath] = e
|
||||||
|
|
||||||
// Set imports of package to correspond to cached packages.
|
// Set imports of package to correspond to cached packages.
|
||||||
// We lock the package cache, but we shouldn't get any inconsistencies
|
// We lock the package cache, but we shouldn't get any inconsistencies
|
||||||
// because we are still holding the lock on the view.
|
// because we are still holding the lock on the view.
|
||||||
imp.view.pcache.mu.Lock()
|
|
||||||
defer imp.view.pcache.mu.Unlock()
|
|
||||||
|
|
||||||
for importPath := range meta.children {
|
for importPath := range meta.children {
|
||||||
if importEntry, ok := imp.view.pcache.packages[importPath]; ok {
|
if importEntry, ok := v.pcache.packages[importPath]; ok {
|
||||||
pkg.imports[importPath] = importEntry.pkg
|
pkg.imports[importPath] = importEntry.pkg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return pkg, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *View) appendPkgError(pkg *Package, err error) {
|
func (v *View) appendPkgError(pkg *Package, err error) {
|
||||||
|
|
|
@ -88,6 +88,7 @@ func (f *File) GetAST(ctx context.Context) *ast.File {
|
||||||
func (f *File) GetPackage(ctx context.Context) source.Package {
|
func (f *File) GetPackage(ctx context.Context) source.Package {
|
||||||
f.view.mu.Lock()
|
f.view.mu.Lock()
|
||||||
defer f.view.mu.Unlock()
|
defer f.view.mu.Unlock()
|
||||||
|
|
||||||
if f.pkg == nil || len(f.view.contentChanges) > 0 {
|
if f.pkg == nil || len(f.view.contentChanges) > 0 {
|
||||||
if errs, err := f.view.parse(ctx, f); err != nil {
|
if errs, err := f.view.parse(ctx, f); err != nil {
|
||||||
// Create diagnostics for errors if we are able to.
|
// Create diagnostics for errors if we are able to.
|
||||||
|
@ -134,3 +135,52 @@ func (f *File) read(ctx context.Context) {
|
||||||
func (f *File) isPopulated() bool {
|
func (f *File) isPopulated() bool {
|
||||||
return f.ast != nil && f.token != nil && f.pkg != nil && f.meta != nil && f.imports != nil
|
return f.ast != nil && f.token != nil && f.pkg != nil && f.meta != nil && f.imports != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *File) GetActiveReverseDeps(ctx context.Context) []source.File {
|
||||||
|
pkg := f.GetPackage(ctx)
|
||||||
|
if pkg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f.view.mu.Lock()
|
||||||
|
defer f.view.mu.Unlock()
|
||||||
|
|
||||||
|
f.view.mcache.mu.Lock()
|
||||||
|
defer f.view.mcache.mu.Unlock()
|
||||||
|
|
||||||
|
seen := make(map[string]struct{}) // visited packages
|
||||||
|
results := make(map[*File]struct{})
|
||||||
|
f.view.reverseDeps(ctx, seen, results, pkg.PkgPath())
|
||||||
|
|
||||||
|
files := make([]source.File, 0, len(results))
|
||||||
|
for rd := range results {
|
||||||
|
if rd == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Don't return any of the active file's in this package.
|
||||||
|
if rd.pkg != nil && rd.pkg == pkg {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
files = append(files, rd)
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) reverseDeps(ctx context.Context, seen map[string]struct{}, results map[*File]struct{}, pkgPath string) {
|
||||||
|
if _, ok := seen[pkgPath]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[pkgPath] = struct{}{}
|
||||||
|
m, ok := v.mcache.packages[pkgPath]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, filename := range m.files {
|
||||||
|
if f, err := v.getFile(span.FileURI(filename)); err == nil && f.active {
|
||||||
|
results[f] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for parentPkgPath := range m.parents {
|
||||||
|
v.reverseDeps(ctx, seen, results, parentPkgPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -124,6 +124,10 @@ func (pkg *Package) GetActionGraph(ctx context.Context, a *analysis.Analyzer) (*
|
||||||
return e.Action, nil
|
return e.Action, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pkg *Package) PkgPath() string {
|
||||||
|
return pkg.pkgPath
|
||||||
|
}
|
||||||
|
|
||||||
func (pkg *Package) GetFilenames() []string {
|
func (pkg *Package) GetFilenames() []string {
|
||||||
return pkg.files
|
return pkg.files
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,45 +18,48 @@ func (s *Server) cacheAndDiagnose(ctx context.Context, uri span.URI, content str
|
||||||
if err := view.SetContent(ctx, uri, []byte(content)); err != nil {
|
if err := view.SetContent(ctx, uri, []byte(content)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
ctx := view.BackgroundContext()
|
ctx := view.BackgroundContext()
|
||||||
if ctx.Err() != nil {
|
s.Diagnostics(ctx, view, uri)
|
||||||
s.log.Errorf(ctx, "canceling diagnostics for %s: %v", uri, ctx.Err())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
reports, err := source.Diagnostics(ctx, view, uri)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Errorf(ctx, "failed to compute diagnostics for %s: %v", uri, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.undeliveredMu.Lock()
|
|
||||||
defer s.undeliveredMu.Unlock()
|
|
||||||
|
|
||||||
for uri, diagnostics := range reports {
|
|
||||||
if err := s.publishDiagnostics(ctx, view, uri, diagnostics); err != nil {
|
|
||||||
if s.undelivered == nil {
|
|
||||||
s.undelivered = make(map[span.URI][]source.Diagnostic)
|
|
||||||
}
|
|
||||||
s.undelivered[uri] = diagnostics
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// In case we had old, undelivered diagnostics.
|
|
||||||
delete(s.undelivered, uri)
|
|
||||||
}
|
|
||||||
// Anytime we compute diagnostics, make sure to also send along any
|
|
||||||
// undelivered ones (only for remaining URIs).
|
|
||||||
for uri, diagnostics := range s.undelivered {
|
|
||||||
s.publishDiagnostics(ctx, view, uri, diagnostics)
|
|
||||||
|
|
||||||
// If we fail to deliver the same diagnostics twice, just give up.
|
|
||||||
delete(s.undelivered, uri)
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) Diagnostics(ctx context.Context, view *cache.View, uri span.URI) {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
s.log.Errorf(ctx, "canceling diagnostics for %s: %v", uri, ctx.Err())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reports, err := source.Diagnostics(ctx, view, uri)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Errorf(ctx, "failed to compute diagnostics for %s: %v", uri, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.undeliveredMu.Lock()
|
||||||
|
defer s.undeliveredMu.Unlock()
|
||||||
|
|
||||||
|
for uri, diagnostics := range reports {
|
||||||
|
if err := s.publishDiagnostics(ctx, view, uri, diagnostics); err != nil {
|
||||||
|
if s.undelivered == nil {
|
||||||
|
s.undelivered = make(map[span.URI][]source.Diagnostic)
|
||||||
|
}
|
||||||
|
s.undelivered[uri] = diagnostics
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// In case we had old, undelivered diagnostics.
|
||||||
|
delete(s.undelivered, uri)
|
||||||
|
}
|
||||||
|
// Anytime we compute diagnostics, make sure to also send along any
|
||||||
|
// undelivered ones (only for remaining URIs).
|
||||||
|
for uri, diagnostics := range s.undelivered {
|
||||||
|
s.publishDiagnostics(ctx, view, uri, diagnostics)
|
||||||
|
|
||||||
|
// If we fail to deliver the same diagnostics twice, just give up.
|
||||||
|
delete(s.undelivered, uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) publishDiagnostics(ctx context.Context, view *cache.View, uri span.URI, diagnostics []source.Diagnostic) error {
|
func (s *Server) publishDiagnostics(ctx context.Context, view *cache.View, uri span.URI, diagnostics []source.Diagnostic) error {
|
||||||
protocolDiagnostics, err := toProtocolDiagnostics(ctx, view, diagnostics)
|
protocolDiagnostics, err := toProtocolDiagnostics(ctx, view, diagnostics)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -64,6 +64,25 @@ func Diagnostics(ctx context.Context, v View, uri span.URI) (map[span.URI][]Diag
|
||||||
for _, filename := range pkg.GetFilenames() {
|
for _, filename := range pkg.GetFilenames() {
|
||||||
reports[span.FileURI(filename)] = []Diagnostic{}
|
reports[span.FileURI(filename)] = []Diagnostic{}
|
||||||
}
|
}
|
||||||
|
// Run diagnostics for the package that this URI belongs to.
|
||||||
|
if !diagnostics(ctx, v, pkg, reports) {
|
||||||
|
// If we don't have any list, parse, or type errors, run analyses.
|
||||||
|
if err := analyses(ctx, v, pkg, reports); err != nil {
|
||||||
|
return singleDiagnostic(uri, "failed to run analyses for %s", uri), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Updates to the diagnostics for this package may need to be propagated.
|
||||||
|
for _, f := range f.GetActiveReverseDeps(ctx) {
|
||||||
|
pkg := f.GetPackage(ctx)
|
||||||
|
for _, filename := range pkg.GetFilenames() {
|
||||||
|
reports[span.FileURI(filename)] = []Diagnostic{}
|
||||||
|
}
|
||||||
|
diagnostics(ctx, v, pkg, reports)
|
||||||
|
}
|
||||||
|
return reports, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func diagnostics(ctx context.Context, v View, pkg Package, reports map[span.URI][]Diagnostic) bool {
|
||||||
var listErrors, parseErrors, typeErrors []packages.Error
|
var listErrors, parseErrors, typeErrors []packages.Error
|
||||||
for _, err := range pkg.GetErrors() {
|
for _, err := range pkg.GetErrors() {
|
||||||
switch err.Kind {
|
switch err.Kind {
|
||||||
|
@ -97,9 +116,11 @@ func Diagnostics(ctx context.Context, v View, uri span.URI) (map[span.URI][]Diag
|
||||||
reports[spn.URI()] = append(reports[spn.URI()], diagnostic)
|
reports[spn.URI()] = append(reports[spn.URI()], diagnostic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(diags) > 0 {
|
// Returns true if we've sent non-empty diagnostics.
|
||||||
return reports, nil
|
return len(diags) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func analyses(ctx context.Context, v View, pkg Package, reports map[span.URI][]Diagnostic) error {
|
||||||
// Type checking and parsing succeeded. Run analyses.
|
// Type checking and parsing succeeded. Run analyses.
|
||||||
if err := runAnalyses(ctx, v, pkg, func(a *analysis.Analyzer, diag analysis.Diagnostic) error {
|
if err := runAnalyses(ctx, v, pkg, func(a *analysis.Analyzer, diag analysis.Diagnostic) error {
|
||||||
r := span.NewRange(v.FileSet(), diag.Pos, 0)
|
r := span.NewRange(v.FileSet(), diag.Pos, 0)
|
||||||
|
@ -120,10 +141,9 @@ func Diagnostics(ctx context.Context, v View, uri span.URI) (map[span.URI][]Diag
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return singleDiagnostic(uri, "unable to run analyses for %s: %v", uri, err), nil
|
return err
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
return reports, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func pointToSpan(ctx context.Context, v View, spn span.Span) span.Span {
|
func pointToSpan(ctx context.Context, v View, spn span.Span) span.Span {
|
||||||
|
|
|
@ -41,11 +41,16 @@ type File interface {
|
||||||
GetPackage(ctx context.Context) Package
|
GetPackage(ctx context.Context) Package
|
||||||
GetToken(ctx context.Context) *token.File
|
GetToken(ctx context.Context) *token.File
|
||||||
GetContent(ctx context.Context) []byte
|
GetContent(ctx context.Context) []byte
|
||||||
|
|
||||||
|
// GetActiveReverseDeps returns the active files belonging to the reverse
|
||||||
|
// dependencies of this file's package.
|
||||||
|
GetActiveReverseDeps(ctx context.Context) []File
|
||||||
}
|
}
|
||||||
|
|
||||||
// Package represents a Go package that has been type-checked. It maintains
|
// Package represents a Go package that has been type-checked. It maintains
|
||||||
// only the relevant fields of a *go/packages.Package.
|
// only the relevant fields of a *go/packages.Package.
|
||||||
type Package interface {
|
type Package interface {
|
||||||
|
PkgPath() string
|
||||||
GetFilenames() []string
|
GetFilenames() []string
|
||||||
GetSyntax() []*ast.File
|
GetSyntax() []*ast.File
|
||||||
GetErrors() []packages.Error
|
GetErrors() []packages.Error
|
||||||
|
|
Loading…
Reference in New Issue