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 }