362 lines
12 KiB
Go
362 lines
12 KiB
Go
|
package apidiff
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"go/types"
|
||
|
"reflect"
|
||
|
)
|
||
|
|
||
|
func (d *differ) checkCompatible(otn *types.TypeName, old, new types.Type) {
|
||
|
switch old := old.(type) {
|
||
|
case *types.Interface:
|
||
|
if new, ok := new.(*types.Interface); ok {
|
||
|
d.checkCompatibleInterface(otn, old, new)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
case *types.Struct:
|
||
|
if new, ok := new.(*types.Struct); ok {
|
||
|
d.checkCompatibleStruct(otn, old, new)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
case *types.Chan:
|
||
|
if new, ok := new.(*types.Chan); ok {
|
||
|
d.checkCompatibleChan(otn, old, new)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
case *types.Basic:
|
||
|
if new, ok := new.(*types.Basic); ok {
|
||
|
d.checkCompatibleBasic(otn, old, new)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
case *types.Named:
|
||
|
panic("unreachable")
|
||
|
|
||
|
default:
|
||
|
d.checkCorrespondence(otn, "", old, new)
|
||
|
return
|
||
|
|
||
|
}
|
||
|
// Here if old and new are different kinds of types.
|
||
|
d.typeChanged(otn, "", old, new)
|
||
|
}
|
||
|
|
||
|
func (d *differ) checkCompatibleChan(otn *types.TypeName, old, new *types.Chan) {
|
||
|
d.checkCorrespondence(otn, ", element type", old.Elem(), new.Elem())
|
||
|
if old.Dir() != new.Dir() {
|
||
|
if new.Dir() == types.SendRecv {
|
||
|
d.compatible(otn, "", "removed direction")
|
||
|
} else {
|
||
|
d.incompatible(otn, "", "changed direction")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (d *differ) checkCompatibleBasic(otn *types.TypeName, old, new *types.Basic) {
|
||
|
// Certain changes to numeric types are compatible. Approximately, the info must
|
||
|
// be the same, and the new values must be a superset of the old.
|
||
|
if old.Kind() == new.Kind() {
|
||
|
// old and new are identical
|
||
|
return
|
||
|
}
|
||
|
if compatibleBasics[[2]types.BasicKind{old.Kind(), new.Kind()}] {
|
||
|
d.compatible(otn, "", "changed from %s to %s", old, new)
|
||
|
} else {
|
||
|
d.typeChanged(otn, "", old, new)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// All pairs (old, new) of compatible basic types.
|
||
|
var compatibleBasics = map[[2]types.BasicKind]bool{
|
||
|
{types.Uint8, types.Uint16}: true,
|
||
|
{types.Uint8, types.Uint32}: true,
|
||
|
{types.Uint8, types.Uint}: true,
|
||
|
{types.Uint8, types.Uint64}: true,
|
||
|
{types.Uint16, types.Uint32}: true,
|
||
|
{types.Uint16, types.Uint}: true,
|
||
|
{types.Uint16, types.Uint64}: true,
|
||
|
{types.Uint32, types.Uint}: true,
|
||
|
{types.Uint32, types.Uint64}: true,
|
||
|
{types.Uint, types.Uint64}: true,
|
||
|
{types.Int8, types.Int16}: true,
|
||
|
{types.Int8, types.Int32}: true,
|
||
|
{types.Int8, types.Int}: true,
|
||
|
{types.Int8, types.Int64}: true,
|
||
|
{types.Int16, types.Int32}: true,
|
||
|
{types.Int16, types.Int}: true,
|
||
|
{types.Int16, types.Int64}: true,
|
||
|
{types.Int32, types.Int}: true,
|
||
|
{types.Int32, types.Int64}: true,
|
||
|
{types.Int, types.Int64}: true,
|
||
|
{types.Float32, types.Float64}: true,
|
||
|
{types.Complex64, types.Complex128}: true,
|
||
|
}
|
||
|
|
||
|
// Interface compatibility:
|
||
|
// If the old interface has an unexported method, the new interface is compatible
|
||
|
// if its exported method set is a superset of the old. (Users could not implement,
|
||
|
// only embed.)
|
||
|
//
|
||
|
// If the old interface did not have an unexported method, the new interface is
|
||
|
// compatible if its exported method set is the same as the old, and it has no
|
||
|
// unexported methods. (Adding an unexported method makes the interface
|
||
|
// unimplementable outside the package.)
|
||
|
//
|
||
|
// TODO: must also check that if any methods were added or removed, every exposed
|
||
|
// type in the package that implemented the interface in old still implements it in
|
||
|
// new. Otherwise external assignments could fail.
|
||
|
func (d *differ) checkCompatibleInterface(otn *types.TypeName, old, new *types.Interface) {
|
||
|
// Method sets are checked in checkCompatibleDefined.
|
||
|
|
||
|
// Does the old interface have an unexported method?
|
||
|
if unexportedMethod(old) != nil {
|
||
|
d.checkMethodSet(otn, old, new, additionsCompatible)
|
||
|
} else {
|
||
|
// Perform an equivalence check, but with more information.
|
||
|
d.checkMethodSet(otn, old, new, additionsIncompatible)
|
||
|
if u := unexportedMethod(new); u != nil {
|
||
|
d.incompatible(otn, u.Name(), "added unexported method")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Return an unexported method from the method set of t, or nil if there are none.
|
||
|
func unexportedMethod(t *types.Interface) *types.Func {
|
||
|
for i := 0; i < t.NumMethods(); i++ {
|
||
|
if m := t.Method(i); !m.Exported() {
|
||
|
return m
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// We need to check three things for structs:
|
||
|
// 1. The set of exported fields must be compatible. This ensures that keyed struct
|
||
|
// literals continue to compile. (There is no compatibility guarantee for unkeyed
|
||
|
// struct literals.)
|
||
|
// 2. The set of exported *selectable* fields must be compatible. This includes the exported
|
||
|
// fields of all embedded structs. This ensures that selections continue to compile.
|
||
|
// 3. If the old struct is comparable, so must the new one be. This ensures that equality
|
||
|
// expressions and uses of struct values as map keys continue to compile.
|
||
|
//
|
||
|
// An unexported embedded struct can't appear in a struct literal outside the
|
||
|
// package, so it doesn't have to be present, or have the same name, in the new
|
||
|
// struct.
|
||
|
//
|
||
|
// Field tags are ignored: they have no compile-time implications.
|
||
|
func (d *differ) checkCompatibleStruct(obj types.Object, old, new *types.Struct) {
|
||
|
d.checkCompatibleObjectSets(obj, exportedFields(old), exportedFields(new))
|
||
|
d.checkCompatibleObjectSets(obj, exportedSelectableFields(old), exportedSelectableFields(new))
|
||
|
// Removing comparability from a struct is an incompatible change.
|
||
|
if types.Comparable(old) && !types.Comparable(new) {
|
||
|
d.incompatible(obj, "", "old is comparable, new is not")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// exportedFields collects all the immediate fields of the struct that are exported.
|
||
|
// This is also the set of exported keys for keyed struct literals.
|
||
|
func exportedFields(s *types.Struct) map[string]types.Object {
|
||
|
m := map[string]types.Object{}
|
||
|
for i := 0; i < s.NumFields(); i++ {
|
||
|
f := s.Field(i)
|
||
|
if f.Exported() {
|
||
|
m[f.Name()] = f
|
||
|
}
|
||
|
}
|
||
|
return m
|
||
|
}
|
||
|
|
||
|
// exportedSelectableFields collects all the exported fields of the struct, including
|
||
|
// exported fields of embedded structs.
|
||
|
//
|
||
|
// We traverse the struct breadth-first, because of the rule that a lower-depth field
|
||
|
// shadows one at a higher depth.
|
||
|
func exportedSelectableFields(s *types.Struct) map[string]types.Object {
|
||
|
var (
|
||
|
m = map[string]types.Object{}
|
||
|
next []*types.Struct // embedded structs at the next depth
|
||
|
seen []*types.Struct // to handle recursive embedding
|
||
|
)
|
||
|
for cur := []*types.Struct{s}; len(cur) > 0; cur, next = next, nil {
|
||
|
seen = append(seen, cur...)
|
||
|
// We only want to consider unambiguous fields. Ambiguous fields (where there
|
||
|
// is more than one field of the same name at the same level) are legal, but
|
||
|
// cannot be selected.
|
||
|
for name, f := range unambiguousFields(cur) {
|
||
|
// Record an exported field we haven't seen before. If we have seen it,
|
||
|
// it occurred a lower depth, so it shadows this field.
|
||
|
if f.Exported() && m[name] == nil {
|
||
|
m[name] = f
|
||
|
}
|
||
|
// Remember embedded structs for processing at the next depth,
|
||
|
// but only if we haven't seen the struct at this depth or above.
|
||
|
if !f.Anonymous() {
|
||
|
continue
|
||
|
}
|
||
|
t := f.Type().Underlying()
|
||
|
if p, ok := t.(*types.Pointer); ok {
|
||
|
t = p.Elem().Underlying()
|
||
|
}
|
||
|
if t, ok := t.(*types.Struct); ok && !contains(seen, t) {
|
||
|
next = append(next, t)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return m
|
||
|
}
|
||
|
|
||
|
func contains(ts []*types.Struct, t *types.Struct) bool {
|
||
|
for _, s := range ts {
|
||
|
if types.Identical(s, t) {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// Given a set of structs at the same depth, the unambiguous fields are the ones whose
|
||
|
// names appear exactly once.
|
||
|
func unambiguousFields(structs []*types.Struct) map[string]*types.Var {
|
||
|
fields := map[string]*types.Var{}
|
||
|
seen := map[string]bool{}
|
||
|
for _, s := range structs {
|
||
|
for i := 0; i < s.NumFields(); i++ {
|
||
|
f := s.Field(i)
|
||
|
name := f.Name()
|
||
|
if seen[name] {
|
||
|
delete(fields, name)
|
||
|
} else {
|
||
|
seen[name] = true
|
||
|
fields[name] = f
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return fields
|
||
|
}
|
||
|
|
||
|
// Anything removed or change from the old set is an incompatible change.
|
||
|
// Anything added to the new set is a compatible change.
|
||
|
func (d *differ) checkCompatibleObjectSets(obj types.Object, old, new map[string]types.Object) {
|
||
|
for name, oldo := range old {
|
||
|
newo := new[name]
|
||
|
if newo == nil {
|
||
|
d.incompatible(obj, name, "removed")
|
||
|
} else {
|
||
|
d.checkCorrespondence(obj, name, oldo.Type(), newo.Type())
|
||
|
}
|
||
|
}
|
||
|
for name := range new {
|
||
|
if old[name] == nil {
|
||
|
d.compatible(obj, name, "added")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (d *differ) checkCompatibleDefined(otn *types.TypeName, old *types.Named, new types.Type) {
|
||
|
// We've already checked that old and new correspond.
|
||
|
d.checkCompatible(otn, old.Underlying(), new.Underlying())
|
||
|
// If there are different kinds of types (e.g. struct and interface), don't bother checking
|
||
|
// the method sets.
|
||
|
if reflect.TypeOf(old.Underlying()) != reflect.TypeOf(new.Underlying()) {
|
||
|
return
|
||
|
}
|
||
|
// Interface method sets are checked in checkCompatibleInterface.
|
||
|
if _, ok := old.Underlying().(*types.Interface); ok {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// A new method set is compatible with an old if the new exported methods are a superset of the old.
|
||
|
d.checkMethodSet(otn, old, new, additionsCompatible)
|
||
|
d.checkMethodSet(otn, types.NewPointer(old), types.NewPointer(new), additionsCompatible)
|
||
|
}
|
||
|
|
||
|
const (
|
||
|
additionsCompatible = true
|
||
|
additionsIncompatible = false
|
||
|
)
|
||
|
|
||
|
func (d *differ) checkMethodSet(otn *types.TypeName, oldt, newt types.Type, addcompat bool) {
|
||
|
// TODO: find a way to use checkCompatibleObjectSets for this.
|
||
|
oldMethodSet := exportedMethods(oldt)
|
||
|
newMethodSet := exportedMethods(newt)
|
||
|
msname := otn.Name()
|
||
|
if _, ok := oldt.(*types.Pointer); ok {
|
||
|
msname = "*" + msname
|
||
|
}
|
||
|
for name, oldMethod := range oldMethodSet {
|
||
|
newMethod := newMethodSet[name]
|
||
|
if newMethod == nil {
|
||
|
var part string
|
||
|
// Due to embedding, it's possible that the method's receiver type is not
|
||
|
// the same as the defined type whose method set we're looking at. So for
|
||
|
// a type T with removed method M that is embedded in some other type U,
|
||
|
// we will generate two "removed" messages for T.M, one for its own type
|
||
|
// T and one for the embedded type U. We want both messages to appear,
|
||
|
// but the messageSet dedup logic will allow only one message for a given
|
||
|
// object. So use the part string to distinguish them.
|
||
|
if receiverNamedType(oldMethod).Obj() != otn {
|
||
|
part = fmt.Sprintf(", method set of %s", msname)
|
||
|
}
|
||
|
d.incompatible(oldMethod, part, "removed")
|
||
|
} else {
|
||
|
obj := oldMethod
|
||
|
// If a value method is changed to a pointer method and has a signature
|
||
|
// change, then we can get two messages for the same method definition: one
|
||
|
// for the value method set that says it's removed, and another for the
|
||
|
// pointer method set that says it changed. To keep both messages (since
|
||
|
// messageSet dedups), use newMethod for the second. (Slight hack.)
|
||
|
if !hasPointerReceiver(oldMethod) && hasPointerReceiver(newMethod) {
|
||
|
obj = newMethod
|
||
|
}
|
||
|
d.checkCorrespondence(obj, "", oldMethod.Type(), newMethod.Type())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Check for added methods.
|
||
|
for name, newMethod := range newMethodSet {
|
||
|
if oldMethodSet[name] == nil {
|
||
|
if addcompat {
|
||
|
d.compatible(newMethod, "", "added")
|
||
|
} else {
|
||
|
d.incompatible(newMethod, "", "added")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// exportedMethods collects all the exported methods of type's method set.
|
||
|
func exportedMethods(t types.Type) map[string]types.Object {
|
||
|
m := map[string]types.Object{}
|
||
|
ms := types.NewMethodSet(t)
|
||
|
for i := 0; i < ms.Len(); i++ {
|
||
|
obj := ms.At(i).Obj()
|
||
|
if obj.Exported() {
|
||
|
m[obj.Name()] = obj
|
||
|
}
|
||
|
}
|
||
|
return m
|
||
|
}
|
||
|
|
||
|
func receiverType(method types.Object) types.Type {
|
||
|
return method.Type().(*types.Signature).Recv().Type()
|
||
|
}
|
||
|
|
||
|
func receiverNamedType(method types.Object) *types.Named {
|
||
|
switch t := receiverType(method).(type) {
|
||
|
case *types.Pointer:
|
||
|
return t.Elem().(*types.Named)
|
||
|
case *types.Named:
|
||
|
return t
|
||
|
default:
|
||
|
panic("unreachable")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func hasPointerReceiver(method types.Object) bool {
|
||
|
_, ok := receiverType(method).(*types.Pointer)
|
||
|
return ok
|
||
|
}
|