1
0
mirror of https://github.com/golang/go synced 2024-09-30 14:38:33 -06:00

internal/lsp: don't publish duplicate diagnostics in one report

CL 242579 changed the mechanism of reporting diagnostics so that we now
combine reports from multiple sources. Previously, we overwrote existing
diagnostics if new ones were reported. This was fine because
source.Diagnostics generated all of the diagnostics for Go files, and
mod.Diagnostics generated all of the diagnostics for go.mod files.

Now, we combine diagnostics from both sources -- mod.Diagnostics can
generate reports for Go files. We may get duplicate reports for packages
with test variants, so now, we check for those and dedupe.

Change-Id: I42e98079b4eead380058dd029a3a0c72a1796ebb
Reviewed-on: https://go-review.googlesource.com/c/tools/+/243778
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
This commit is contained in:
Rebecca Stambler 2020-07-21 02:05:22 -04:00
parent 188b38280e
commit 80cb79702c
3 changed files with 79 additions and 27 deletions

View File

@ -6,6 +6,7 @@ package lsp
import (
"context"
"crypto/sha1"
"fmt"
"strings"
"sync"
@ -19,7 +20,9 @@ import (
"golang.org/x/xerrors"
)
type diagnosticKey struct {
// idWithAnalysis is used to track if the diagnostics for a given file were
// computed with analyses.
type idWithAnalysis struct {
id source.FileIdentity
withAnalysis bool
}
@ -45,7 +48,7 @@ func (s *Server) diagnoseSnapshot(snapshot source.Snapshot) {
// diagnose is a helper function for running diagnostics with a given context.
// Do not call it directly.
func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, alwaysAnalyze bool) (map[diagnosticKey][]*source.Diagnostic, *protocol.ShowMessageParams) {
func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, alwaysAnalyze bool) (map[idWithAnalysis]map[string]*source.Diagnostic, *protocol.ShowMessageParams) {
ctx, done := event.Start(ctx, "lsp:background-worker")
defer done()
@ -57,28 +60,32 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, alwaysA
}
defer func() { <-s.diagnosticsSema }()
allReports := make(map[diagnosticKey][]*source.Diagnostic)
var reportsMu sync.Mutex
var wg sync.WaitGroup
reports := map[idWithAnalysis]map[string]*source.Diagnostic{}
// Diagnose the go.mod file.
reports, modErr := mod.Diagnostics(ctx, snapshot)
// First, diagnose the go.mod file.
modReports, modErr := mod.Diagnostics(ctx, snapshot)
if ctx.Err() != nil {
return nil, nil
}
if modErr != nil {
event.Error(ctx, "warning: diagnose go.mod", modErr, tag.Directory.Of(snapshot.View().Folder().Filename()))
}
for id, diags := range reports {
for id, diags := range modReports {
if id.URI == "" {
event.Error(ctx, "missing URI for module diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename()))
continue
}
key := diagnosticKey{
key := idWithAnalysis{
id: id,
withAnalysis: true, // treat go.mod diagnostics like analyses
}
allReports[key] = diags
if _, ok := reports[key]; !ok {
reports[key] = map[string]*source.Diagnostic{}
}
for _, d := range diags {
reports[key][diagnosticKey(d)] = d
}
}
// Diagnose all of the packages in the workspace.
@ -99,23 +106,30 @@ If you believe this is a mistake, please file an issue: https://github.com/golan
}
return nil, nil
}
var shows *protocol.ShowMessageParams
var (
showMsg *protocol.ShowMessageParams
wg sync.WaitGroup
)
for _, ph := range wsPackages {
wg.Add(1)
go func(ph source.PackageHandle) {
defer wg.Done()
// Only run analyses for packages with open files.
withAnalyses := alwaysAnalyze
withAnalysis := alwaysAnalyze
for _, pgh := range ph.CompiledGoFiles() {
if snapshot.IsOpen(pgh.File().URI()) {
withAnalyses = true
withAnalysis = true
break
}
}
reports, warn, err := source.Diagnostics(ctx, snapshot, ph, withAnalyses)
pkgReports, warn, err := source.Diagnostics(ctx, snapshot, ph, withAnalysis)
// Check if might want to warn the user about their build configuration.
// Our caller decides whether to send the message.
if warn && !snapshot.View().ValidBuildConfiguration() {
shows = &protocol.ShowMessageParams{
showMsg = &protocol.ShowMessageParams{
Type: protocol.Warning,
Message: `You are neither in a module nor in your GOPATH. If you are using modules, please open your editor at the directory containing the go.mod. If you believe this warning is incorrect, please file an issue: https://github.com/golang/go/issues/new.`,
}
@ -124,22 +138,44 @@ If you believe this is a mistake, please file an issue: https://github.com/golan
event.Error(ctx, "warning: diagnose package", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(ph.ID()))
return
}
// Add all reports to the global map, checking for duplciates.
reportsMu.Lock()
for id, diags := range reports {
key := diagnosticKey{
for id, diags := range pkgReports {
key := idWithAnalysis{
id: id,
withAnalysis: withAnalyses,
withAnalysis: withAnalysis,
}
if _, ok := reports[key]; !ok {
reports[key] = map[string]*source.Diagnostic{}
}
for _, d := range diags {
reports[key][diagnosticKey(d)] = d
}
allReports[key] = append(allReports[key], diags...)
}
reportsMu.Unlock()
}(ph)
}
wg.Wait()
return allReports, shows
return reports, showMsg
}
func (s *Server) publishReports(ctx context.Context, snapshot source.Snapshot, reports map[diagnosticKey][]*source.Diagnostic) {
// diagnosticKey creates a unique identifier for a given diagnostic, since we
// cannot use source.Diagnostics as map keys. This is used to de-duplicate
// diagnostics.
func diagnosticKey(d *source.Diagnostic) string {
var tags, related string
for _, t := range d.Tags {
tags += fmt.Sprintf("%s", t)
}
for _, r := range d.Related {
related += fmt.Sprintf("%s%s%s", r.URI, r.Message, r.Range)
}
key := fmt.Sprintf("%s%s%s%s%s%s", d.Message, d.Range, d.Severity, d.Source, tags, related)
return fmt.Sprintf("%x", sha1.Sum([]byte(key)))
}
func (s *Server) publishReports(ctx context.Context, snapshot source.Snapshot, reports map[idWithAnalysis]map[string]*source.Diagnostic) {
// Check for context cancellation before publishing diagnostics.
if ctx.Err() != nil {
return
@ -148,12 +184,16 @@ func (s *Server) publishReports(ctx context.Context, snapshot source.Snapshot, r
s.deliveredMu.Lock()
defer s.deliveredMu.Unlock()
for key, diagnostics := range reports {
for key, diagnosticsMap := range reports {
// Don't deliver diagnostics if the context has already been canceled.
if ctx.Err() != nil {
break
}
// Pre-sort diagnostics to avoid extra work when we compare them.
var diagnostics []*source.Diagnostic
for _, d := range diagnosticsMap {
diagnostics = append(diagnostics, d)
}
source.SortDiagnostics(diagnostics)
toSend := sentDiagnostics{
version: key.id.Version,
@ -295,8 +335,8 @@ See https://github.com/golang/go/issues/39164 for more detail on this issue.`,
if err != nil {
return false
}
s.publishReports(ctx, snapshot, map[diagnosticKey][]*source.Diagnostic{
{id: fh.Identity()}: {diag},
s.publishReports(ctx, snapshot, map[idWithAnalysis]map[string]*source.Diagnostic{
{id: fh.Identity()}: {diagnosticKey(diag): diag},
})
return true
}

View File

@ -38,7 +38,7 @@ func TestLSP(t *testing.T) {
type runner struct {
server *Server
data *tests.Data
diagnostics map[span.URI][]*source.Diagnostic
diagnostics map[span.URI]map[string]*source.Diagnostic
ctx context.Context
}
@ -118,7 +118,7 @@ func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens)
func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnostic) {
// Get the diagnostics for this view if we have not done it before.
if r.diagnostics == nil {
r.diagnostics = make(map[span.URI][]*source.Diagnostic)
r.diagnostics = make(map[span.URI]map[string]*source.Diagnostic)
v := r.server.session.View(r.data.Config.Dir)
// Always run diagnostics with analysis.
reports, _ := r.server.diagnose(r.ctx, v.Snapshot(), true)
@ -126,7 +126,10 @@ func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnost
r.diagnostics[key.id.URI] = diags
}
}
got := r.diagnostics[uri]
var got []*source.Diagnostic
for _, d := range r.diagnostics[uri] {
got = append(got, d)
}
// A special case to test that there are no diagnostics for a file.
if len(want) == 1 && want[0].Source == "no_diagnostics" {
if len(got) != 0 {
@ -380,7 +383,7 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string)
}
// Get the diagnostics for this view if we have not done it before.
if r.diagnostics == nil {
r.diagnostics = make(map[span.URI][]*source.Diagnostic)
r.diagnostics = make(map[span.URI]map[string]*source.Diagnostic)
// Always run diagnostics with analysis.
reports, _ := r.server.diagnose(r.ctx, view.Snapshot(), true)
for key, diags := range reports {

View File

@ -258,8 +258,17 @@ func Hello() {
env.SaveBuffer("go.mod")
env.Await(
EmptyDiagnostics("main.go"),
)
metBy := env.Await(
env.DiagnosticAtRegexp("bob/bob.go", "x"),
)
d, ok := metBy[0].(*protocol.PublishDiagnosticsParams)
if !ok {
t.Fatalf("unexpected met by result %v (%T)", metBy, metBy)
}
if len(d.Diagnostics) != 1 {
t.Fatalf("expected 1 diagnostic, got %v", len(d.Diagnostics))
}
})
})
t.Run("initialized", func(t *testing.T) {