internal/breakings: type strings for interfaces Interface type identity is defined in terms of type sets, which are infeasible to compute with syntax. We provide an approximation. For typical interfaces with just a list of methods, sorting them will do. Change-Id: I522bcca7f3265ebc9f3e56e0898a8f0828d3dab4 Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/820720 LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> TryBot-Bypass: Jonathan Amsterdam <jba@google.com> Reviewed-by: Ethan Lee <ethanalee@google.com>
diff --git a/internal/breakings/api.go b/internal/breakings/api.go index 25be067..bdbce71 100644 --- a/internal/breakings/api.go +++ b/internal/breakings/api.go
@@ -13,6 +13,7 @@ "go/printer" "go/token" "reflect" + "slices" "strings" "golang.org/x/tools/go/ast/astutil" @@ -29,11 +30,52 @@ return "func" + sigString(t) case *ast.StructType: return structString(t) + case *ast.InterfaceType: + return interfaceString(t) default: return nodeString(t) } } +// interfaceString returns a string representation of an interface type +// with sorted methods and canonical formatting (e.g., "interface{Close() error; Read([]byte) (int, error)}"). +// Embedded interfaces should be expanded to their methods, but that would require +// access to other packages; their names are included instead. +func interfaceString(it *ast.InterfaceType) string { + methods := interfaceMethods(it) + if len(methods) == 0 { + return "interface{}" + } + slices.Sort(methods) + methods = slices.Compact(methods) + return "interface{" + strings.Join(methods, "; ") + "}" +} + +func interfaceMethods(it *ast.InterfaceType) []string { + if it == nil || it.Methods == nil { + return nil + } + var methods []string + for _, m := range it.Methods.List { + if len(m.Names) > 0 { + if ft, ok := m.Type.(*ast.FuncType); ok { + for _, name := range m.Names { + methods = append(methods, name.Name+sigString(ft)) + } + } + continue + } + + // Handle embedded types. + if embedded, ok := m.Type.(*ast.InterfaceType); ok { + methods = append(methods, interfaceMethods(embedded)...) + } else { + methods = append(methods, typeString(m.Type)) + } + } + return methods +} + // structString returns a string representation of a struct type // with expanded field lists and canonical formatting (e.g., "struct{X T; Y T}"). func structString(st *ast.StructType) string {
diff --git a/internal/breakings/api_test.go b/internal/breakings/api_test.go index 385c0d5..a9b3a0e 100644 --- a/internal/breakings/api_test.go +++ b/internal/breakings/api_test.go
@@ -33,6 +33,13 @@ {"struct{ X, Y int; Z string }", "struct{X int; Y int; Z string}"}, {"struct{ X int `json:\"x\"`; Y string }", "struct{X int `json:\"x\"`; Y string}"}, {"struct{ T; U }", "struct{T; U}"}, + {"interface{}", "interface{}"}, + {"interface{ Close() error; Read([]byte) (int, error) }", "interface{Close() error; Read([]byte) (int, error)}"}, + {"interface{ Read([]byte) (int, error); Close() error }", "interface{Close() error; Read([]byte) (int, error)}"}, + {"interface{ io.Reader; Close() error }", "interface{Close() error; io.Reader}"}, + {"interface{ B(x, y int) bool; A() }", "interface{A(); B(int, int) bool}"}, + {"interface{ Close() error; interface{ Read([]byte) (int, error) } }", "interface{Close() error; Read([]byte) (int, error)}"}, + {"interface{ Close() error; interface{ Close() error } }", "interface{Close() error}"}, } for _, tc := range testCases {