blob: 6f7c8e854d009b53d04e11cb47ae269fe29902f0 [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("unexpeced 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 @suggestedfixerr or @codeactionerr
// 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 @suggestedfixerr or @codeactionerr
// 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)
}
}
})
}