internal/lsp: use version numbers in diagnostic messages

This change uses the FileIdentity when reporting an error message, so
that the version number can be propagated to through the
publishDiagnostics notification.

Change-Id: I6a2103e304717ca09895008ea40336e3ace3c66d
Reviewed-on: https://go-review.googlesource.com/c/tools/+/208260
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/internal/lsp/cache/errors.go b/internal/lsp/cache/errors.go
index 5c377af..6fdee56 100644
--- a/internal/lsp/cache/errors.go
+++ b/internal/lsp/cache/errors.go
@@ -87,8 +87,12 @@
 	if err != nil {
 		return nil, err
 	}
+	ph, err := pkg.File(spn.URI())
+	if err != nil {
+		return nil, err
+	}
 	return &source.Error{
-		URI:            spn.URI(),
+		File:           ph.File().Identity(),
 		Range:          rng,
 		Message:        msg,
 		Kind:           kind,
diff --git a/internal/lsp/cmd/test/check.go b/internal/lsp/cmd/test/check.go
index ad260d2..74b27cb 100644
--- a/internal/lsp/cmd/test/check.go
+++ b/internal/lsp/cmd/test/check.go
@@ -51,12 +51,12 @@
 		got[l] = struct{}{}
 	}
 	for _, diag := range want {
-		expect := fmt.Sprintf("%v:%v:%v: %v", diag.URI.Filename(), diag.Range.Start.Line+1, diag.Range.Start.Character+1, diag.Message)
+		expect := fmt.Sprintf("%v:%v:%v: %v", diag.File.URI.Filename(), diag.Range.Start.Line+1, diag.Range.Start.Character+1, diag.Message)
 		if diag.Range.Start.Character == 0 {
-			expect = fmt.Sprintf("%v:%v: %v", diag.URI.Filename(), diag.Range.Start.Line+1, diag.Message)
+			expect = fmt.Sprintf("%v:%v: %v", diag.File.URI.Filename(), diag.Range.Start.Line+1, diag.Message)
 		}
 		// Skip the badimport test for now, until we do a better job with diagnostic ranges.
-		if strings.Contains(diag.URI.Filename(), "badimport") {
+		if strings.Contains(diag.File.URI.Filename(), "badimport") {
 			continue
 		}
 		_, found := got[expect]
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index a77b490..8334d6d 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -55,18 +55,18 @@
 	s.undeliveredMu.Lock()
 	defer s.undeliveredMu.Unlock()
 
-	for uri, diagnostics := range reports {
-		if err := s.publishDiagnostics(ctx, uri, diagnostics); err != nil {
+	for fileID, diagnostics := range reports {
+		if err := s.publishDiagnostics(ctx, fileID, diagnostics); err != nil {
 			if s.undelivered == nil {
-				s.undelivered = make(map[span.URI][]source.Diagnostic)
+				s.undelivered = make(map[source.FileIdentity][]source.Diagnostic)
 			}
-			s.undelivered[uri] = diagnostics
+			s.undelivered[fileID] = diagnostics
 
 			log.Error(ctx, "failed to deliver diagnostic (will retry)", err, telemetry.File)
 			continue
 		}
 		// In case we had old, undelivered diagnostics.
-		delete(s.undelivered, uri)
+		delete(s.undelivered, fileID)
 	}
 	// Anytime we compute diagnostics, make sure to also send along any
 	// undelivered ones (only for remaining URIs).
@@ -81,10 +81,11 @@
 	return nil
 }
 
-func (s *Server) publishDiagnostics(ctx context.Context, uri span.URI, diagnostics []source.Diagnostic) error {
+func (s *Server) publishDiagnostics(ctx context.Context, fileID source.FileIdentity, diagnostics []source.Diagnostic) error {
 	s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
 		Diagnostics: toProtocolDiagnostics(ctx, diagnostics),
-		URI:         protocol.NewURI(uri),
+		URI:         protocol.NewURI(fileID.URI),
+		Version:     fileID.Version,
 	})
 	return nil
 }
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 2895c9e..e4cb28b 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -62,7 +62,7 @@
 	r := &runner{
 		server: &Server{
 			session:     session,
-			undelivered: make(map[span.URI][]source.Diagnostic),
+			undelivered: make(map[source.FileIdentity][]source.Diagnostic),
 		},
 		data: data,
 		ctx:  ctx,
@@ -78,11 +78,12 @@
 	if err != nil {
 		t.Fatalf("no file for %s: %v", f, err)
 	}
+	identity := v.Snapshot().Handle(r.ctx, f).Identity()
 	results, _, err := source.Diagnostics(r.ctx, v.Snapshot(), f, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
-	got := results[uri]
+	got := results[identity]
 	// 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 {
@@ -336,7 +337,9 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	diagnostics, _, err := source.Diagnostics(r.ctx, view.Snapshot(), f, nil)
+	snapshot := view.Snapshot()
+	fileID := snapshot.Handle(r.ctx, f).Identity()
+	diagnostics, _, err := source.Diagnostics(r.ctx, snapshot, f, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -346,7 +349,7 @@
 		},
 		Context: protocol.CodeActionContext{
 			Only:        []protocol.CodeActionKind{protocol.QuickFix},
-			Diagnostics: toProtocolDiagnostics(r.ctx, diagnostics[uri]),
+			Diagnostics: toProtocolDiagnostics(r.ctx, diagnostics[fileID]),
 		},
 	})
 	if err != nil {
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index c13047c..dacfd48 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -14,7 +14,6 @@
 	"golang.org/x/tools/internal/jsonrpc2"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
-	"golang.org/x/tools/internal/span"
 )
 
 // NewClientServer
@@ -82,7 +81,7 @@
 	// undelivered is a cache of any diagnostics that the server
 	// failed to deliver for some reason.
 	undeliveredMu sync.Mutex
-	undelivered   map[span.URI][]source.Diagnostic
+	undelivered   map[source.FileIdentity][]source.Diagnostic
 
 	// folders is only valid between initialize and initialized, and holds the
 	// set of folders to build views for when we are ready
diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go
index b112c08..a3c8e8c 100644
--- a/internal/lsp/source/diagnostics.go
+++ b/internal/lsp/source/diagnostics.go
@@ -17,7 +17,7 @@
 )
 
 type Diagnostic struct {
-	URI      span.URI
+	File     FileIdentity
 	Range    protocol.Range
 	Message  string
 	Source   string
@@ -39,10 +39,11 @@
 	Message string
 }
 
-func Diagnostics(ctx context.Context, snapshot Snapshot, f File, disabledAnalyses map[string]struct{}) (map[span.URI][]Diagnostic, string, error) {
+func Diagnostics(ctx context.Context, snapshot Snapshot, f File, disabledAnalyses map[string]struct{}) (map[FileIdentity][]Diagnostic, string, error) {
 	ctx, done := trace.StartSpan(ctx, "source.Diagnostics", telemetry.File.Of(f.URI()))
 	defer done()
 
+	fh := snapshot.Handle(ctx, f)
 	cphs, err := snapshot.PackageHandles(ctx, f)
 	if err != nil {
 		return nil, "", err
@@ -51,7 +52,6 @@
 	if err != nil {
 		return nil, "", err
 	}
-
 	// If we are missing dependencies, it may because the user's workspace is
 	// not correctly configured. Report errors, if possible.
 	var warningMsg string
@@ -64,21 +64,21 @@
 	pkg, err := cph.Check(ctx)
 	if err != nil {
 		log.Error(ctx, "no package for file", err)
-		return singleDiagnostic(f.URI(), "%s is not part of a package", f.URI()), "", nil
+		return singleDiagnostic(fh.Identity(), "%s is not part of a package", f.URI()), "", nil
 	}
 
 	// Prepare the reports we will send for the files in this package.
-	reports := make(map[span.URI][]Diagnostic)
+	reports := make(map[FileIdentity][]Diagnostic)
 	for _, fh := range pkg.CompiledGoFiles() {
-		clearReports(snapshot, reports, fh.File().Identity().URI)
+		clearReports(snapshot, reports, fh.File().Identity())
 	}
 
 	// Prepare any additional reports for the errors in this package.
-	for _, err := range pkg.GetErrors() {
-		if err.Kind != ListError {
+	for _, e := range pkg.GetErrors() {
+		if e.Kind != ListError {
 			continue
 		}
-		clearReports(snapshot, reports, err.URI)
+		clearReports(snapshot, reports, e.File)
 	}
 
 	// Run diagnostics for the package that this URI belongs to.
@@ -96,7 +96,7 @@
 			return nil, warningMsg, err
 		}
 		for _, fh := range pkg.CompiledGoFiles() {
-			clearReports(snapshot, reports, fh.File().Identity().URI)
+			clearReports(snapshot, reports, fh.File().Identity())
 		}
 		diagnostics(ctx, pkg, reports)
 	}
@@ -107,25 +107,25 @@
 	listErrors, parseErrors, typeErrors []*Diagnostic
 }
 
-func diagnostics(ctx context.Context, pkg Package, reports map[span.URI][]Diagnostic) bool {
+func diagnostics(ctx context.Context, pkg Package, reports map[FileIdentity][]Diagnostic) bool {
 	ctx, done := trace.StartSpan(ctx, "source.diagnostics", telemetry.Package.Of(pkg.ID()))
 	_ = ctx // circumvent SA4006
 	defer done()
 
-	diagSets := make(map[span.URI]*diagnosticSet)
-	for _, err := range pkg.GetErrors() {
+	diagSets := make(map[FileIdentity]*diagnosticSet)
+	for _, e := range pkg.GetErrors() {
 		diag := &Diagnostic{
-			URI:      err.URI,
-			Message:  err.Message,
-			Range:    err.Range,
+			File:     e.File,
+			Message:  e.Message,
+			Range:    e.Range,
 			Severity: protocol.SeverityError,
 		}
-		set, ok := diagSets[diag.URI]
+		set, ok := diagSets[e.File]
 		if !ok {
 			set = &diagnosticSet{}
-			diagSets[diag.URI] = set
+			diagSets[e.File] = set
 		}
-		switch err.Kind {
+		switch e.Kind {
 		case ParseError:
 			set.parseErrors = append(set.parseErrors, diag)
 			diag.Source = "syntax"
@@ -138,7 +138,7 @@
 		}
 	}
 	var nonEmptyDiagnostics bool // track if we actually send non-empty diagnostics
-	for uri, set := range diagSets {
+	for fileID, set := range diagSets {
 		// Don't report type errors if there are parse errors or list errors.
 		diags := set.typeErrors
 		if len(set.parseErrors) > 0 {
@@ -150,15 +150,15 @@
 			nonEmptyDiagnostics = true
 		}
 		for _, diag := range diags {
-			if _, ok := reports[uri]; ok {
-				reports[uri] = append(reports[uri], *diag)
+			if _, ok := reports[fileID]; ok {
+				reports[fileID] = append(reports[fileID], *diag)
 			}
 		}
 	}
 	return nonEmptyDiagnostics
 }
 
-func analyses(ctx context.Context, snapshot Snapshot, cph CheckPackageHandle, disabledAnalyses map[string]struct{}, reports map[span.URI][]Diagnostic) error {
+func analyses(ctx context.Context, snapshot Snapshot, cph CheckPackageHandle, disabledAnalyses map[string]struct{}, reports map[FileIdentity][]Diagnostic) error {
 	var analyzers []*analysis.Analyzer
 	for _, a := range snapshot.View().Options().Analyzers {
 		if _, ok := disabledAnalyses[a.Name]; ok {
@@ -181,8 +181,8 @@
 		if onlyDeletions(e.SuggestedFixes) {
 			tags = append(tags, protocol.Unnecessary)
 		}
-		addReport(snapshot, reports, Diagnostic{
-			URI:            e.URI,
+		addReport(ctx, snapshot, reports, Diagnostic{
+			File:           e.File,
 			Range:          e.Range,
 			Message:        e.Message,
 			Source:         e.Category,
@@ -195,27 +195,28 @@
 	return nil
 }
 
-func clearReports(snapshot Snapshot, reports map[span.URI][]Diagnostic, uri span.URI) {
-	if snapshot.View().Ignore(uri) {
+func clearReports(snapshot Snapshot, reports map[FileIdentity][]Diagnostic, fileID FileIdentity) {
+	if snapshot.View().Ignore(fileID.URI) {
 		return
 	}
-	reports[uri] = []Diagnostic{}
+	reports[fileID] = []Diagnostic{}
 }
 
-func addReport(snapshot Snapshot, reports map[span.URI][]Diagnostic, diagnostic Diagnostic) {
-	if snapshot.View().Ignore(diagnostic.URI) {
-		return
+func addReport(ctx context.Context, snapshot Snapshot, reports map[FileIdentity][]Diagnostic, diagnostic Diagnostic) error {
+	if snapshot.View().Ignore(diagnostic.File.URI) {
+		return nil
 	}
-	if _, ok := reports[diagnostic.URI]; ok {
-		reports[diagnostic.URI] = append(reports[diagnostic.URI], diagnostic)
+	if _, ok := reports[diagnostic.File]; ok {
+		reports[diagnostic.File] = append(reports[diagnostic.File], diagnostic)
 	}
+	return nil
 }
 
-func singleDiagnostic(uri span.URI, format string, a ...interface{}) map[span.URI][]Diagnostic {
-	return map[span.URI][]Diagnostic{
-		uri: []Diagnostic{{
-			Source:   "LSP",
-			URI:      uri,
+func singleDiagnostic(fileID FileIdentity, format string, a ...interface{}) map[FileIdentity][]Diagnostic {
+	return map[FileIdentity][]Diagnostic{
+		fileID: []Diagnostic{{
+			File:     fileID,
+			Source:   "gopls",
 			Range:    protocol.Range{},
 			Message:  fmt.Sprintf(format, a...),
 			Severity: protocol.SeverityError,
diff --git a/internal/lsp/source/format.go b/internal/lsp/source/format.go
index 021ec71..51059f3 100644
--- a/internal/lsp/source/format.go
+++ b/internal/lsp/source/format.go
@@ -381,8 +381,8 @@
 
 // hasParseErrors returns true if the given file has parse errors.
 func hasParseErrors(pkg Package, uri span.URI) bool {
-	for _, err := range pkg.GetErrors() {
-		if err.URI == uri && err.Kind == ParseError {
+	for _, e := range pkg.GetErrors() {
+		if e.File.URI == uri && e.Kind == ParseError {
 			return true
 		}
 	}
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index fb825a7..b2c0f53 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -71,11 +71,13 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	results, _, err := source.Diagnostics(r.ctx, r.view.Snapshot(), f, nil)
+	snapshot := r.view.Snapshot()
+	fileID := snapshot.Handle(r.ctx, f).Identity()
+	results, _, err := source.Diagnostics(r.ctx, snapshot, f, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
-	got := results[uri]
+	got := results[fileID]
 	// 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 {
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index b41b8cf..be5c393 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -287,8 +287,8 @@
 	Kind FileKind
 }
 
-func (identity FileIdentity) String() string {
-	return fmt.Sprintf("%s%f%s%s", identity.URI, identity.Version, identity.Identifier, identity.Kind)
+func (fileID FileIdentity) String() string {
+	return fmt.Sprintf("%s%f%s%s", fileID.URI, fileID.Version, fileID.Identifier, fileID.Kind)
 }
 
 // File represents a source file of any type.
@@ -342,7 +342,7 @@
 }
 
 type Error struct {
-	URI            span.URI
+	File           FileIdentity
 	Range          protocol.Range
 	Kind           ErrorKind
 	Message        string
@@ -362,7 +362,7 @@
 )
 
 func (e *Error) Error() string {
-	return fmt.Sprintf("%s:%s: %s", e.URI, e.Range, e.Message)
+	return fmt.Sprintf("%s:%s: %s", e.File, e.Range, e.Message)
 }
 
 type BuiltinPackage interface {
diff --git a/internal/lsp/tests/diagnostics.go b/internal/lsp/tests/diagnostics.go
index 92f948c..8ec1c6e 100644
--- a/internal/lsp/tests/diagnostics.go
+++ b/internal/lsp/tests/diagnostics.go
@@ -32,7 +32,7 @@
 			return summarizeDiagnostics(i, want, got, "incorrect Source got %v want %v", g.Source, w.Source)
 		}
 		// Don't check the range on the badimport test.
-		if strings.Contains(string(g.URI), "badimport") {
+		if strings.Contains(string(g.File.URI), "badimport") {
 			continue
 		}
 		if protocol.ComparePosition(w.Range.Start, g.Range.Start) != 0 {
@@ -54,7 +54,7 @@
 }
 
 func compareDiagnostic(a, b source.Diagnostic) int {
-	if r := span.CompareURI(a.URI, b.URI); r != 0 {
+	if r := span.CompareURI(a.File.URI, b.File.URI); r != 0 {
 		return r
 	}
 	if r := protocol.CompareRange(a.Range, b.Range); r != 0 {
@@ -80,11 +80,11 @@
 	fmt.Fprintf(msg, reason, args...)
 	fmt.Fprint(msg, ":\nexpected:\n")
 	for _, d := range want {
-		fmt.Fprintf(msg, "  %s:%v: %s\n", d.URI, d.Range, d.Message)
+		fmt.Fprintf(msg, "  %s:%v: %s\n", d.File.URI, d.Range, d.Message)
 	}
 	fmt.Fprintf(msg, "got:\n")
 	for _, d := range got {
-		fmt.Fprintf(msg, "  %s:%v: %s\n", d.URI, d.Range, d.Message)
+		fmt.Fprintf(msg, "  %s:%v: %s\n", d.File.URI, d.Range, d.Message)
 	}
 	return msg.String()
 }
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index d7c75ee..6aeebbd 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -704,7 +704,9 @@
 	// This is not the correct way to do this,
 	// but it seems excessive to do the full conversion here.
 	want := source.Diagnostic{
-		URI: spn.URI(),
+		File: source.FileIdentity{
+			URI: spn.URI(),
+		},
 		Range: protocol.Range{
 			Start: protocol.Position{
 				Line:      float64(spn.Start().Line()) - 1,
diff --git a/internal/lsp/watched_files.go b/internal/lsp/watched_files.go
index a5efe05..87fa0f4 100644
--- a/internal/lsp/watched_files.go
+++ b/internal/lsp/watched_files.go
@@ -70,7 +70,9 @@
 
 				// If this was the only file in the package, clear its diagnostics.
 				if otherFile == nil {
-					if err := s.publishDiagnostics(ctx, uri, []source.Diagnostic{}); err != nil {
+					if err := s.publishDiagnostics(ctx, source.FileIdentity{
+						URI: uri,
+					}, []source.Diagnostic{}); err != nil {
 						log.Error(ctx, "failed to clear diagnostics", err, telemetry.URI.Of(uri))
 					}
 					return nil