blob: a9f47c1a2e6f5972783052c44d6abe07bb058b27 [file] [log] [blame]
// Copyright 2024 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 methodsets
// This is an internal test of [fingerprint] and [unify].
//
// TODO(adonovan): avoid internal tests.
// Break fingerprint.go off into its own package?
import (
"go/types"
"testing"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/testenv"
"golang.org/x/tools/internal/testfiles"
"golang.org/x/tools/txtar"
)
// Test_fingerprint runs the fingerprint encoder, decoder, and printer
// on the types of all package-level symbols in gopls, and ensures
// that parse+print is lossless.
func Test_fingerprint(t *testing.T) {
if testing.Short() {
t.Skip("skipping slow test")
}
cfg := &packages.Config{Mode: packages.NeedTypes}
pkgs, err := packages.Load(cfg, "std", "golang.org/x/tools/gopls/...")
if err != nil {
t.Fatal(err)
}
// Record the fingerprint of each logical type (equivalence
// class of types.Types) and assert that they are all equal.
// (Non-tricky types only.)
var fingerprints typeutil.Map
type eqclass struct {
class map[types.Type]bool
fp string
}
for _, pkg := range pkgs {
switch pkg.Types.Path() {
case "unsafe", "builtin":
continue
}
scope := pkg.Types.Scope()
for _, name := range scope.Names() {
obj := scope.Lookup(name)
typ := obj.Type()
if basic, ok := typ.(*types.Basic); ok &&
basic.Info()&types.IsUntyped != 0 {
continue // untyped constant
}
fp, tricky := fingerprint(typ) // check Type encoder doesn't panic
// All equivalent (non-tricky) types have the same fingerprint.
if !tricky {
if prevfp, ok := fingerprints.At(typ).(string); !ok {
fingerprints.Set(typ, fp)
} else if fp != prevfp {
t.Errorf("inconsistent fingerprints for type %v:\n- old: %s\n- new: %s",
typ, fp, prevfp)
}
}
tree := parseFingerprint(fp) // check parser doesn't panic
fp2 := sexprString(tree) // check formatter doesn't pannic
// A parse+print round-trip should be lossless.
if fp != fp2 {
t.Errorf("%s: %v: parse+print changed fingerprint:\n"+
"was: %s\ngot: %s\ntype: %v",
pkg.Fset.Position(obj.Pos()), obj, fp, fp2, typ)
}
}
}
}
// Test_unify exercises the matching algorithm for generic types.
func Test_unify(t *testing.T) {
if testenv.Go1Point() < 24 {
testenv.NeedsGoExperiment(t, "aliastypeparams") // testenv.Go1Point() >= 24 implies aliastypeparams=1
}
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 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
`
pkg := testfiles.LoadPackages(t, txtar.Parse([]byte(src)), "./a")[0]
scope := pkg.Types.Scope()
for _, test := range []struct {
a, b string
method string // optional field or method
want bool
}{
{"Eq", "casefold", "Equal", true},
{"A", "AString", "", true},
{"A", "Eq", "", false}, // completely unrelated
{"B", "String", "", true},
{"B", "Int", "", true},
{"B", "A", "", true},
{"C1", "C2", "", true}, // matches despite inconsistent substitution
{"C1", "C3", "", true},
{"C1", "C4", "", false},
{"C1", "C5", "", false},
{"C2", "C3", "", false}, // intransitive (C1≡C2 ^ C1≡C3)
{"C2", "C4", "", false},
{"C3", "C4", "", false},
{"DAny", "DString", "", true},
{"DAny", "DInt", "", true},
{"DString", "DInt", "", false}, // different instantiations of Named
{"E1", "E2", "", true}, // byte and rune are just aliases
{"E2", "E3", "", false},
} {
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()
}
a := lookup(test.a)
b := lookup(test.b)
afp, _ := fingerprint(a)
bfp, _ := fingerprint(b)
atree := parseFingerprint(afp)
btree := parseFingerprint(bfp)
got := unify(atree, btree)
if got != test.want {
t.Errorf("a=%s b=%s method=%s: unify returned %t for these inputs:\n- %s\n- %s",
test.a, test.b, test.method,
got, sexprString(atree), sexprString(btree))
}
}
}