internal/lsp: show "do not edit" message when user opens generated file

This is probably a better approach than showing an extra diagnostic,
since a user cannot dismiss a diagnostic.

Fixes golang/go#33397

Change-Id: I92b9a00f51a463673993793abfd4cfb99ce69a91
Reviewed-on: https://go-review.googlesource.com/c/tools/+/188766
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go
index e5e149c..3ef28f1 100644
--- a/internal/lsp/cache/parse.go
+++ b/internal/lsp/cache/parse.go
@@ -85,7 +85,7 @@
 	defer func() { <-parseLimit }()
 	parserMode := parser.AllErrors | parser.ParseComments
 	if mode == source.ParseHeader {
-		parserMode = parser.ImportsOnly
+		parserMode = parser.ImportsOnly | parser.ParseComments
 	}
 	ast, err := parser.ParseFile(c.fset, fh.Identity().URI.Filename(), buf, parserMode)
 	if ast != nil {
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 6f270ee..a1588d4 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -84,6 +84,13 @@
 			t.Fatal(err)
 		}
 		got := results[uri]
+		// 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 {
+				t.Errorf("expected no diagnostics for %s, got %v", uri, got)
+			}
+			continue
+		}
 		if diff := diffDiagnostics(uri, want, got); diff != "" {
 			t.Error(diff)
 		}
diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go
index 121d6c1..51cc41d 100644
--- a/internal/lsp/source/diagnostics.go
+++ b/internal/lsp/source/diagnostics.go
@@ -108,9 +108,10 @@
 	listErrors, parseErrors, typeErrors []Diagnostic
 }
 
-func diagnostics(ctx context.Context, v View, pkg Package, reports map[span.URI][]Diagnostic) bool {
+func diagnostics(ctx context.Context, view View, pkg Package, reports map[span.URI][]Diagnostic) bool {
 	ctx, done := trace.StartSpan(ctx, "source.diagnostics", telemetry.Package.Of(pkg.ID()))
 	defer done()
+
 	diagSets := make(map[span.URI]*diagnosticSet)
 	for _, err := range pkg.GetErrors() {
 		diag := Diagnostic{
@@ -129,7 +130,7 @@
 			set.parseErrors = append(set.parseErrors, diag)
 		case packages.TypeError:
 			if diag.Span.IsPoint() {
-				diag.Span = pointToSpan(ctx, v, diag.Span)
+				diag.Span = pointToSpan(ctx, view, diag.Span)
 			}
 			set.typeErrors = append(set.typeErrors, diag)
 		default:
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index b74ec42..fee66ee 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -62,6 +62,13 @@
 			t.Fatal(err)
 		}
 		got := results[uri]
+		// 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 {
+				t.Errorf("expected no diagnostics for %s, got %v", uri, got)
+			}
+			continue
+		}
 		if diff := diffDiagnostics(uri, want, got); diff != "" {
 			t.Error(diff)
 		}
diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go
index 38ca24e..ad25225 100644
--- a/internal/lsp/source/util.go
+++ b/internal/lsp/source/util.go
@@ -5,14 +5,49 @@
 package source
 
 import (
+	"context"
 	"fmt"
 	"go/ast"
 	"go/token"
 	"go/types"
 	"path/filepath"
+	"regexp"
 	"strings"
+
+	"golang.org/x/tools/internal/span"
 )
 
+func IsGenerated(ctx context.Context, view View, uri span.URI) bool {
+	f, err := view.GetFile(ctx, uri)
+	if err != nil {
+		return false
+	}
+	ph := view.Session().Cache().ParseGoHandle(f.Handle(ctx), ParseHeader)
+	parsed, err := ph.Parse(ctx)
+	if parsed == nil {
+		return false
+	}
+	tok := view.Session().Cache().FileSet().File(parsed.Pos())
+	if tok == nil {
+		return false
+	}
+	for _, commentGroup := range parsed.Comments {
+		for _, comment := range commentGroup.List {
+			if matched := generatedRx.MatchString(comment.Text); matched {
+				// Check if comment is at the beginning of the line in source.
+				if pos := tok.Position(comment.Slash); pos.Column == 1 {
+					return true
+				}
+			}
+		}
+	}
+	return false
+}
+
+// Matches cgo generated comment as well as the proposed standard:
+//	https://golang.org/s/generatedcode
+var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`)
+
 func DetectLanguage(langID, filename string) FileKind {
 	switch langID {
 	case "go":
diff --git a/internal/lsp/testdata/generated/generated.go b/internal/lsp/testdata/generated/generated.go
new file mode 100644
index 0000000..abd2bee
--- /dev/null
+++ b/internal/lsp/testdata/generated/generated.go
@@ -0,0 +1,7 @@
+package generated
+
+// Code generated by generator.go. DO NOT EDIT.
+
+func _() {
+	var y int //@diag("y", "LSP", "y declared but not used")
+}
diff --git a/internal/lsp/testdata/generated/generator.go b/internal/lsp/testdata/generated/generator.go
new file mode 100644
index 0000000..0369987
--- /dev/null
+++ b/internal/lsp/testdata/generated/generator.go
@@ -0,0 +1,5 @@
+package generated
+
+func _() {
+	var x int //@diag("x", "LSP", "x declared but not used")
+}
diff --git a/internal/lsp/testdata/good/good0.go b/internal/lsp/testdata/good/good0.go
index ceb26c5..98f03b0 100644
--- a/internal/lsp/testdata/good/good0.go
+++ b/internal/lsp/testdata/good/good0.go
@@ -1,4 +1,4 @@
-package good //@diag("package", "", "")
+package good //@diag("package", "no_diagnostics", "")
 
 func stuff() { //@item(good_stuff, "stuff", "func()", "func")
 	x := 5
diff --git a/internal/lsp/testdata/good/good1.go b/internal/lsp/testdata/good/good1.go
index 95a9250..184fbd2 100644
--- a/internal/lsp/testdata/good/good1.go
+++ b/internal/lsp/testdata/good/good1.go
@@ -1,4 +1,4 @@
-package good //@diag("package", "", "")
+package good //@diag("package", "no_diagnostics", "")
 
 import (
 	"golang.org/x/tools/internal/lsp/types" //@item(types_import, "types", "\"golang.org/x/tools/internal/lsp/types\"", "package")
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index d649864..73063c8 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -28,7 +28,7 @@
 const (
 	ExpectedCompletionsCount       = 144
 	ExpectedCompletionSnippetCount = 15
-	ExpectedDiagnosticsCount       = 17
+	ExpectedDiagnosticsCount       = 21
 	ExpectedFormatCount            = 6
 	ExpectedImportCount            = 2
 	ExpectedDefinitionsCount       = 38
@@ -420,11 +420,6 @@
 	if _, ok := data.Diagnostics[spn.URI()]; !ok {
 		data.Diagnostics[spn.URI()] = []source.Diagnostic{}
 	}
-	// If a file has an empty diagnostic message, return. This allows us to
-	// avoid testing diagnostics in files that may have a lot of them.
-	if msg == "" {
-		return
-	}
 	severity := source.SeverityError
 	if strings.Contains(string(spn.URI()), "analyzer") {
 		severity = source.SeverityWarning
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
index 48979d6..68abec0 100644
--- a/internal/lsp/text_synchronization.go
+++ b/internal/lsp/text_synchronization.go
@@ -7,6 +7,7 @@
 import (
 	"bytes"
 	"context"
+	"fmt"
 
 	"golang.org/x/tools/internal/jsonrpc2"
 	"golang.org/x/tools/internal/lsp/protocol"
@@ -28,8 +29,17 @@
 	// Open the file.
 	s.session.DidOpen(ctx, uri, fileKind, text)
 
-	// Run diagnostics on the newly-changed file.
 	view := s.session.ViewOf(uri)
+
+	// TODO: Ideally, we should be able to specify that a generated file should be opened as read-only.
+	if source.IsGenerated(ctx, view, uri) {
+		s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
+			Message: fmt.Sprintf("Do not edit this file! %s is a generated file.", uri.Filename()),
+			Type:    protocol.Warning,
+		})
+	}
+
+	// Run diagnostics on the newly-changed file.
 	go func() {
 		ctx := view.BackgroundContext()
 		ctx, done := trace.StartSpan(ctx, "lsp:background-worker")