golang.org/x/tools/gopls@v0.15.3/internal/test/integration/workspace/quickfix_test.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package workspace
     6  
     7  import (
     8  	"strings"
     9  	"testing"
    10  
    11  	"golang.org/x/tools/gopls/internal/protocol"
    12  	"golang.org/x/tools/gopls/internal/test/compare"
    13  
    14  	. "golang.org/x/tools/gopls/internal/test/integration"
    15  )
    16  
    17  func TestQuickFix_UseModule(t *testing.T) {
    18  	t.Skip("temporary skip for golang/go#57979: with zero-config gopls these files are no longer orphaned")
    19  
    20  	const files = `
    21  -- go.work --
    22  go 1.20
    23  
    24  use (
    25  	./a
    26  )
    27  -- a/go.mod --
    28  module mod.com/a
    29  
    30  go 1.18
    31  
    32  -- a/main.go --
    33  package main
    34  
    35  import "mod.com/a/lib"
    36  
    37  func main() {
    38  	_ = lib.C
    39  }
    40  
    41  -- a/lib/lib.go --
    42  package lib
    43  
    44  const C = "b"
    45  -- b/go.mod --
    46  module mod.com/b
    47  
    48  go 1.18
    49  
    50  -- b/main.go --
    51  package main
    52  
    53  import "mod.com/b/lib"
    54  
    55  func main() {
    56  	_ = lib.C
    57  }
    58  
    59  -- b/lib/lib.go --
    60  package lib
    61  
    62  const C = "b"
    63  `
    64  
    65  	for _, title := range []string{
    66  		"Use this module",
    67  		"Use all modules",
    68  	} {
    69  		t.Run(title, func(t *testing.T) {
    70  			Run(t, files, func(t *testing.T, env *Env) {
    71  				env.OpenFile("b/main.go")
    72  				var d protocol.PublishDiagnosticsParams
    73  				env.AfterChange(ReadDiagnostics("b/main.go", &d))
    74  				fixes := env.GetQuickFixes("b/main.go", d.Diagnostics)
    75  				var toApply []protocol.CodeAction
    76  				for _, fix := range fixes {
    77  					if strings.Contains(fix.Title, title) {
    78  						toApply = append(toApply, fix)
    79  					}
    80  				}
    81  				if len(toApply) != 1 {
    82  					t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), title, toApply)
    83  				}
    84  				env.ApplyCodeAction(toApply[0])
    85  				env.AfterChange(NoDiagnostics())
    86  				want := `go 1.20
    87  
    88  use (
    89  	./a
    90  	./b
    91  )
    92  `
    93  				got := env.ReadWorkspaceFile("go.work")
    94  				if diff := compare.Text(want, got); diff != "" {
    95  					t.Errorf("unexpeced go.work content:\n%s", diff)
    96  				}
    97  			})
    98  		})
    99  	}
   100  }
   101  
   102  func TestQuickFix_AddGoWork(t *testing.T) {
   103  	t.Skip("temporary skip for golang/go#57979: with zero-config gopls these files are no longer orphaned")
   104  
   105  	const files = `
   106  -- a/go.mod --
   107  module mod.com/a
   108  
   109  go 1.18
   110  
   111  -- a/main.go --
   112  package main
   113  
   114  import "mod.com/a/lib"
   115  
   116  func main() {
   117  	_ = lib.C
   118  }
   119  
   120  -- a/lib/lib.go --
   121  package lib
   122  
   123  const C = "b"
   124  -- b/go.mod --
   125  module mod.com/b
   126  
   127  go 1.18
   128  
   129  -- b/main.go --
   130  package main
   131  
   132  import "mod.com/b/lib"
   133  
   134  func main() {
   135  	_ = lib.C
   136  }
   137  
   138  -- b/lib/lib.go --
   139  package lib
   140  
   141  const C = "b"
   142  `
   143  
   144  	tests := []struct {
   145  		name  string
   146  		file  string
   147  		title string
   148  		want  string // expected go.work content, excluding go directive line
   149  	}{
   150  		{
   151  			"use b",
   152  			"b/main.go",
   153  			"Add a go.work file using this module",
   154  			`
   155  use ./b
   156  `,
   157  		},
   158  		{
   159  			"use a",
   160  			"a/main.go",
   161  			"Add a go.work file using this module",
   162  			`
   163  use ./a
   164  `,
   165  		},
   166  		{
   167  			"use all",
   168  			"a/main.go",
   169  			"Add a go.work file using all modules",
   170  			`
   171  use (
   172  	./a
   173  	./b
   174  )
   175  `,
   176  		},
   177  	}
   178  
   179  	for _, test := range tests {
   180  		t.Run(test.name, func(t *testing.T) {
   181  			Run(t, files, func(t *testing.T, env *Env) {
   182  				env.OpenFile(test.file)
   183  				var d protocol.PublishDiagnosticsParams
   184  				env.AfterChange(ReadDiagnostics(test.file, &d))
   185  				fixes := env.GetQuickFixes(test.file, d.Diagnostics)
   186  				var toApply []protocol.CodeAction
   187  				for _, fix := range fixes {
   188  					if strings.Contains(fix.Title, test.title) {
   189  						toApply = append(toApply, fix)
   190  					}
   191  				}
   192  				if len(toApply) != 1 {
   193  					t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), test.title, toApply)
   194  				}
   195  				env.ApplyCodeAction(toApply[0])
   196  				env.AfterChange(
   197  					NoDiagnostics(ForFile(test.file)),
   198  				)
   199  
   200  				got := env.ReadWorkspaceFile("go.work")
   201  				// Ignore the `go` directive, which we assume is on the first line of
   202  				// the go.work file. This allows the test to be independent of go version.
   203  				got = strings.Join(strings.Split(got, "\n")[1:], "\n")
   204  				if diff := compare.Text(test.want, got); diff != "" {
   205  					t.Errorf("unexpected go.work content:\n%s", diff)
   206  				}
   207  			})
   208  		})
   209  	}
   210  }
   211  
   212  func TestQuickFix_UnsavedGoWork(t *testing.T) {
   213  	t.Skip("temporary skip for golang/go#57979: with zero-config gopls these files are no longer orphaned")
   214  
   215  	const files = `
   216  -- go.work --
   217  go 1.21
   218  
   219  use (
   220  	./a
   221  )
   222  -- a/go.mod --
   223  module mod.com/a
   224  
   225  go 1.18
   226  
   227  -- a/main.go --
   228  package main
   229  
   230  func main() {}
   231  -- b/go.mod --
   232  module mod.com/b
   233  
   234  go 1.18
   235  
   236  -- b/main.go --
   237  package main
   238  
   239  func main() {}
   240  `
   241  
   242  	for _, title := range []string{
   243  		"Use this module",
   244  		"Use all modules",
   245  	} {
   246  		t.Run(title, func(t *testing.T) {
   247  			Run(t, files, func(t *testing.T, env *Env) {
   248  				env.OpenFile("go.work")
   249  				env.OpenFile("b/main.go")
   250  				env.RegexpReplace("go.work", "go 1.21", "go 1.21 // arbitrary comment")
   251  				var d protocol.PublishDiagnosticsParams
   252  				env.AfterChange(ReadDiagnostics("b/main.go", &d))
   253  				fixes := env.GetQuickFixes("b/main.go", d.Diagnostics)
   254  				var toApply []protocol.CodeAction
   255  				for _, fix := range fixes {
   256  					if strings.Contains(fix.Title, title) {
   257  						toApply = append(toApply, fix)
   258  					}
   259  				}
   260  				if len(toApply) != 1 {
   261  					t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), title, toApply)
   262  				}
   263  				fix := toApply[0]
   264  				err := env.Editor.ApplyCodeAction(env.Ctx, fix)
   265  				if err == nil {
   266  					t.Fatalf("codeAction(%q) succeeded unexpectedly", fix.Title)
   267  				}
   268  
   269  				if got := err.Error(); !strings.Contains(got, "must save") {
   270  					t.Errorf("codeAction(%q) returned error %q, want containing \"must save\"", fix.Title, err)
   271  				}
   272  			})
   273  		})
   274  	}
   275  }
   276  
   277  func TestQuickFix_GOWORKOff(t *testing.T) {
   278  	t.Skip("temporary skip for golang/go#57979: with zero-config gopls these files are no longer orphaned")
   279  
   280  	const files = `
   281  -- go.work --
   282  go 1.21
   283  
   284  use (
   285  	./a
   286  )
   287  -- a/go.mod --
   288  module mod.com/a
   289  
   290  go 1.18
   291  
   292  -- a/main.go --
   293  package main
   294  
   295  func main() {}
   296  -- b/go.mod --
   297  module mod.com/b
   298  
   299  go 1.18
   300  
   301  -- b/main.go --
   302  package main
   303  
   304  func main() {}
   305  `
   306  
   307  	for _, title := range []string{
   308  		"Use this module",
   309  		"Use all modules",
   310  	} {
   311  		t.Run(title, func(t *testing.T) {
   312  			WithOptions(
   313  				EnvVars{"GOWORK": "off"},
   314  			).Run(t, files, func(t *testing.T, env *Env) {
   315  				env.OpenFile("go.work")
   316  				env.OpenFile("b/main.go")
   317  				var d protocol.PublishDiagnosticsParams
   318  				env.AfterChange(ReadDiagnostics("b/main.go", &d))
   319  				fixes := env.GetQuickFixes("b/main.go", d.Diagnostics)
   320  				var toApply []protocol.CodeAction
   321  				for _, fix := range fixes {
   322  					if strings.Contains(fix.Title, title) {
   323  						toApply = append(toApply, fix)
   324  					}
   325  				}
   326  				if len(toApply) != 1 {
   327  					t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), title, toApply)
   328  				}
   329  				fix := toApply[0]
   330  				err := env.Editor.ApplyCodeAction(env.Ctx, fix)
   331  				if err == nil {
   332  					t.Fatalf("codeAction(%q) succeeded unexpectedly", fix.Title)
   333  				}
   334  
   335  				if got := err.Error(); !strings.Contains(got, "GOWORK=off") {
   336  					t.Errorf("codeAction(%q) returned error %q, want containing \"GOWORK=off\"", fix.Title, err)
   337  				}
   338  			})
   339  		})
   340  	}
   341  }
   342  
   343  func TestStubMethods64087(t *testing.T) {
   344  	// We can't use the @fix or @suggestedfixerr or @codeactionerr
   345  	// because the error now reported by the corrected logic
   346  	// is internal and silently causes no fix to be offered.
   347  	//
   348  	// See also the similar TestStubMethods64545 below.
   349  
   350  	const files = `
   351  This is a regression test for a panic (issue #64087) in stub methods.
   352  
   353  The illegal expression int("") caused a "cannot convert" error that
   354  spuriously triggered the "stub methods" in a function whose return
   355  statement had too many operands, leading to an out-of-bounds index.
   356  
   357  -- go.mod --
   358  module mod.com
   359  go 1.18
   360  
   361  -- a.go --
   362  package a
   363  
   364  func f() error {
   365  	return nil, myerror{int("")}
   366  }
   367  
   368  type myerror struct{any}
   369  `
   370  	Run(t, files, func(t *testing.T, env *Env) {
   371  		env.OpenFile("a.go")
   372  
   373  		// Expect a "wrong result count" diagnostic.
   374  		var d protocol.PublishDiagnosticsParams
   375  		env.AfterChange(ReadDiagnostics("a.go", &d))
   376  
   377  		// In no particular order, we expect:
   378  		//  "...too many return values..." (compiler)
   379  		//  "...cannot convert..." (compiler)
   380  		// and possibly:
   381  		//  "...too many return values..." (fillreturns)
   382  		// We check only for the first of these.
   383  		found := false
   384  		for i, diag := range d.Diagnostics {
   385  			t.Logf("Diagnostics[%d] = %q (%s)", i, diag.Message, diag.Source)
   386  			if strings.Contains(diag.Message, "too many return") {
   387  				found = true
   388  			}
   389  		}
   390  		if !found {
   391  			t.Fatalf("Expected WrongResultCount diagnostic not found.")
   392  		}
   393  
   394  		// GetQuickFixes should not panic (the original bug).
   395  		fixes := env.GetQuickFixes("a.go", d.Diagnostics)
   396  
   397  		// We should not be offered a "stub methods" fix.
   398  		for _, fix := range fixes {
   399  			if strings.Contains(fix.Title, "Implement error") {
   400  				t.Errorf("unexpected 'stub methods' fix: %#v", fix)
   401  			}
   402  		}
   403  	})
   404  }
   405  
   406  func TestStubMethods64545(t *testing.T) {
   407  	// We can't use the @fix or @suggestedfixerr or @codeactionerr
   408  	// because the error now reported by the corrected logic
   409  	// is internal and silently causes no fix to be offered.
   410  	//
   411  	// TODO(adonovan): we may need to generalize this test and
   412  	// TestStubMethods64087 if this happens a lot.
   413  
   414  	const files = `
   415  This is a regression test for a panic (issue #64545) in stub methods.
   416  
   417  The illegal expression int("") caused a "cannot convert" error that
   418  spuriously triggered the "stub methods" in a function whose var
   419  spec had no RHS values, leading to an out-of-bounds index.
   420  
   421  -- go.mod --
   422  module mod.com
   423  go 1.18
   424  
   425  -- a.go --
   426  package a
   427  
   428  var _ [int("")]byte
   429  `
   430  	Run(t, files, func(t *testing.T, env *Env) {
   431  		env.OpenFile("a.go")
   432  
   433  		// Expect a "cannot convert" diagnostic, and perhaps others.
   434  		var d protocol.PublishDiagnosticsParams
   435  		env.AfterChange(ReadDiagnostics("a.go", &d))
   436  
   437  		found := false
   438  		for i, diag := range d.Diagnostics {
   439  			t.Logf("Diagnostics[%d] = %q (%s)", i, diag.Message, diag.Source)
   440  			if strings.Contains(diag.Message, "cannot convert") {
   441  				found = true
   442  			}
   443  		}
   444  		if !found {
   445  			t.Fatalf("Expected 'cannot convert' diagnostic not found.")
   446  		}
   447  
   448  		// GetQuickFixes should not panic (the original bug).
   449  		fixes := env.GetQuickFixes("a.go", d.Diagnostics)
   450  
   451  		// We should not be offered a "stub methods" fix.
   452  		for _, fix := range fixes {
   453  			if strings.Contains(fix.Title, "Implement error") {
   454  				t.Errorf("unexpected 'stub methods' fix: %#v", fix)
   455  			}
   456  		}
   457  	})
   458  }