internal/lsp: fix swallowed package errors

If a package has an error that makes it completely unparseable, such as containing a .go file with no "package" statement, the error was previously unreported. Such errors would manifest as other errors.

Fixes golang/go#31712

Change-Id: I11b8d0e2e4d64b03fbcb4c35e7f0b02fccc83fad
GitHub-Last-Rev: 1581cbe36c269dd964f0b9226dbd63b1650c2a5b
GitHub-Pull-Request: golang/tools#102
Reviewed-on: https://go-review.googlesource.com/c/tools/+/177605
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go
index 00ccd02..9b676c9 100644
--- a/internal/lsp/cmd/cmd.go
+++ b/internal/lsp/cmd/cmd.go
@@ -339,7 +339,7 @@
 		}
 		f := c.fset.AddFile(fname, -1, len(content))
 		f.SetLinesForContent(content)
-		file.mapper = protocol.NewColumnMapper(uri, c.fset, f, content)
+		file.mapper = protocol.NewColumnMapper(uri, fname, c.fset, f, content)
 	}
 	return file
 }
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index 851d9bd..4e4f7fc 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -6,6 +6,7 @@
 
 import (
 	"context"
+	"strings"
 
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
@@ -40,8 +41,10 @@
 	// 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)
-
+		err := s.publishDiagnostics(ctx, view, uri, diagnostics)
+		if err != nil {
+			s.session.Logger().Errorf(ctx, "failed to deliver diagnostic for %s: %v", uri, err)
+		}
 		// If we fail to deliver the same diagnostics twice, just give up.
 		delete(s.undelivered, uri)
 	}
@@ -78,7 +81,7 @@
 			return nil, err
 		}
 		reports = append(reports, protocol.Diagnostic{
-			Message:  diag.Message,
+			Message:  strings.TrimSpace(diag.Message), // go list returns errors prefixed by newline
 			Range:    rng,
 			Severity: severity,
 			Source:   diag.Source,
diff --git a/internal/lsp/link.go b/internal/lsp/link.go
index d437fb1..fd8eb49 100644
--- a/internal/lsp/link.go
+++ b/internal/lsp/link.go
@@ -6,6 +6,7 @@
 
 import (
 	"context"
+	"fmt"
 	"strconv"
 
 	"golang.org/x/tools/internal/lsp/protocol"
@@ -21,6 +22,10 @@
 	}
 	// find the import block
 	ast := f.GetAST(ctx)
+	if ast == nil {
+		return nil, fmt.Errorf("no AST for %v", uri)
+	}
+
 	var result []protocol.DocumentLink
 	for _, imp := range ast.Imports {
 		spn, err := span.NewRange(f.GetFileSet(ctx), imp.Pos(), imp.End()).Span()
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 172f77c..e6cffa0 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -594,7 +594,7 @@
 	if err != nil {
 		return nil, err
 	}
-	return protocol.NewColumnMapper(uri, fset, f, content), nil
+	return protocol.NewColumnMapper(uri, filename, fset, f, content), nil
 }
 
 func TestBytesOffset(t *testing.T) {
@@ -624,7 +624,7 @@
 		fset := token.NewFileSet()
 		f := fset.AddFile(fname, -1, len(test.text))
 		f.SetLinesForContent([]byte(test.text))
-		mapper := protocol.NewColumnMapper(span.FileURI(fname), fset, f, []byte(test.text))
+		mapper := protocol.NewColumnMapper(span.FileURI(fname), fname, fset, f, []byte(test.text))
 		got, err := mapper.Point(test.pos)
 		if err != nil && test.want != -1 {
 			t.Errorf("unexpected error: %v", err)
diff --git a/internal/lsp/protocol/span.go b/internal/lsp/protocol/span.go
index 9045b2c..f41fba8 100644
--- a/internal/lsp/protocol/span.go
+++ b/internal/lsp/protocol/span.go
@@ -23,10 +23,17 @@
 	return string(uri)
 }
 
-func NewColumnMapper(uri span.URI, fset *token.FileSet, f *token.File, content []byte) *ColumnMapper {
+func NewColumnMapper(uri span.URI, fn string, fset *token.FileSet, f *token.File, content []byte) *ColumnMapper {
+	var converter *span.TokenConverter
+	if f == nil {
+		converter = span.NewContentConverter(fn, content)
+	} else {
+		converter = span.NewTokenConverter(fset, f)
+	}
+
 	return &ColumnMapper{
 		URI:       uri,
-		Converter: span.NewTokenConverter(fset, f),
+		Converter: converter,
 		Content:   content,
 	}
 }
diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go
index f6ae675..2977928 100644
--- a/internal/lsp/source/diagnostics.go
+++ b/internal/lsp/source/diagnostics.go
@@ -8,6 +8,7 @@
 	"bytes"
 	"context"
 	"fmt"
+	"strings"
 
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/analysis/passes/asmdecl"
@@ -72,6 +73,12 @@
 		}
 		reports[uri] = []Diagnostic{}
 	}
+
+	// Prepare reports for package errors
+	for _, pkgErr := range pkg.GetErrors() {
+		reports[packageErrorSpan(pkgErr).URI()] = []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.
@@ -117,7 +124,7 @@
 		diags = listErrors
 	}
 	for _, diag := range diags {
-		spn := span.Parse(diag.Pos)
+		spn := packageErrorSpan(diag)
 		if spn.IsPoint() && diag.Kind == packages.TypeError {
 			spn = pointToSpan(ctx, v, spn)
 		}
@@ -161,6 +168,29 @@
 	return nil
 }
 
+// parseDiagnosticMessage attempts to parse a standard error message by stripping off the trailing error message.
+// Works only on errors where the message is prefixed by ": ".
+// e.g.:
+//   attributes.go:13:1: expected 'package', found 'type'
+func parseDiagnosticMessage(input string) span.Span {
+	input = strings.TrimSpace(input)
+
+	msgIndex := strings.Index(input, ": ")
+	if msgIndex < 0 {
+		return span.Parse(input)
+	}
+
+	return span.Parse(input[:msgIndex])
+}
+
+func packageErrorSpan(pkgErr packages.Error) span.Span {
+	if pkgErr.Pos == "" {
+		return parseDiagnosticMessage(pkgErr.Msg)
+	}
+
+	return span.Parse(pkgErr.Pos)
+}
+
 func pointToSpan(ctx context.Context, v View, spn span.Span) span.Span {
 	// Don't set a range if it's anything other than a type error.
 	f, err := v.GetFile(ctx, spn.URI())
diff --git a/internal/lsp/source/diagnostics_test.go b/internal/lsp/source/diagnostics_test.go
new file mode 100644
index 0000000..8d37a52
--- /dev/null
+++ b/internal/lsp/source/diagnostics_test.go
@@ -0,0 +1,55 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package source
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestParseErrorMessage(t *testing.T) {
+	tests := []struct {
+		name             string
+		in               string
+		expectedFileName string
+		expectedLine     int
+		expectedColumn   int
+	}{
+		{
+			name:             "from go list output",
+			in:               "\nattributes.go:13:1: expected 'package', found 'type'",
+			expectedFileName: "attributes.go",
+			expectedLine:     13,
+			expectedColumn:   1,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			spn := parseDiagnosticMessage(tt.in)
+			fn, err := spn.URI().Filename()
+			if err != nil {
+				t.Fatalf("unexpected error: %v", err)
+			}
+
+			if !strings.HasSuffix(fn, tt.expectedFileName) {
+				t.Errorf("expected filename with suffix %v but got %v", tt.expectedFileName, fn)
+			}
+
+			if !spn.HasPosition() {
+				t.Fatalf("expected span to have position")
+			}
+
+			pos := spn.Start()
+			if pos.Line() != tt.expectedLine {
+				t.Errorf("expected line %v but got %v", tt.expectedLine, pos.Line())
+			}
+
+			if pos.Column() != tt.expectedColumn {
+				t.Errorf("expected line %v but got %v", tt.expectedLine, pos.Line())
+			}
+		})
+	}
+}
diff --git a/internal/lsp/util.go b/internal/lsp/util.go
index 0d61de5..aa8be47 100644
--- a/internal/lsp/util.go
+++ b/internal/lsp/util.go
@@ -51,11 +51,14 @@
 	if err != nil {
 		return nil, nil, err
 	}
-	tok := f.GetToken(ctx)
-	if tok == nil {
-		return nil, nil, fmt.Errorf("no file information for %v", f.URI())
+
+	fname, err := f.URI().Filename()
+	if err != nil {
+		return nil, nil, err
 	}
-	m := protocol.NewColumnMapper(f.URI(), f.GetFileSet(ctx), tok, f.GetContent(ctx))
+
+	m := protocol.NewColumnMapper(f.URI(), fname, f.GetFileSet(ctx), f.GetToken(ctx), f.GetContent(ctx))
+
 	return f, m, nil
 }