// 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" "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 { 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 }