blob: e9a4a5b9143d3ad1f07fbd7f06c59148866a523c [file] [log] [blame]
// Copyright 2021 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.
//go:build go1.18
package typeparams_test
import (
"bytes"
"go/ast"
"go/build"
"go/importer"
"go/parser"
"go/token"
"go/types"
"strings"
"testing"
)
// TestAPIConsistency verifies that exported APIs match at Go 1.17 and Go
// 1.18+.
//
// It relies on the convention that the names of type aliases in the typeparams
// package match the names of the types they are aliasing.
//
// This test could be made more precise.
func TestAPIConsistency(t *testing.T) {
api118 := getAPI(buildPackage(t, true))
api117 := getAPI(buildPackage(t, false))
for name, api := range api117 {
if api != api118[name] {
t.Errorf("%q: got %s at 1.17, but %s at 1.18+", name, api, api118[name])
}
delete(api118, name)
}
for name, api := range api118 {
// Go 1.23 has iterator methods that return Seq.
// These methods can't be supported at 1.17.
if strings.Contains(api, "Seq") && api117[name] == "" {
continue
}
if api != api117[name] {
t.Errorf("%q: got %s at 1.18+, but %s at 1.17", name, api, api117[name])
}
}
}
func getAPI(pkg *types.Package) map[string]string {
api := make(map[string]string)
for _, name := range pkg.Scope().Names() {
if !token.IsExported(name) {
continue
}
api[name] = name
obj := pkg.Scope().Lookup(name)
if f, ok := obj.(*types.Func); ok {
api[name] = formatSignature(f.Type().(*types.Signature))
}
typ := pkg.Scope().Lookup(name).Type()
// Consider method sets of pointer and non-pointer receivers.
msets := map[string]*types.MethodSet{
name: types.NewMethodSet(typ),
"*" + name: types.NewMethodSet(types.NewPointer(typ)),
}
for name, mset := range msets {
for i := 0; i < mset.Len(); i++ {
f := mset.At(i).Obj().(*types.Func)
mname := f.Name()
if token.IsExported(mname) {
api[name+"."+mname] = formatSignature(f.Type().(*types.Signature))
}
}
}
}
return api
}
func formatSignature(sig *types.Signature) string {
var b bytes.Buffer
b.WriteString("func")
writeTuple(&b, sig.Params())
writeTuple(&b, sig.Results())
return b.String()
}
func writeTuple(buf *bytes.Buffer, t *types.Tuple) {
buf.WriteRune('(')
// The API at Go 1.18 uses aliases for types in go/types. These types are
// _actually_ in the go/types package, and therefore would be formatted as
// e.g. *types.TypeParam, which would not match *typeparams.TypeParam --
// go/types does not track aliases. As we use the same name for all aliases,
// we can make the formatted signatures match by dropping the package
// qualifier.
qf := func(*types.Package) string { return "" }
for i := 0; i < t.Len(); i++ {
if i > 0 {
buf.WriteString(", ")
}
buf.WriteString(types.TypeString(t.At(i).Type(), qf))
}
buf.WriteRune(')')
}
func buildPackage(t *testing.T, go118 bool) *types.Package {
ctxt := build.Default
if !go118 {
for i, tag := range ctxt.ReleaseTags {
if tag == "go1.18" {
ctxt.ReleaseTags = ctxt.ReleaseTags[:i]
break
}
}
}
bpkg, err := ctxt.ImportDir(".", 0)
if err != nil {
t.Fatal(err)
}
return typeCheck(t, bpkg.GoFiles)
}
func typeCheck(t *testing.T, filenames []string) *types.Package {
fset := token.NewFileSet()
var files []*ast.File
for _, name := range filenames {
f, err := parser.ParseFile(fset, name, nil, 0)
if err != nil {
t.Fatal(err)
}
files = append(files, f)
}
conf := types.Config{
Importer: importer.Default(),
}
pkg, err := conf.Check("", fset, files, nil)
if err != nil {
t.Fatal(err)
}
return pkg
}