internal/lsp: add a test for gc annotation details code lens
This change adds a test to check the functionality of the GC details
code lens, including the behavior of toggling it on and off. This
exposed a Windows bug that was mentioned on Slack, which can be fixed by
adjusting the URI.
I also refactored a bit of the code to use a JSON decoder, which
simplifies things a little bit.
Change-Id: I7737107cf020fa35bca245485a3b1a1416920fd2
Reviewed-on: https://go-review.googlesource.com/c/tools/+/256940
Trust: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Peter Weinberger <pjw@google.com>
diff --git a/gopls/internal/regtest/codelens_test.go b/gopls/internal/regtest/codelens_test.go
index 3fcfdce..5dd3afb 100644
--- a/gopls/internal/regtest/codelens_test.go
+++ b/gopls/internal/regtest/codelens_test.go
@@ -5,6 +5,8 @@
package regtest
import (
+ "runtime"
+ "strings"
"testing"
"golang.org/x/tools/internal/lsp/protocol"
@@ -177,24 +179,7 @@
`
runner.Run(t, shouldRemoveDep, func(t *testing.T, env *Env) {
env.OpenFile("go.mod")
- lenses := env.CodeLens("go.mod")
- want := "Tidy module"
- var found protocol.CodeLens
- for _, lens := range lenses {
- if lens.Command.Title == want {
- found = lens
- break
- }
- }
- if found.Command.Command == "" {
- t.Fatalf("did not find lens %q, got %v", want, found.Command)
- }
- if _, err := env.Editor.Server.ExecuteCommand(env.Ctx, &protocol.ExecuteCommandParams{
- Command: found.Command.Command,
- Arguments: found.Command.Arguments,
- }); err != nil {
- t.Fatal(err)
- }
+ env.ExecuteCodeLensCommand("go.mod", source.CommandTidy)
env.Await(NoOutstandingWork())
got := env.ReadWorkspaceFile("go.mod")
const wantGoMod = `module mod.com
@@ -239,19 +224,62 @@
env.Await(env.DiagnosticAtRegexp("cgo.go", `C\.(fortytwo)`))
// Regenerate cgo, fixing the diagnostic.
- lenses := env.CodeLens("cgo.go")
- var lens protocol.CodeLens
- for _, l := range lenses {
- if l.Command.Command == source.CommandRegenerateCgo.Name {
- lens = l
+ env.ExecuteCodeLensCommand("cgo.go", source.CommandRegenerateCgo)
+ env.Await(EmptyDiagnostics("cgo.go"))
+ })
+}
+
+func TestGCDetails(t *testing.T) {
+ testenv.NeedsGo1Point(t, 15)
+ if runtime.GOOS == "android" {
+ t.Skipf("the gc details code lens doesn't work on Android")
+ }
+
+ const mod = `
+-- go.mod --
+module mod.com
+
+go 1.15
+-- main.go --
+package main
+
+import "fmt"
+
+func main() {
+ var x string
+ fmt.Println(x)
+}
+`
+ withOptions(
+ EditorConfig{CodeLens: map[string]bool{"gc_details": true}},
+ ).run(t, mod, func(t *testing.T, env *Env) {
+ env.Await(InitialWorkspaceLoad)
+ env.OpenFile("main.go")
+ env.ExecuteCodeLensCommand("main.go", source.CommandToggleDetails)
+ d := &protocol.PublishDiagnosticsParams{}
+ env.Await(
+ OnceMet(
+ DiagnosticAt("main.go", 6, 12),
+ ReadDiagnostics("main.go", d),
+ ),
+ )
+ // Confirm that the diagnostics come from the gc details code lens.
+ var found bool
+ for _, d := range d.Diagnostics {
+ if d.Severity != protocol.SeverityInformation {
+ t.Fatalf("unexpected diagnostic severity %v, wanted Information", d.Severity)
+ }
+ if strings.Contains(d.Message, "x escapes") {
+ found = true
}
}
- if _, err := env.Editor.Server.ExecuteCommand(env.Ctx, &protocol.ExecuteCommandParams{
- Command: lens.Command.Command,
- Arguments: lens.Command.Arguments,
- }); err != nil {
- t.Fatal(err)
+ if !found {
+ t.Fatalf(`expected to find diagnostic with message "escape(x escapes to heap)", found none`)
}
- env.Await(EmptyDiagnostics("cgo.go"))
+ // Toggle the GC details code lens again so now it should be off.
+ env.ExecuteCodeLensCommand("main.go", source.CommandToggleDetails)
+ env.Await(
+ EmptyDiagnostics("main.go"),
+ )
})
}
diff --git a/gopls/internal/regtest/wrappers.go b/gopls/internal/regtest/wrappers.go
index 8d8136f..294d303 100644
--- a/gopls/internal/regtest/wrappers.go
+++ b/gopls/internal/regtest/wrappers.go
@@ -10,6 +10,7 @@
"golang.org/x/tools/internal/lsp/fake"
"golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/lsp/source"
errors "golang.org/x/xerrors"
)
@@ -259,6 +260,29 @@
return lens
}
+// ExecuteCodeLensCommand executes the command for the code lens matching the
+// given command name.
+func (e *Env) ExecuteCodeLensCommand(path string, cmd *source.Command) {
+ lenses := e.CodeLens(path)
+ var lens protocol.CodeLens
+ var found bool
+ for _, l := range lenses {
+ if l.Command.Command == cmd.Name {
+ lens = l
+ found = true
+ }
+ }
+ if !found {
+ e.T.Fatalf("found no command with the title %s", cmd.Name)
+ }
+ if _, err := e.Editor.Server.ExecuteCommand(e.Ctx, &protocol.ExecuteCommandParams{
+ Command: lens.Command.Command,
+ Arguments: lens.Command.Arguments,
+ }); err != nil {
+ e.T.Fatal(err)
+ }
+}
+
// References calls textDocument/references for the given path at the given
// position.
func (e *Env) References(path string, pos fake.Pos) []protocol.Location {
diff --git a/internal/lsp/command.go b/internal/lsp/command.go
index 2f0f4d5..e6937ef 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -11,8 +11,6 @@
"fmt"
"io"
"io/ioutil"
- "log"
- "path"
"path/filepath"
"golang.org/x/tools/internal/event"
@@ -203,7 +201,7 @@
if err := source.UnmarshalArgs(args, &fileURI); err != nil {
return err
}
- pkgDir := span.URIFromPath(path.Dir(fileURI.Filename()))
+ pkgDir := span.URIFromPath(filepath.Dir(fileURI.Filename()))
s.gcOptimizationDetailsMu.Lock()
if _, ok := s.gcOptimizatonDetails[pkgDir]; ok {
delete(s.gcOptimizatonDetails, pkgDir)
@@ -211,8 +209,6 @@
s.gcOptimizatonDetails[pkgDir] = struct{}{}
}
s.gcOptimizationDetailsMu.Unlock()
- event.Log(ctx, fmt.Sprintf("gc_details %s now %v %v", pkgDir, s.gcOptimizatonDetails[pkgDir],
- s.gcOptimizatonDetails))
// need to recompute diagnostics.
// so find the snapshot
sv, err := s.session.ViewOf(fileURI)
@@ -290,7 +286,6 @@
var failedTests int
for _, funcName := range tests {
args := []string{pkgPath, "-v", "-count=1", "-run", fmt.Sprintf("^%s$", funcName)}
- log.Printf("running with these args: %v", args)
if err := snapshot.RunGoCommandPiped(ctx, "test", args, out, out); err != nil {
if errors.Is(err, context.Canceled) {
return err
diff --git a/internal/lsp/source/code_lens.go b/internal/lsp/source/code_lens.go
index 427b548..2aee889 100644
--- a/internal/lsp/source/code_lens.go
+++ b/internal/lsp/source/code_lens.go
@@ -242,14 +242,12 @@
if err != nil {
return nil, err
}
- return []protocol.CodeLens{
- {
- Range: rng,
- Command: protocol.Command{
- Title: "Toggle gc annotation details",
- Command: CommandToggleDetails.Name,
- Arguments: jsonArgs,
- },
+ return []protocol.CodeLens{{
+ Range: rng,
+ Command: protocol.Command{
+ Title: "Toggle gc annotation details",
+ Command: CommandToggleDetails.Name,
+ Arguments: jsonArgs,
},
- }, nil
+ }}, nil
}
diff --git a/internal/lsp/source/gc_annotations.go b/internal/lsp/source/gc_annotations.go
index 2d85026..8aa13cb 100644
--- a/internal/lsp/source/gc_annotations.go
+++ b/internal/lsp/source/gc_annotations.go
@@ -20,6 +20,7 @@
func GCOptimizationDetails(ctx context.Context, snapshot Snapshot, pkgDir span.URI) (map[VersionedFileIdentity][]*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
}
@@ -28,7 +29,15 @@
return nil, err
}
defer os.Remove(tmpFile.Name())
- args := []string{fmt.Sprintf("-gcflags=-json=0,%s", outDir),
+
+ outDirURI := span.URIFromPath(outDir)
+ // GC details doesn't handle Windows URIs in the form of "file:///C:/...",
+ // so rewrite them to "file://C:/...". See golang/go#41614.
+ if !strings.HasPrefix(outDir, "/") {
+ outDirURI = span.URI(strings.Replace(string(outDirURI), "file:///", "file://", 1))
+ }
+ args := []string{
+ fmt.Sprintf("-gcflags=-json=0,%s", outDirURI),
fmt.Sprintf("-o=%s", tmpFile.Name()),
pkgDir.Filename(),
}
@@ -44,100 +53,114 @@
opts := snapshot.View().Options()
var parseError error
for _, fn := range files {
- fname, v, err := parseDetailsFile(fn)
+ uri, diagnostics, err := parseDetailsFile(fn, opts)
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 {
+ fh := snapshot.FindFile(uri)
+ if fh == nil {
continue
}
- v = filterDiagnostics(v, opts)
- reports[x.VersionedFileIdentity()] = v
+ reports[fh.VersionedFileIdentity()] = diagnostics
}
return reports, parseError
}
-func filterDiagnostics(v []*Diagnostic, o *Options) []*Diagnostic {
- var ans []*Diagnostic
- for _, x := range v {
- if x.Source != "go compiler" {
- continue
- }
- if o.Annotations["noInline"] &&
- (strings.HasPrefix(x.Message, "canInline") ||
- strings.HasPrefix(x.Message, "cannotInline") ||
- strings.HasPrefix(x.Message, "inlineCall")) {
- continue
- } else if o.Annotations["noEscape"] &&
- (strings.HasPrefix(x.Message, "escape") || x.Message == "leak") {
- continue
- } else if o.Annotations["noNilcheck"] && strings.HasPrefix(x.Message, "nilcheck") {
- continue
- } else if o.Annotations["noBounds"] &&
- (strings.HasPrefix(x.Message, "isInBounds") ||
- strings.HasPrefix(x.Message, "isSliceInBounds")) {
- continue
- }
- ans = append(ans, x)
- }
- return ans
-}
-
-func parseDetailsFile(fn string) (string, []*Diagnostic, error) {
- buf, err := ioutil.ReadFile(fn)
+func parseDetailsFile(filename string, options *Options) (span.URI, []*Diagnostic, error) {
+ buf, err := ioutil.ReadFile(filename)
if err != nil {
- return "", nil, err // This is an internal error. Likely ever file will fail.
+ return "", nil, err
}
- var fname string
- var ans []*Diagnostic
- lines := bytes.Split(buf, []byte{'\n'})
- for i, l := range lines {
- if len(l) == 0 {
- continue
- }
+ var (
+ uri span.URI
+ i int
+ diagnostics []*Diagnostic
+ )
+ type metadata struct {
+ File string `json:"file,omitempty"`
+ }
+ for dec := json.NewDecoder(bytes.NewReader(buf)); dec.More(); {
+ // The first element always contains metadata.
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)
+ i++
+ m := new(metadata)
+ if err := dec.Decode(m); err != nil {
+ return "", nil, err
}
- fname = x["file"].(string)
+ if !strings.HasSuffix(m.File, ".go") {
+ continue // <autogenerated>
+ }
+ uri = span.URIFromPath(m.File)
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)
+ d := new(protocol.Diagnostic)
+ if err := dec.Decode(d); err != nil {
+ return "", nil, err
}
- 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)
+ msg := d.Code.(string)
+ if msg != "" {
+ msg = fmt.Sprintf("%s(%s)", msg, d.Message)
}
- x := Diagnostic{
- Range: y.Range,
- Message: msg,
- Source: y.Source,
- Severity: y.Severity,
+ if skipDiagnostic(msg, d.Source, options) {
+ continue
}
- for _, ri := range y.RelatedInformation {
- x.Related = append(x.Related, RelatedInformation{
+ var related []RelatedInformation
+ for _, ri := range d.RelatedInformation {
+ related = append(related, RelatedInformation{
URI: ri.Location.URI.SpanURI(),
- Range: ri.Location.Range,
+ Range: zeroIndexedRange(ri.Location.Range),
Message: ri.Message,
})
}
- ans = append(ans, &x)
+ diagnostic := &Diagnostic{
+ Range: zeroIndexedRange(d.Range),
+ Message: msg,
+ Severity: d.Severity,
+ Source: d.Source,
+ Tags: d.Tags,
+ Related: related,
+ }
+ diagnostics = append(diagnostics, diagnostic)
+ i++
}
- return fname, ans, nil
+ return uri, diagnostics, nil
+}
+
+// skipDiagnostic reports whether a given diagnostic should be shown to the end
+// user, given the current options.
+func skipDiagnostic(msg, source string, o *Options) bool {
+ if source != "go compiler" {
+ return false
+ }
+ switch {
+ case o.Annotations["noInline"]:
+ return strings.HasPrefix(msg, "canInline") ||
+ strings.HasPrefix(msg, "cannotInline") ||
+ strings.HasPrefix(msg, "inlineCall")
+ case o.Annotations["noEscape"]:
+ return strings.HasPrefix(msg, "escape") || msg == "leak"
+ case o.Annotations["noNilcheck"]:
+ return strings.HasPrefix(msg, "nilcheck")
+ case o.Annotations["noBounds"]:
+ return strings.HasPrefix(msg, "isInBounds") ||
+ strings.HasPrefix(msg, "isSliceInBounds")
+ }
+ return false
+}
+
+// The range produced by the compiler is 1-indexed, so subtract range by 1.
+func zeroIndexedRange(rng protocol.Range) protocol.Range {
+ return protocol.Range{
+ Start: protocol.Position{
+ Line: rng.Start.Line - 1,
+ Character: rng.Start.Character - 1,
+ },
+ End: protocol.Position{
+ Line: rng.End.Line - 1,
+ Character: rng.End.Character - 1,
+ },
+ }
}
func findJSONFiles(dir string) ([]string, error) {