internal/lsp: show compiler optimization decisions

The gc compiler will report its decisions about inlining, escapes, etc.
This can be turned on and off with a new optional code lens gc_details.
When enabled, the code lens will be displayed above the package
statement. The compiler's decisions are shown as information diagnostics.
(Other diagnostics have been errors and warnings.)

Change-Id: I7d1d5b5b5cf8acd7ff08f683e537ea618e269547
Reviewed-on: https://go-review.googlesource.com/c/tools/+/243119
Run-TryBot: Peter Weinberger <pjw@google.com>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md
index 7933696..fe0191a 100644
--- a/gopls/doc/settings.md
+++ b/gopls/doc/settings.md
@@ -86,14 +86,24 @@
 ### **codelens** *map[string]bool*
 
 Overrides the enabled/disabled state of various code lenses. Currently, we
-support two code lenses:
+support several code lenses:
 
 * `generate`: [default: enabled] run `go generate` as specified by a `//go:generate` directive.
-* `upgrade.dependency`: [default: enabled] upgrade a dependency listed in a `go.mod` file.
+* `upgrade_dependency`: [default: enabled] upgrade a dependency listed in a `go.mod` file.
 * `test`: [default: disabled] run `go test -run` for a test func.
+* `gc_details`: [default: disabled] Show the gc compiler's choices for inline analysis and escaping.
 
-By default, both of these code lenses are enabled.
-
+Example Usage:
+```json5
+"gopls": {
+...
+  "codelens": {
+    "generate": false,  // Don't run `go generate`.
+    "gc_details": true  // Show a code lens toggling the display of gc's choices.
+  }
+...
+}
+```
 ### **completionDocumentation** *boolean*
 
 If false, indicates that the user does not want documentation with completion results.
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
index 218df2c..a9cbcd9 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -8,6 +8,7 @@
 	"context"
 	"fmt"
 	"io"
+	"path"
 	"strings"
 
 	"golang.org/x/tools/internal/event"
@@ -50,8 +51,9 @@
 		}
 	}
 	if unsaved {
-		switch command {
-		case source.CommandTest, source.CommandGenerate:
+		switch params.Command {
+		case source.CommandTest.Name, source.CommandGenerate.Name, source.CommandToggleDetails.Name:
+			// TODO(PJW): for Toggle, not an error if it is being disabled
 			return nil, s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
 				Type:    protocol.Error,
 				Message: fmt.Sprintf("cannot run command %s: unsaved files in the view", params.Command),
@@ -145,6 +147,29 @@
 		return nil, err
 	default:
 		return nil, fmt.Errorf("unknown command: %s", params.Command)
+	case source.CommandToggleDetails:
+		var fileURI span.URI
+		if err := source.UnmarshalArgs(params.Arguments, &fileURI); err != nil {
+			return nil, err
+		}
+		pkgDir := span.URI(path.Dir(fileURI.Filename()))
+		s.deliveredMu.Lock()
+		if s.gcOptimizatonDetails[pkgDir] {
+			delete(s.gcOptimizatonDetails, pkgDir)
+		} else {
+			s.gcOptimizatonDetails[pkgDir] = true
+		}
+		event.Log(ctx, fmt.Sprintf("PJW details %s now %v %v", pkgDir, s.gcOptimizatonDetails[pkgDir],
+			s.gcOptimizatonDetails))
+		s.deliveredMu.Unlock()
+		// need to recompute diagnostics.
+		// so find the snapshot
+		sv, err := s.session.ViewOf(fileURI)
+		if err != nil {
+			return nil, err
+		}
+		s.diagnoseSnapshot(sv.Snapshot())
+		return nil, nil
 	}
 	return nil, nil
 }
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index 5491178..e3f2d75 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -8,6 +8,7 @@
 	"context"
 	"crypto/sha1"
 	"fmt"
+	"path/filepath"
 	"strings"
 	"sync"
 
@@ -16,6 +17,7 @@
 	"golang.org/x/tools/internal/lsp/mod"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/span"
 	"golang.org/x/tools/internal/xcontext"
 	"golang.org/x/xerrors"
 )
@@ -115,16 +117,32 @@
 		go func(pkg source.Package) {
 			defer wg.Done()
 
+			detailsDir := ""
 			// Only run analyses for packages with open files.
 			withAnalysis := alwaysAnalyze
 			for _, pgf := range pkg.CompiledGoFiles() {
 				if snapshot.IsOpen(pgf.URI) {
 					withAnalysis = true
-					break
+				}
+				if detailsDir == "" {
+					dir := filepath.Dir(pgf.URI.Filename())
+					if s.gcOptimizatonDetails[span.URI(dir)] {
+						detailsDir = dir
+					}
 				}
 			}
 
 			pkgReports, warn, err := source.Diagnostics(ctx, snapshot, pkg, withAnalysis)
+			if detailsDir != "" {
+				var more map[source.FileIdentity][]*source.Diagnostic
+				more, err = source.DoGcDetails(ctx, snapshot, detailsDir)
+				if err != nil {
+					event.Error(ctx, "warning: gcdetails", err, tag.Snapshot.Of(snapshot.ID()))
+				}
+				for k, v := range more {
+					pkgReports[k] = append(pkgReports[k], v...)
+				}
+			}
 
 			// Check if might want to warn the user about their build configuration.
 			// Our caller decides whether to send the message.
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 5b9f71f..a362c03 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -23,10 +23,11 @@
 // messages on on the supplied stream.
 func NewServer(session source.Session, client protocol.Client) *Server {
 	return &Server{
-		delivered:       make(map[span.URI]sentDiagnostics),
-		session:         session,
-		client:          client,
-		diagnosticsSema: make(chan struct{}, concurrentAnalyses),
+		delivered:            make(map[span.URI]sentDiagnostics),
+		gcOptimizatonDetails: make(map[span.URI]bool),
+		session:              session,
+		client:               client,
+		diagnosticsSema:      make(chan struct{}, concurrentAnalyses),
 	}
 }
 
@@ -73,6 +74,10 @@
 	deliveredMu sync.Mutex
 	delivered   map[span.URI]sentDiagnostics
 
+	// gcOptimizationDetails describes which packages we want optimization details
+	// included in the diagnostics. The key is the directory of the package.
+	gcOptimizatonDetails map[span.URI]bool
+
 	// diagnosticsSema limits the concurrency of diagnostics runs, which can be expensive.
 	diagnosticsSema chan struct{}
 
diff --git a/internal/lsp/source/code_lens.go b/internal/lsp/source/code_lens.go
index d3cfb4b..b0f146f 100644
--- a/internal/lsp/source/code_lens.go
+++ b/internal/lsp/source/code_lens.go
@@ -23,6 +23,7 @@
 	CommandGenerate.Name:      goGenerateCodeLens,
 	CommandTest.Name:          runTestCodeLens,
 	CommandRegenerateCgo.Name: regenerateCgoLens,
+	CommandToggleDetails.Name: toggleDetailsCodeLens,
 }
 
 // CodeLens computes code lens for Go source code.
@@ -221,3 +222,26 @@
 		},
 	}, nil
 }
+
+func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) {
+	_, pgf, err := getParsedFile(ctx, snapshot, fh, WidestPackage)
+	fset := snapshot.View().Session().Cache().FileSet()
+	rng, err := newMappedRange(fset, pgf.Mapper, pgf.File.Package, pgf.File.Package).Range()
+	if err != nil {
+		return nil, err
+	}
+	jsonArgs, err := MarshalArgs(fh.URI())
+	if err != nil {
+		return nil, err
+	}
+	return []protocol.CodeLens{
+		{
+			Range: rng,
+			Command: protocol.Command{
+				Title:     "Toggle gc annotation details",
+				Command:   CommandToggleDetails.Name,
+				Arguments: jsonArgs,
+			},
+		},
+	}, nil
+}
diff --git a/internal/lsp/source/command.go b/internal/lsp/source/command.go
index 211f562..ff52f3e 100644
--- a/internal/lsp/source/command.go
+++ b/internal/lsp/source/command.go
@@ -53,6 +53,7 @@
 	CommandVendor,
 	CommandExtractVariable,
 	CommandExtractFunction,
+	CommandToggleDetails,
 }
 
 var (
@@ -86,6 +87,11 @@
 		Name: "regenerate_cgo",
 	}
 
+	// CommandToggleDetails controls calculation of gc annotations.
+	CommandToggleDetails = &Command{
+		Name: "gc_details",
+	}
+
 	// CommandFillStruct is a gopls command to fill a struct with default
 	// values.
 	CommandFillStruct = &Command{
diff --git a/internal/lsp/source/gc_annotations.go b/internal/lsp/source/gc_annotations.go
new file mode 100644
index 0000000..4d6e028
--- /dev/null
+++ b/internal/lsp/source/gc_annotations.go
@@ -0,0 +1,121 @@
+// Copyright 2020 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 (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/span"
+)
+
+func DoGcDetails(ctx context.Context, snapshot Snapshot, pkgDir string) (map[FileIdentity][]*Diagnostic, error) {
+	outDir := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.details", os.Getpid()))
+	if err := os.MkdirAll(outDir, 0700); err != nil {
+		return nil, err
+	}
+	args := []string{fmt.Sprintf("-gcflags=-json=0,%s", outDir), pkgDir}
+	err := snapshot.RunGoCommandDirect(ctx, "build", args)
+	if err != nil {
+		return nil, err
+	}
+	files, err := findJSONFiles(outDir)
+	if err != nil {
+		return nil, err
+	}
+	reports := make(map[FileIdentity][]*Diagnostic)
+	var parseError error
+	for _, fn := range files {
+		fname, v, err := parseDetailsFile(fn)
+		if err != nil {
+			// expect errors for all the files, save 1
+			parseError = err
+		}
+		if !strings.HasSuffix(fname, ".go") {
+			continue // <autogenerated>
+		}
+		uri := span.URIFromPath(fname)
+		x := snapshot.FindFile(uri)
+		if x == nil {
+			continue
+		}
+		k := x.Identity()
+		reports[k] = v
+	}
+	return reports, parseError
+}
+
+func parseDetailsFile(fn string) (string, []*Diagnostic, error) {
+	buf, err := ioutil.ReadFile(fn)
+	if err != nil {
+		return "", nil, err // This is an internal error. Likely ever file will fail.
+	}
+	var fname string
+	var ans []*Diagnostic
+	lines := bytes.Split(buf, []byte{'\n'})
+	for i, l := range lines {
+		if len(l) == 0 {
+			continue
+		}
+		if i == 0 {
+			x := make(map[string]interface{})
+			if err := json.Unmarshal(l, &x); err != nil {
+				return "", nil, fmt.Errorf("internal error (%v) parsing first line of json file %s",
+					err, fn)
+			}
+			fname = x["file"].(string)
+			continue
+		}
+		y := protocol.Diagnostic{}
+		if err := json.Unmarshal(l, &y); err != nil {
+			return "", nil, fmt.Errorf("internal error (%#v) parsing json file for %s", err, fname)
+		}
+		y.Range.Start.Line-- // change from 1-based to 0-based
+		y.Range.Start.Character--
+		y.Range.End.Line--
+		y.Range.End.Character--
+		msg := y.Code.(string)
+		if y.Message != "" {
+			msg = fmt.Sprintf("%s(%s)", msg, y.Message)
+		}
+		x := Diagnostic{
+			Range:    y.Range,
+			Message:  msg,
+			Source:   y.Source,
+			Severity: y.Severity,
+		}
+		for _, ri := range y.RelatedInformation {
+			x.Related = append(x.Related, RelatedInformation{
+				URI:     ri.Location.URI.SpanURI(),
+				Range:   ri.Location.Range,
+				Message: ri.Message,
+			})
+		}
+		ans = append(ans, &x)
+	}
+	return fname, ans, nil
+}
+
+func findJSONFiles(dir string) ([]string, error) {
+	ans := []string{}
+	f := func(path string, fi os.FileInfo, err error) error {
+		if fi.IsDir() {
+			return nil
+		}
+		if strings.HasSuffix(path, ".json") {
+			ans = append(ans, path)
+		}
+		return nil
+	}
+	err := filepath.Walk(dir, f)
+	return ans, err
+}
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index f6bf891..7755e6a 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -102,6 +102,7 @@
 				CommandGenerate.Name:          true,
 				CommandUpgradeDependency.Name: true,
 				CommandRegenerateCgo.Name:     true,
+				CommandToggleDetails.Name:     false,
 			},
 		},
 		DebuggingOptions: DebuggingOptions{