From 1ba3d7bc0e3ba64e9293922c84c938e6adfb0708 Mon Sep 17 00:00:00 2001 From: Ilya Ilyinykh Date: Fri, 5 Dec 2025 20:09:23 +0300 Subject: [PATCH 1/2] gopls/internal/golang: support variadic functions and constructors in generated tests Add `IsVariadic` flag to function metadata and expand variadic arguments with `...` in test templates. Update constructor handling to propagate variadic information. Refactor import collection helper and add varargs test cases. Fixes #76682 --- gopls/internal/golang/addtest.go | 28 ++- .../marker/testdata/codeaction/addtest.txt | 179 ++++++++++++++++++ 2 files changed, 204 insertions(+), 3 deletions(-) diff --git a/gopls/internal/golang/addtest.go b/gopls/internal/golang/addtest.go index c84a0f0be29..d4e62c77d13 100644 --- a/gopls/internal/golang/addtest.go +++ b/gopls/internal/golang/addtest.go @@ -91,6 +91,7 @@ func {{.TestFuncName}}(t *{{.TestingPackageName}}.T) { {{- if ne $index 0}}, {{end}} {{- if .Name}}tt.{{.Name}}{{else}}{{.Value}}{{end}} {{- end -}} + {{- if and .Receiver.Constructor.IsVariadic (not .Receiver.Constructor.IsUnusedVariadic) }}...{{- end -}} ) {{- /* Handles the error return from constructor. */}} @@ -123,6 +124,7 @@ func {{.TestFuncName}}(t *{{.TestingPackageName}}.T) { {{- if ne $index 0}}, {{end}} {{- if .Name}}tt.{{.Name}}{{else}}{{.Value}}{{end}} {{- end -}} + {{- if and .Func.IsVariadic (not .Func.IsUnusedVariadic) }}...{{- end -}} ) {{- /* Handles the returned error before the rest of return value. */}} @@ -167,6 +169,12 @@ type function struct { Name string Args []field Results []field + // IsVariadic holds information if the function is variadic. + // In case of the IsVariadic=true must expand it in the function-under-test call. + IsVariadic bool + // IsUnusedVariadic holds information if the function's variadic args is not used. + // In this case ... must be skipped. + IsUnusedVariadic bool } type receiver struct { @@ -238,7 +246,7 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. extraImports = make(map[string]string) // imports to add to test file ) - var collectImports = func(file *ast.File) (map[string]string, error) { + collectImports := func(file *ast.File) (map[string]string, error) { imps := make(map[string]string) for _, spec := range file.Imports { // TODO(hxjiang): support dot imports. @@ -478,7 +486,8 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. PackageName: qual(pkg.Types()), TestFuncName: testName, Func: function{ - Name: fn.Name(), + Name: fn.Name(), + IsVariadic: sig.Variadic(), }, } @@ -493,6 +502,11 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. if i == 0 && isContextType(typ) { f.Value = qual(types.NewPackage("context", "context")) + ".Background()" } else if name == "" || name == "_" { + if data.Func.IsVariadic && sig.Params().Len()-1 == i { + // skip the last argument, don't need to render it. + data.Func.IsUnusedVariadic = true + continue + } f.Value, _ = typesinternal.ZeroString(typ, qual) } else { f.Name = name @@ -619,7 +633,10 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. } if constructor != nil { - data.Receiver.Constructor = &function{Name: constructor.Name()} + data.Receiver.Constructor = &function{ + Name: constructor.Name(), + IsVariadic: constructor.Signature().Variadic(), + } for i := range constructor.Signature().Params().Len() { param := constructor.Signature().Params().At(i) name, typ := param.Name(), param.Type() @@ -627,6 +644,11 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. if i == 0 && isContextType(typ) { f.Value = qual(types.NewPackage("context", "context")) + ".Background()" } else if name == "" || name == "_" { + if data.Receiver.Constructor.IsVariadic && sig.Params().Len()-1 == i { + // skip the last argument, don't need to render it. + data.Receiver.Constructor.IsUnusedVariadic = true + continue + } f.Value, _ = typesinternal.ZeroString(typ, qual) } else { f.Name = name diff --git a/gopls/internal/test/marker/testdata/codeaction/addtest.txt b/gopls/internal/test/marker/testdata/codeaction/addtest.txt index c084339fdb2..7e75cd17f9e 100644 --- a/gopls/internal/test/marker/testdata/codeaction/addtest.txt +++ b/gopls/internal/test/marker/testdata/codeaction/addtest.txt @@ -1540,3 +1540,182 @@ type Foo struct {} func NewFoo() func (*Foo) Method[T any]() {} // no suggested fix +-- varargsinput/varargsinput.go -- +package main + +func Function(args ...string) (out, out1, out2 string) {return args[0], "", ""} //@codeaction("Function", "source.addTest", edit=function_varargsinput) + +type Foo struct {} + +func NewFoo(args ...string) (*Foo, error) { + _ = args + return nil, nil +} + +func (*Foo) Method(args ...string) (out, out1, out2 string) {return args[0], "", ""} //@codeaction("Method", "source.addTest", edit=method_varargsinput) +-- varargsinput/varargsinput_test.go -- +package main_test +-- @function_varargsinput/varargsinput/varargsinput_test.go -- +@@ -2 +2,34 @@ ++ ++import ( ++ "testing" ++ ++ "golang.org/lsptests/addtest/varargsinput" ++) ++ ++func TestFunction(t *testing.T) { ++ tests := []struct { ++ name string // description of this test case ++ // Named input parameters for target function. ++ args []string ++ want string ++ want2 string ++ want3 string ++ }{ ++ // TODO: Add test cases. ++ } ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ got, got2, got3 := main.Function(tt.args...) ++ // TODO: update the condition below to compare got with tt.want. ++ if true { ++ t.Errorf("Function() = %v, want %v", got, tt.want) ++ } ++ if true { ++ t.Errorf("Function() = %v, want %v", got2, tt.want2) ++ } ++ if true { ++ t.Errorf("Function() = %v, want %v", got3, tt.want3) ++ } ++ }) ++ } ++} +-- @method_varargsinput/varargsinput/varargsinput_test.go -- +@@ -2 +2,40 @@ ++ ++import ( ++ "testing" ++ ++ "golang.org/lsptests/addtest/varargsinput" ++) ++ ++func TestFoo_Method(t *testing.T) { ++ tests := []struct { ++ name string // description of this test case ++ // Named input parameters for receiver constructor. ++ cargs []string ++ // Named input parameters for target function. ++ args []string ++ want string ++ want2 string ++ want3 string ++ }{ ++ // TODO: Add test cases. ++ } ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ f, err := main.NewFoo(tt.cargs...) ++ if err != nil { ++ t.Fatalf("could not construct receiver type: %v", err) ++ } ++ got, got2, got3 := f.Method(tt.args...) ++ // TODO: update the condition below to compare got with tt.want. ++ if true { ++ t.Errorf("Method() = %v, want %v", got, tt.want) ++ } ++ if true { ++ t.Errorf("Method() = %v, want %v", got2, tt.want2) ++ } ++ if true { ++ t.Errorf("Method() = %v, want %v", got3, tt.want3) ++ } ++ }) ++ } ++} +-- unusedvarargsinput/unusedvarargsinput.go -- +package main + +func Function(_ ...string) (out, out1, out2 string) {return "", "", ""} //@codeaction("Function", "source.addTest", edit=function_unusedvarargsinput) + +type Foo struct {} + +func NewFoo(_ ...string) (*Foo, error) { + return nil, nil +} + +func (*Foo) Method(_ ...string) (out, out1, out2 string) {return "", "", ""} //@codeaction("Method", "source.addTest", edit=method_unusedvarargsinput) +-- unusedvarargsinput/unusedvarargsinput_test.go -- +package main_test +-- @function_unusedvarargsinput/unusedvarargsinput/unusedvarargsinput_test.go -- +@@ -2 +2,32 @@ ++ ++import ( ++ "testing" ++ ++ "golang.org/lsptests/addtest/unusedvarargsinput" ++) ++ ++func TestFunction(t *testing.T) { ++ tests := []struct { ++ name string // description of this test case ++ want string ++ want2 string ++ want3 string ++ }{ ++ // TODO: Add test cases. ++ } ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ got, got2, got3 := main.Function() ++ // TODO: update the condition below to compare got with tt.want. ++ if true { ++ t.Errorf("Function() = %v, want %v", got, tt.want) ++ } ++ if true { ++ t.Errorf("Function() = %v, want %v", got2, tt.want2) ++ } ++ if true { ++ t.Errorf("Function() = %v, want %v", got3, tt.want3) ++ } ++ }) ++ } ++} +-- @method_unusedvarargsinput/unusedvarargsinput/unusedvarargsinput_test.go -- +@@ -2 +2,36 @@ ++ ++import ( ++ "testing" ++ ++ "golang.org/lsptests/addtest/unusedvarargsinput" ++) ++ ++func TestFoo_Method(t *testing.T) { ++ tests := []struct { ++ name string // description of this test case ++ want string ++ want2 string ++ want3 string ++ }{ ++ // TODO: Add test cases. ++ } ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ f, err := main.NewFoo() ++ if err != nil { ++ t.Fatalf("could not construct receiver type: %v", err) ++ } ++ got, got2, got3 := f.Method() ++ // TODO: update the condition below to compare got with tt.want. ++ if true { ++ t.Errorf("Method() = %v, want %v", got, tt.want) ++ } ++ if true { ++ t.Errorf("Method() = %v, want %v", got2, tt.want2) ++ } ++ if true { ++ t.Errorf("Method() = %v, want %v", got3, tt.want3) ++ } ++ }) ++ } ++} From acb706d0ca00af12405b5adecce5d2bcc0429af1 Mon Sep 17 00:00:00 2001 From: Ilya Ilyinykh Date: Sun, 7 Dec 2025 19:10:30 +0300 Subject: [PATCH 2/2] refactor(golang): simplify unused parameter detection and variadic handling in test generation Introduce `isUnusedParameter` helper to replace repeated `name == "" || name == "_"` checks. Refactor logic for both functions and constructors to use the helper and correctly skip unused variadic arguments while generating test case structs. --- gopls/internal/golang/addtest.go | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/gopls/internal/golang/addtest.go b/gopls/internal/golang/addtest.go index d4e62c77d13..51664c58d98 100644 --- a/gopls/internal/golang/addtest.go +++ b/gopls/internal/golang/addtest.go @@ -495,18 +495,22 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. return typesinternal.IsTypeNamed(t, "context", "Context") } + isUnusedParameter := func(name string) bool { + return name == "" || name == "_" + } + for i := range sig.Params().Len() { param := sig.Params().At(i) name, typ := param.Name(), param.Type() f := field{Type: types.TypeString(typ, qual)} if i == 0 && isContextType(typ) { f.Value = qual(types.NewPackage("context", "context")) + ".Background()" - } else if name == "" || name == "_" { - if data.Func.IsVariadic && sig.Params().Len()-1 == i { - // skip the last argument, don't need to render it. - data.Func.IsUnusedVariadic = true - continue - } + } else if isUnusedParameter(name) && data.Func.IsVariadic && sig.Params().Len()-1 == i { + // The last argument is the variadic argument, and it's not used in the function body, + // so we don't need to render it in the test case struct. + data.Func.IsUnusedVariadic = true + continue + } else if isUnusedParameter(name) { f.Value, _ = typesinternal.ZeroString(typ, qual) } else { f.Name = name @@ -643,12 +647,12 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. f := field{Type: types.TypeString(typ, qual)} if i == 0 && isContextType(typ) { f.Value = qual(types.NewPackage("context", "context")) + ".Background()" - } else if name == "" || name == "_" { - if data.Receiver.Constructor.IsVariadic && sig.Params().Len()-1 == i { - // skip the last argument, don't need to render it. - data.Receiver.Constructor.IsUnusedVariadic = true - continue - } + } else if isUnusedParameter(name) && data.Receiver.Constructor.IsVariadic && constructor.Signature().Params().Len()-1 == i { + // The last argument is the variadic argument, and it's not used in the function body, + // so we don't need to render it in the test case struct. + data.Receiver.Constructor.IsUnusedVariadic = true + continue + } else if isUnusedParameter(name) { f.Value, _ = typesinternal.ZeroString(typ, qual) } else { f.Name = name