Skip to content

Commit

Permalink
gopls/internal/golang, go/ssa: remove unnamed input parameter
Browse files Browse the repository at this point in the history
- Unnamed/blank parameters are now assigned zero values, as
   they are not referenced within the function body.
- The `zeroString` function has been moved from `go/ssa` to
  `internal/typesinternal` for better organization.
- Honor the input parameter name from the function signature.
- Input parameters from both consutrctor and target functions
  are flattened into the test case struct. Potential field
  name duplication is handled by introducing prefixes.

For golang/vscode-go#1594

Change-Id: I8b56d8f3e0f0432d4f9fe269cc7ba86ea46decfc
Reviewed-on: https://go-review.googlesource.com/c/tools/+/626537
Reviewed-by: Robert Findley <rfindley@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Auto-Submit: Hongxiang Jiang <hxjiang@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
  • Loading branch information
h9jiang authored and gopherbot committed Nov 14, 2024
1 parent a8d0fa5 commit b4332e0
Show file tree
Hide file tree
Showing 9 changed files with 573 additions and 244 deletions.
48 changes: 8 additions & 40 deletions go/ssa/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import (
"go/token"
"go/types"
"strconv"
"strings"

"golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/typesinternal"
)

// NewConst returns a new constant of the specified value and type.
Expand Down Expand Up @@ -78,7 +78,13 @@ func zeroConst(t types.Type) *Const {
func (c *Const) RelString(from *types.Package) string {
var s string
if c.Value == nil {
s = zeroString(c.typ, from)
if _, ok := c.typ.(*types.TypeParam); ok {
// Type parameter's underlying type may be interface that is
// nillable. A better zero value of type parameter is *new(T).
s = typesinternal.ZeroString(c.typ, types.RelativeTo(from))
} else {
s = typesinternal.ZeroString(c.typ.Underlying(), types.RelativeTo(from))
}
} else if c.Value.Kind() == constant.String {
s = constant.StringVal(c.Value)
const max = 20
Expand All @@ -93,44 +99,6 @@ func (c *Const) RelString(from *types.Package) string {
return s + ":" + relType(c.Type(), from)
}

// zeroString returns the string representation of the "zero" value of the type t.
func zeroString(t types.Type, from *types.Package) string {
switch t := t.(type) {
case *types.Basic:
switch {
case t.Info()&types.IsBoolean != 0:
return "false"
case t.Info()&types.IsNumeric != 0:
return "0"
case t.Info()&types.IsString != 0:
return `""`
case t.Kind() == types.UnsafePointer:
fallthrough
case t.Kind() == types.UntypedNil:
return "nil"
default:
panic(fmt.Sprint("zeroString for unexpected type:", t))
}
case *types.Pointer, *types.Slice, *types.Interface, *types.Chan, *types.Map, *types.Signature:
return "nil"
case *types.Named, *types.Alias:
return zeroString(t.Underlying(), from)
case *types.Array, *types.Struct:
return relType(t, from) + "{}"
case *types.Tuple:
// Tuples are not normal values.
// We are currently format as "(t[0], ..., t[n])". Could be something else.
components := make([]string, t.Len())
for i := 0; i < t.Len(); i++ {
components[i] = zeroString(t.At(i).Type(), from)
}
return "(" + strings.Join(components, ", ") + ")"
case *types.TypeParam:
return "*new(" + relType(t, from) + ")"
}
panic(fmt.Sprint("zeroString: unexpected ", t))
}

func (c *Const) Name() string {
return c.RelString(nil)
}
Expand Down
165 changes: 102 additions & 63 deletions gopls/internal/golang/addtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import (
"go/ast"
"go/token"
"go/types"
"html/template"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"text/template"
"unicode"

"golang.org/x/tools/go/ast/astutil"
Expand All @@ -34,44 +34,34 @@ import (

const testTmplString = `
func {{.TestFuncName}}(t *{{.TestingPackageName}}.T) {
{{- /* Constructor input parameters struct declaration. */}}
{{- if and .Receiver .Receiver.Constructor}}
{{- if gt (len .Receiver.Constructor.Args) 1}}
type constructorArgs struct {
{{- range .Receiver.Constructor.Args}}
{{.Name}} {{.Type}}
{{- end}}
}
{{- end}}
{{- end}}
{{- /* Functions/methods input parameters struct declaration. */}}
{{- if gt (len .Func.Args) 1}}
type args struct {
{{- range .Func.Args}}
{{.Name}} {{.Type}}
{{- end}}
}
{{- end}}
{{- /* Test cases struct declaration and empty initialization. */}}
tests := []struct {
name string // description of this test case
{{- $commentPrinted := false }}
{{- if and .Receiver .Receiver.Constructor}}
{{- if gt (len .Receiver.Constructor.Args) 1}}
constructorArgs constructorArgs
{{- range .Receiver.Constructor.Args}}
{{- if .Name}}
{{- if not $commentPrinted}}
// Named input parameters for receiver constructor.
{{- $commentPrinted = true }}
{{- end}}
{{.Name}} {{.Type}}
{{- end}}
{{- if eq (len .Receiver.Constructor.Args) 1}}
constructorArg {{(index .Receiver.Constructor.Args 0).Type}}
{{- end}}
{{- end}}
{{- if gt (len .Func.Args) 1}}
args args
{{- $commentPrinted := false }}
{{- range .Func.Args}}
{{- if .Name}}
{{- if not $commentPrinted}}
// Named input parameters for target function.
{{- $commentPrinted = true }}
{{- end}}
{{.Name}} {{.Type}}
{{- end}}
{{- if eq (len .Func.Args) 1}}
arg {{(index .Func.Args 0).Type}}
{{- end}}
{{- range $index, $res := .Func.Results}}
{{- if eq $res.Name "gotErr"}}
wantErr bool
Expand All @@ -96,7 +86,12 @@ func {{.TestFuncName}}(t *{{.TestingPackageName}}.T) {
{{- .Receiver.Constructor.Name}}
{{- /* Constructor input parameters. */ -}}
({{- if eq (len .Receiver.Constructor.Args) 1}}tt.constructorArg{{end}}{{if gt (len .Func.Args) 1}}{{fieldNames .Receiver.Constructor.Args "tt.constructorArgs."}}{{end}})
(
{{- range $index, $arg := .Receiver.Constructor.Args}}
{{- if ne $index 0}}, {{end}}
{{- if .Name}}tt.{{.Name}}{{else}}{{.Value}}{{end}}
{{- end -}}
)
{{- /* Handles the error return from constructor. */}}
{{- $last := last .Receiver.Constructor.Results}}
Expand All @@ -123,7 +118,12 @@ func {{.TestFuncName}}(t *{{.TestingPackageName}}.T) {
{{- end}}{{.Func.Name}}
{{- /* Input parameters. */ -}}
({{- if eq (len .Func.Args) 1}}tt.arg{{end}}{{if gt (len .Func.Args) 1}}{{fieldNames .Func.Args "tt.args."}}{{end}})
(
{{- range $index, $arg := .Func.Args}}
{{- if ne $index 0}}, {{end}}
{{- if .Name}}tt.{{.Name}}{{else}}{{.Value}}{{end}}
{{- end -}}
)
{{- /* Handles the returned error before the rest of return value. */}}
{{- $last := last .Func.Results}}
Expand Down Expand Up @@ -155,8 +155,12 @@ func {{.TestFuncName}}(t *{{.TestingPackageName}}.T) {
}
`

// Name is the name of the field this input parameter should reference.
// Value is the expression this input parameter should accept.
//
// Exactly one of Name or Value must be set.
type field struct {
Name, Type string
Name, Type, Value string
}

type function struct {
Expand Down Expand Up @@ -191,6 +195,9 @@ type testInfo struct {
var testTmpl = template.Must(template.New("test").Funcs(template.FuncMap{
"add": func(a, b int) int { return a + b },
"last": func(slice []field) field {
if len(slice) == 0 {
return field{}
}
return slice[len(slice)-1]
},
"fieldNames": func(fields []field, qualifier string) (res string) {
Expand Down Expand Up @@ -450,36 +457,32 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol.

errorType := types.Universe.Lookup("error").Type()

// TODO(hxjiang): if input parameter is not named (meaning it's not used),
// pass the zero value to the function call.
// TODO(hxjiang): if the input parameter is named, define the field by using
// the parameter's name instead of in%d.
// TODO(hxjiang): handle special case for ctx.Context input.
for index := range sig.Params().Len() {
var name string
if index == 0 {
name = "in"
for i := range sig.Params().Len() {
param := sig.Params().At(i)
name, typ := param.Name(), param.Type()
f := field{Type: types.TypeString(typ, qf)}
if name == "" || name == "_" {
f.Value = typesinternal.ZeroString(typ, qf)
} else {
name = fmt.Sprintf("in%d", index+1)
f.Name = name
}
data.Func.Args = append(data.Func.Args, field{
Name: name,
Type: types.TypeString(sig.Params().At(index).Type(), qf),
})
data.Func.Args = append(data.Func.Args, f)
}

for index := range sig.Results().Len() {
for i := range sig.Results().Len() {
typ := sig.Results().At(i).Type()
var name string
if index == sig.Results().Len()-1 && types.Identical(sig.Results().At(index).Type(), errorType) {
if i == sig.Results().Len()-1 && types.Identical(typ, errorType) {
name = "gotErr"
} else if index == 0 {
} else if i == 0 {
name = "got"
} else {
name = fmt.Sprintf("got%d", index+1)
name = fmt.Sprintf("got%d", i+1)
}
data.Func.Results = append(data.Func.Results, field{
Name: name,
Type: types.TypeString(sig.Results().At(index).Type(), qf),
Type: types.TypeString(typ, qf),
})
}

Expand Down Expand Up @@ -587,25 +590,25 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol.

if constructor != nil {
data.Receiver.Constructor = &function{Name: constructor.Name()}
for index := range constructor.Signature().Params().Len() {
var name string
if index == 0 {
name = "in"
for i := range constructor.Signature().Params().Len() {
param := constructor.Signature().Params().At(i)
name, typ := param.Name(), param.Type()
f := field{Type: types.TypeString(typ, qf)}
if name == "" || name == "_" {
f.Value = typesinternal.ZeroString(typ, qf)
} else {
name = fmt.Sprintf("in%d", index+1)
f.Name = name
}
data.Receiver.Constructor.Args = append(data.Receiver.Constructor.Args, field{
Name: name,
Type: types.TypeString(constructor.Signature().Params().At(index).Type(), qf),
})
data.Receiver.Constructor.Args = append(data.Receiver.Constructor.Args, f)
}
for index := range constructor.Signature().Results().Len() {
for i := range constructor.Signature().Results().Len() {
typ := constructor.Signature().Results().At(i).Type()
var name string
if index == 0 {
if i == 0 {
// The first return value must be of type T, *T, or a type whose named
// type is the same as named type of T.
name = varName
} else if index == constructor.Signature().Results().Len()-1 && types.Identical(constructor.Signature().Results().At(index).Type(), errorType) {
} else if i == constructor.Signature().Results().Len()-1 && types.Identical(typ, errorType) {
name = "err"
} else {
// Drop any return values beyond the first and the last.
Expand All @@ -614,12 +617,48 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol.
}
data.Receiver.Constructor.Results = append(data.Receiver.Constructor.Results, field{
Name: name,
Type: types.TypeString(constructor.Signature().Results().At(index).Type(), qf),
Type: types.TypeString(typ, qf),
})
}
}
}

// Resolves duplicate parameter names between the function and its
// receiver's constructor. It adds prefix to the constructor's parameters
// until no conflicts remain.
if data.Receiver != nil && data.Receiver.Constructor != nil {
seen := map[string]bool{}
for _, f := range data.Func.Args {
if f.Name == "" {
continue
}
seen[f.Name] = true
}

// "" for no change, "c" for constructor, "i" for input.
for _, prefix := range []string{"", "c", "c_", "i", "i_"} {
conflict := false
for _, f := range data.Receiver.Constructor.Args {
if f.Name == "" {
continue
}
if seen[prefix+f.Name] {
conflict = true
break
}
}
if !conflict {
for i, f := range data.Receiver.Constructor.Args {
if f.Name == "" {
continue
}
data.Receiver.Constructor.Args[i].Name = prefix + data.Receiver.Constructor.Args[i].Name
}
break
}
}
}

// Compute edits to update imports.
//
// If we're adding to an existing test file, we need to adjust existing
Expand Down
2 changes: 1 addition & 1 deletion gopls/internal/golang/completion/postfix_snippets.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ func (a *postfixTmplArgs) TypeName(t types.Type) (string, error) {

// Zero return the zero value representation of type t
func (a *postfixTmplArgs) Zero(t types.Type) string {
return formatZeroValue(t, a.qf)
return typesinternal.ZeroString(t, a.qf)
}

func (a *postfixTmplArgs) IsIdent() bool {
Expand Down
5 changes: 3 additions & 2 deletions gopls/internal/golang/completion/statements.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"golang.org/x/tools/gopls/internal/golang"
"golang.org/x/tools/gopls/internal/golang/completion/snippet"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/typesinternal"
)

// addStatementCandidates adds full statement completion candidates
Expand Down Expand Up @@ -294,7 +295,7 @@ func (c *completer) addErrCheck() {
} else {
snip.WriteText("return ")
for i := 0; i < result.Len()-1; i++ {
snip.WriteText(formatZeroValue(result.At(i).Type(), c.qf))
snip.WriteText(typesinternal.ZeroString(result.At(i).Type(), c.qf))
snip.WriteText(", ")
}
snip.WritePlaceholder(func(b *snippet.Builder) {
Expand Down Expand Up @@ -404,7 +405,7 @@ func (c *completer) addReturnZeroValues() {
fmt.Fprintf(&label, ", ")
}

zero := formatZeroValue(result.At(i).Type(), c.qf)
zero := typesinternal.ZeroString(result.At(i).Type(), c.qf)
snip.WritePlaceholder(func(b *snippet.Builder) {
b.WriteText(zero)
})
Expand Down
Loading

0 comments on commit b4332e0

Please sign in to comment.