blob: b7253bb8bf78533c4e62b6e4f47d8e5c076843bb [file] [log] [blame]
// Copyright 2025 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 golang
import (
"go/types"
"maps"
"testing"
"golang.org/x/tools/internal/testfiles"
"golang.org/x/tools/txtar"
)
func TestUnify(t *testing.T) {
// Most cases from TestMatches in gopls/internal/util/fingerprint/fingerprint_test.go.
const src = `
-- go.mod --
module example.com
go 1.24
-- a/a.go --
package a
type Int = int
type String = string
// Eq.Equal matches casefold.Equal.
type Eq[T any] interface { Equal(T, T) bool }
type casefold struct{}
func (casefold) Equal(x, y string) bool
// A matches AString.
type A[T any] = struct { x T }
type AString = struct { x string }
// B matches anything!
type B[T any] = T
func C1[T any](int, T, ...string) T { panic(0) }
func C2[U any](int, int, ...U) bool { panic(0) }
func C3(int, bool, ...string) rune
func C4(int, bool, ...string)
func C5(int, float64, bool, string) bool
func C6(int, bool, ...string) bool
func DAny[T any](Named[T]) { panic(0) }
func DString(Named[string])
func DInt(Named[int])
type Named[T any] struct { x T }
func E1(byte) rune
func E2(uint8) int32
func E3(int8) uint32
// generic vs. generic
func F1[T any](T) { panic(0) }
func F2[T any](*T) { panic(0) }
func F3[T any](T, T) { panic(0) }
func F4[U any](U, *U) {panic(0) }
func F4a[U any](U, Named[U]) {panic(0) }
func F5[T, U any](T, U, U) { panic(0) }
func F6[T any](T, int, T) { panic(0) }
func F7[T any](bool, T, T) { panic(0) }
func F8[V any](*V, int, int) { panic(0) }
func F9[V any](V, *V, V) { panic(0) }
`
type tmap = map[*types.TypeParam]types.Type
var (
boolType = types.Typ[types.Bool]
intType = types.Typ[types.Int]
stringType = types.Typ[types.String]
)
pkg := testfiles.LoadPackages(t, txtar.Parse([]byte(src)), "./a")[0]
scope := pkg.Types.Scope()
tparam := func(name string, index int) *types.TypeParam {
obj := scope.Lookup(name)
var tps *types.TypeParamList
switch obj := obj.(type) {
case *types.Func:
tps = obj.Signature().TypeParams()
case *types.TypeName:
if n, ok := obj.Type().(*types.Named); ok {
tps = n.TypeParams()
} else {
tps = obj.Type().(*types.Alias).TypeParams()
}
default:
t.Fatalf("unsupported object of type %T", obj)
}
return tps.At(index)
}
for _, test := range []struct {
x, y string // the symbols in the above source code whose types to unify
method string // optional field or method
params tmap // initial values of type params
want bool // success or failure
wantParams tmap // expected output
}{
{
// In Eq[T], T is bound to string.
x: "Eq",
y: "casefold",
method: "Equal",
want: true,
wantParams: tmap{tparam("Eq", 0): stringType},
},
{
// If we unify A[T] and A[string], T should be bound to string.
x: "A",
y: "AString",
want: true,
wantParams: tmap{tparam("A", 0): stringType},
},
{x: "A", y: "Eq", want: false}, // completely unrelated
{
x: "B",
y: "String",
want: true,
wantParams: tmap{tparam("B", 0): stringType},
},
{
x: "B",
y: "Int",
want: true,
wantParams: tmap{tparam("B", 0): intType},
},
{
x: "B",
y: "A",
want: true,
// B's T is bound to A's struct { x T }
wantParams: tmap{tparam("B", 0): scope.Lookup("A").Type().Underlying()},
},
{
// C1's U unifies with C6's bool.
x: "C1",
y: "C6",
wantParams: tmap{tparam("C1", 0): boolType},
want: true,
},
// C1 fails to unify with C2 because C1's T must be bound to both int and bool.
{x: "C1", y: "C2", want: false},
// The remaining "C" cases fail for less interesting reasons, usually different numbers
// or types of parameters or results.
{x: "C1", y: "C3", want: false},
{x: "C1", y: "C4", want: false},
{x: "C1", y: "C5", want: false},
{x: "C2", y: "C3", want: false},
{x: "C2", y: "C4", want: false},
{x: "C3", y: "C4", want: false},
{
x: "DAny",
y: "DString",
want: true,
wantParams: tmap{tparam("DAny", 0): stringType},
},
{x: "DString", y: "DInt", want: false}, // different instantiations of Named
{x: "E1", y: "E2", want: true}, // byte and rune are just aliases
{x: "E2", y: "E3", want: false},
// The following tests cover all of the type param cases of unify.
{
// F1[*int] = F2[int], for example
// F1's T is bound to a pointer to F2's T.
x: "F1",
// F2's T is unbound: any instantiation works.
y: "F2",
want: true,
wantParams: tmap{tparam("F1", 0): types.NewPointer(tparam("F2", 0))},
},
{x: "F3", y: "F4", want: false}, // would require U identical to *U, prevented by occur check
{x: "F3", y: "F4a", want: false}, // occur check through Named[T]
{
x: "F5",
y: "F6",
want: true,
wantParams: tmap{
tparam("F5", 0): intType,
tparam("F5", 1): intType,
tparam("F6", 0): intType,
},
},
{x: "F6", y: "F7", want: false}, // both are bound
{
x: "F5",
y: "F6",
params: tmap{tparam("F6", 0): intType}, // consistent with the result
want: true,
wantParams: tmap{
tparam("F5", 0): intType,
tparam("F5", 1): intType,
tparam("F6", 0): intType,
},
},
{
x: "F5",
y: "F6",
params: tmap{tparam("F6", 0): boolType}, // not consistent
want: false,
},
{x: "F6", y: "F7", want: false}, // both are bound
{
// T=*V, U=int, V=int
x: "F5",
y: "F8",
want: true,
wantParams: tmap{
tparam("F5", 0): types.NewPointer(tparam("F8", 0)),
tparam("F5", 1): intType,
},
},
{
// T=*V, U=int, V=int
// Partial initial information is fine, as long as it's consistent.
x: "F5",
y: "F8",
want: true,
params: tmap{tparam("F5", 1): intType},
wantParams: tmap{
tparam("F5", 0): types.NewPointer(tparam("F8", 0)),
tparam("F5", 1): intType,
},
},
{
// T=*V, U=int, V=int
// Partial initial information is fine, as long as it's consistent.
x: "F5",
y: "F8",
want: true,
params: tmap{tparam("F5", 0): types.NewPointer(tparam("F8", 0))},
wantParams: tmap{
tparam("F5", 0): types.NewPointer(tparam("F8", 0)),
tparam("F5", 1): intType,
},
},
{x: "F5", y: "F9", want: false}, // T is unbound, V is bound, and T occurs in V
{
// T bound to Named[T']
x: "F1",
y: "DAny",
want: true,
wantParams: tmap{
tparam("F1", 0): scope.Lookup("DAny").(*types.Func).Signature().Params().At(0).Type()},
},
} {
lookup := func(name string) types.Type {
obj := scope.Lookup(name)
if obj == nil {
t.Fatalf("Lookup %s failed", name)
}
if test.method != "" {
obj, _, _ = types.LookupFieldOrMethod(obj.Type(), true, pkg.Types, test.method)
if obj == nil {
t.Fatalf("Lookup %s.%s failed", name, test.method)
}
}
return obj.Type()
}
check := func(a, b string, want, compareParams bool) {
t.Helper()
ta := lookup(a)
tb := lookup(b)
var gotParams tmap
if test.params == nil {
// Get the unifier even if there are no input params.
gotParams = tmap{}
} else {
gotParams = maps.Clone(test.params)
}
got := unify(ta, tb, gotParams)
if got != want {
t.Errorf("a=%s b=%s method=%s: unify returned %t for these inputs:\n- %s\n- %s",
a, b, test.method, got, ta, tb)
return
}
if !compareParams {
return
}
if !maps.EqualFunc(gotParams, test.wantParams, types.Identical) {
t.Errorf("x=%s y=%s method=%s: params: got %v, want %v",
a, b, test.method, gotParams, test.wantParams)
}
}
check(test.x, test.y, test.want, true)
// unify is symmetric
check(test.y, test.x, test.want, true)
// unify is reflexive
check(test.x, test.x, true, false)
check(test.y, test.y, true, false)
}
}