blob: 94109841237a40ce3c954e5ee5379e0da9b02723 [file] [log] [blame]
// Copyright 2023 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 workspace
import (
"strings"
"testing"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/test/compare"
. "golang.org/x/tools/gopls/internal/test/integration"
)
func TestQuickFix_UseModule(t *testing.T) {
t.Skip("temporary skip for golang/go#57979: with zero-config gopls these files are no longer orphaned")
const files = `
-- go.work --
go 1.20
use (
./a
)
-- a/go.mod --
module mod.com/a
go 1.18
-- a/main.go --
package main
import "mod.com/a/lib"
func main() {
_ = lib.C
}
-- a/lib/lib.go --
package lib
const C = "b"
-- b/go.mod --
module mod.com/b
go 1.18
-- b/main.go --
package main
import "mod.com/b/lib"
func main() {
_ = lib.C
}
-- b/lib/lib.go --
package lib
const C = "b"
`
for _, title := range []string{
"Use this module",
"Use all modules",
} {
t.Run(title, func(t *testing.T) {
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("b/main.go")
var d protocol.PublishDiagnosticsParams
env.AfterChange(ReadDiagnostics("b/main.go", &d))
fixes := env.GetQuickFixes("b/main.go", d.Diagnostics)
var toApply []protocol.CodeAction
for _, fix := range fixes {
if strings.Contains(fix.Title, title) {
toApply = append(toApply, fix)
}
}
if len(toApply) != 1 {
t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), title, toApply)
}
env.ApplyCodeAction(toApply[0])
env.AfterChange(NoDiagnostics())
want := `go 1.20
use (
./a
./b
)
`
got := env.ReadWorkspaceFile("go.work")
if diff := compare.Text(want, got); diff != "" {
t.Errorf("unexpected go.work content:\n%s", diff)
}
})
})
}
}
func TestQuickFix_AddGoWork(t *testing.T) {
t.Skip("temporary skip for golang/go#57979: with zero-config gopls these files are no longer orphaned")
const files = `
-- a/go.mod --
module mod.com/a
go 1.18
-- a/main.go --
package main
import "mod.com/a/lib"
func main() {
_ = lib.C
}
-- a/lib/lib.go --
package lib
const C = "b"
-- b/go.mod --
module mod.com/b
go 1.18
-- b/main.go --
package main
import "mod.com/b/lib"
func main() {
_ = lib.C
}
-- b/lib/lib.go --
package lib
const C = "b"
`
tests := []struct {
name string
file string
title string
want string // expected go.work content, excluding go directive line
}{
{
"use b",
"b/main.go",
"Add a go.work file using this module",
`
use ./b
`,
},
{
"use a",
"a/main.go",
"Add a go.work file using this module",
`
use ./a
`,
},
{
"use all",
"a/main.go",
"Add a go.work file using all modules",
`
use (
./a
./b
)
`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile(test.file)
var d protocol.PublishDiagnosticsParams
env.AfterChange(ReadDiagnostics(test.file, &d))
fixes := env.GetQuickFixes(test.file, d.Diagnostics)
var toApply []protocol.CodeAction
for _, fix := range fixes {
if strings.Contains(fix.Title, test.title) {
toApply = append(toApply, fix)
}
}
if len(toApply) != 1 {
t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), test.title, toApply)
}
env.ApplyCodeAction(toApply[0])
env.AfterChange(
NoDiagnostics(ForFile(test.file)),
)
got := env.ReadWorkspaceFile("go.work")
// Ignore the `go` directive, which we assume is on the first line of
// the go.work file. This allows the test to be independent of go version.
got = strings.Join(strings.Split(got, "\n")[1:], "\n")
if diff := compare.Text(test.want, got); diff != "" {
t.Errorf("unexpected go.work content:\n%s", diff)
}
})
})
}
}
func TestQuickFix_UnsavedGoWork(t *testing.T) {
t.Skip("temporary skip for golang/go#57979: with zero-config gopls these files are no longer orphaned")
const files = `
-- go.work --
go 1.21
use (
./a
)
-- a/go.mod --
module mod.com/a
go 1.18
-- a/main.go --
package main
func main() {}
-- b/go.mod --
module mod.com/b
go 1.18
-- b/main.go --
package main
func main() {}
`
for _, title := range []string{
"Use this module",
"Use all modules",
} {
t.Run(title, func(t *testing.T) {
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("go.work")
env.OpenFile("b/main.go")
env.RegexpReplace("go.work", "go 1.21", "go 1.21 // arbitrary comment")
var d protocol.PublishDiagnosticsParams
env.AfterChange(ReadDiagnostics("b/main.go", &d))
fixes := env.GetQuickFixes("b/main.go", d.Diagnostics)
var toApply []protocol.CodeAction
for _, fix := range fixes {
if strings.Contains(fix.Title, title) {
toApply = append(toApply, fix)
}
}
if len(toApply) != 1 {
t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), title, toApply)
}
fix := toApply[0]
err := env.Editor.ApplyCodeAction(env.Ctx, fix)
if err == nil {
t.Fatalf("codeAction(%q) succeeded unexpectedly", fix.Title)
}
if got := err.Error(); !strings.Contains(got, "must save") {
t.Errorf("codeAction(%q) returned error %q, want containing \"must save\"", fix.Title, err)
}
})
})
}
}
func TestQuickFix_GOWORKOff(t *testing.T) {
t.Skip("temporary skip for golang/go#57979: with zero-config gopls these files are no longer orphaned")
const files = `
-- go.work --
go 1.21
use (
./a
)
-- a/go.mod --
module mod.com/a
go 1.18
-- a/main.go --
package main
func main() {}
-- b/go.mod --
module mod.com/b
go 1.18
-- b/main.go --
package main
func main() {}
`
for _, title := range []string{
"Use this module",
"Use all modules",
} {
t.Run(title, func(t *testing.T) {
WithOptions(
EnvVars{"GOWORK": "off"},
).Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("go.work")
env.OpenFile("b/main.go")
var d protocol.PublishDiagnosticsParams
env.AfterChange(ReadDiagnostics("b/main.go", &d))
fixes := env.GetQuickFixes("b/main.go", d.Diagnostics)
var toApply []protocol.CodeAction
for _, fix := range fixes {
if strings.Contains(fix.Title, title) {
toApply = append(toApply, fix)
}
}
if len(toApply) != 1 {
t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), title, toApply)
}
fix := toApply[0]
err := env.Editor.ApplyCodeAction(env.Ctx, fix)
if err == nil {
t.Fatalf("codeAction(%q) succeeded unexpectedly", fix.Title)
}
if got := err.Error(); !strings.Contains(got, "GOWORK=off") {
t.Errorf("codeAction(%q) returned error %q, want containing \"GOWORK=off\"", fix.Title, err)
}
})
})
}
}
func TestStubMethods64087(t *testing.T) {
// We can't use the @fix or @quickfixerr or @codeaction
// because the error now reported by the corrected logic
// is internal and silently causes no fix to be offered.
//
// See also the similar TestStubMethods64545 below.
const files = `
This is a regression test for a panic (issue #64087) in stub methods.
The illegal expression int("") caused a "cannot convert" error that
spuriously triggered the "stub methods" in a function whose return
statement had too many operands, leading to an out-of-bounds index.
-- go.mod --
module mod.com
go 1.18
-- a.go --
package a
func f() error {
return nil, myerror{int("")}
}
type myerror struct{any}
`
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("a.go")
// Expect a "wrong result count" diagnostic.
var d protocol.PublishDiagnosticsParams
env.AfterChange(ReadDiagnostics("a.go", &d))
// In no particular order, we expect:
// "...too many return values..." (compiler)
// "...cannot convert..." (compiler)
// and possibly:
// "...too many return values..." (fillreturns)
// We check only for the first of these.
found := false
for i, diag := range d.Diagnostics {
t.Logf("Diagnostics[%d] = %q (%s)", i, diag.Message, diag.Source)
if strings.Contains(diag.Message, "too many return") {
found = true
}
}
if !found {
t.Fatalf("Expected WrongResultCount diagnostic not found.")
}
// GetQuickFixes should not panic (the original bug).
fixes := env.GetQuickFixes("a.go", d.Diagnostics)
// We should not be offered a "stub methods" fix.
for _, fix := range fixes {
if strings.Contains(fix.Title, "Implement error") {
t.Errorf("unexpected 'stub methods' fix: %#v", fix)
}
}
})
}
func TestStubMethods64545(t *testing.T) {
// We can't use the @fix or @quickfixerr or @codeaction
// because the error now reported by the corrected logic
// is internal and silently causes no fix to be offered.
//
// TODO(adonovan): we may need to generalize this test and
// TestStubMethods64087 if this happens a lot.
const files = `
This is a regression test for a panic (issue #64545) in stub methods.
The illegal expression int("") caused a "cannot convert" error that
spuriously triggered the "stub methods" in a function whose var
spec had no RHS values, leading to an out-of-bounds index.
-- go.mod --
module mod.com
go 1.18
-- a.go --
package a
var _ [int("")]byte
`
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("a.go")
// Expect a "cannot convert" diagnostic, and perhaps others.
var d protocol.PublishDiagnosticsParams
env.AfterChange(ReadDiagnostics("a.go", &d))
found := false
for i, diag := range d.Diagnostics {
t.Logf("Diagnostics[%d] = %q (%s)", i, diag.Message, diag.Source)
if strings.Contains(diag.Message, "cannot convert") {
found = true
}
}
if !found {
t.Fatalf("Expected 'cannot convert' diagnostic not found.")
}
// GetQuickFixes should not panic (the original bug).
fixes := env.GetQuickFixes("a.go", d.Diagnostics)
// We should not be offered a "stub methods" fix.
for _, fix := range fixes {
if strings.Contains(fix.Title, "Implement error") {
t.Errorf("unexpected 'stub methods' fix: %#v", fix)
}
}
})
}
// quick fix didn't offer add imports
func TestIssue70755(t *testing.T) {
files := `
-- go.mod --
module failure.com
go 1.23
-- bar/bar.go --
package notbar
type NotBar struct{}
-- baz/baz.go --
package baz
type Baz struct{}
-- foo/foo.go --
package foo
type foo struct {
bar notbar.NotBar
bzz baz.Baz
}
`
Run(t, files, func(t *testing.T, env *Env) {
env.OpenFile("foo/foo.go")
var d protocol.PublishDiagnosticsParams
env.AfterChange(ReadDiagnostics("foo/foo.go", &d))
// should get two, one for undefined notbar
// and one for undefined baz
fixes := env.GetQuickFixes("foo/foo.go", d.Diagnostics)
if len(fixes) != 2 {
t.Fatalf("got %v, want 2 quick fixes", fixes)
}
good := 0
failures := ""
for _, f := range fixes {
ti := f.Title
// these may be overly white-space sensitive
if ti == "Add import: notbar \"failure.com/bar\"" ||
ti == "Add import: \"failure.com/baz\"" {
good++
} else {
failures += ti
}
}
if good != 2 {
t.Errorf("failed to find\n%q, got\n%q\n%q", failures, fixes[0].Title,
fixes[1].Title)
}
})
}