golang.org/x/exp@v0.0.0-20240506185415-9bf2ced13842/apidiff/compatibility.go (about)

     1  package apidiff
     2  
     3  import (
     4  	"fmt"
     5  	"go/types"
     6  	"reflect"
     7  )
     8  
     9  func (d *differ) checkCompatible(otn *types.TypeName, old, new types.Type) {
    10  	switch old := old.(type) {
    11  	case *types.Interface:
    12  		if new, ok := new.(*types.Interface); ok {
    13  			d.checkCompatibleInterface(otn, old, new)
    14  			return
    15  		}
    16  
    17  	case *types.Struct:
    18  		if new, ok := new.(*types.Struct); ok {
    19  			d.checkCompatibleStruct(objectWithSide{otn, false}, old, new)
    20  			return
    21  		}
    22  
    23  	case *types.Chan:
    24  		if new, ok := new.(*types.Chan); ok {
    25  			d.checkCompatibleChan(otn, old, new)
    26  			return
    27  		}
    28  
    29  	case *types.Basic:
    30  		if new, ok := new.(*types.Basic); ok {
    31  			d.checkCompatibleBasic(otn, old, new)
    32  			return
    33  		}
    34  
    35  	case *types.Named:
    36  		panic("unreachable")
    37  
    38  	default:
    39  		d.checkCorrespondence(objectWithSide{otn, false}, "", old, new)
    40  		return
    41  
    42  	}
    43  	// Here if old and new are different kinds of types.
    44  	d.typeChanged(objectWithSide{otn, false}, "", old, new)
    45  }
    46  
    47  func (d *differ) checkCompatibleChan(otn *types.TypeName, old, new *types.Chan) {
    48  	d.checkCorrespondence(objectWithSide{otn, false}, ", element type", old.Elem(), new.Elem())
    49  	if old.Dir() != new.Dir() {
    50  		if new.Dir() == types.SendRecv {
    51  			d.compatible(objectWithSide{otn, false}, "", "removed direction")
    52  		} else {
    53  			d.incompatible(objectWithSide{otn, false}, "", "changed direction")
    54  		}
    55  	}
    56  }
    57  
    58  func (d *differ) checkCompatibleBasic(otn *types.TypeName, old, new *types.Basic) {
    59  	// Certain changes to numeric types are compatible. Approximately, the info must
    60  	// be the same, and the new values must be a superset of the old.
    61  	if old.Kind() == new.Kind() {
    62  		// old and new are identical
    63  		return
    64  	}
    65  	if compatibleBasics[[2]types.BasicKind{old.Kind(), new.Kind()}] {
    66  		d.compatible(objectWithSide{otn, false}, "", "changed from %s to %s", old, new)
    67  	} else {
    68  		d.typeChanged(objectWithSide{otn, false}, "", old, new)
    69  	}
    70  }
    71  
    72  // All pairs (old, new) of compatible basic types.
    73  var compatibleBasics = map[[2]types.BasicKind]bool{
    74  	{types.Uint8, types.Uint16}:         true,
    75  	{types.Uint8, types.Uint32}:         true,
    76  	{types.Uint8, types.Uint}:           true,
    77  	{types.Uint8, types.Uint64}:         true,
    78  	{types.Uint16, types.Uint32}:        true,
    79  	{types.Uint16, types.Uint}:          true,
    80  	{types.Uint16, types.Uint64}:        true,
    81  	{types.Uint32, types.Uint}:          true,
    82  	{types.Uint32, types.Uint64}:        true,
    83  	{types.Uint, types.Uint64}:          true,
    84  	{types.Int8, types.Int16}:           true,
    85  	{types.Int8, types.Int32}:           true,
    86  	{types.Int8, types.Int}:             true,
    87  	{types.Int8, types.Int64}:           true,
    88  	{types.Int16, types.Int32}:          true,
    89  	{types.Int16, types.Int}:            true,
    90  	{types.Int16, types.Int64}:          true,
    91  	{types.Int32, types.Int}:            true,
    92  	{types.Int32, types.Int64}:          true,
    93  	{types.Int, types.Int64}:            true,
    94  	{types.Float32, types.Float64}:      true,
    95  	{types.Complex64, types.Complex128}: true,
    96  }
    97  
    98  // Interface compatibility:
    99  // If the old interface has an unexported method, the new interface is compatible
   100  // if its exported method set is a superset of the old. (Users could not implement,
   101  // only embed.)
   102  //
   103  // If the old interface did not have an unexported method, the new interface is
   104  // compatible if its exported method set is the same as the old, and it has no
   105  // unexported methods. (Adding an unexported method makes the interface
   106  // unimplementable outside the package.)
   107  //
   108  // TODO: must also check that if any methods were added or removed, every exposed
   109  // type in the package that implemented the interface in old still implements it in
   110  // new. Otherwise external assignments could fail.
   111  func (d *differ) checkCompatibleInterface(otn *types.TypeName, old, new *types.Interface) {
   112  	// Method sets are checked in checkCompatibleDefined.
   113  
   114  	// Does the old interface have an unexported method?
   115  	if unexportedMethod(old) != nil {
   116  		d.checkMethodSet(otn, old, new, additionsCompatible)
   117  	} else {
   118  		// Perform an equivalence check, but with more information.
   119  		d.checkMethodSet(otn, old, new, additionsIncompatible)
   120  		if u := unexportedMethod(new); u != nil {
   121  			d.incompatible(objectWithSide{otn, false}, u.Name(), "added unexported method")
   122  		}
   123  	}
   124  }
   125  
   126  // Return an unexported method from the method set of t, or nil if there are none.
   127  func unexportedMethod(t *types.Interface) *types.Func {
   128  	for i := 0; i < t.NumMethods(); i++ {
   129  		if m := t.Method(i); !m.Exported() {
   130  			return m
   131  		}
   132  	}
   133  	return nil
   134  }
   135  
   136  // We need to check three things for structs:
   137  //
   138  //  1. The set of exported fields must be compatible. This ensures that keyed struct
   139  //     literals continue to compile. (There is no compatibility guarantee for unkeyed
   140  //     struct literals.)
   141  //
   142  //  2. The set of exported *selectable* fields must be compatible. This includes the exported
   143  //     fields of all embedded structs. This ensures that selections continue to compile.
   144  //
   145  //  3. If the old struct is comparable, so must the new one be. This ensures that equality
   146  //     expressions and uses of struct values as map keys continue to compile.
   147  //
   148  // An unexported embedded struct can't appear in a struct literal outside the
   149  // package, so it doesn't have to be present, or have the same name, in the new
   150  // struct.
   151  //
   152  // Field tags are ignored: they have no compile-time implications.
   153  func (d *differ) checkCompatibleStruct(obj objectWithSide, old, new *types.Struct) {
   154  	d.checkCompatibleObjectSets(obj, exportedFields(old), exportedFields(new))
   155  	d.checkCompatibleObjectSets(obj, exportedSelectableFields(old), exportedSelectableFields(new))
   156  	// Removing comparability from a struct is an incompatible change.
   157  	if types.Comparable(old) && !types.Comparable(new) {
   158  		d.incompatible(obj, "", "old is comparable, new is not")
   159  	}
   160  }
   161  
   162  // exportedFields collects all the immediate fields of the struct that are exported.
   163  // This is also the set of exported keys for keyed struct literals.
   164  func exportedFields(s *types.Struct) map[string]types.Object {
   165  	m := map[string]types.Object{}
   166  	for i := 0; i < s.NumFields(); i++ {
   167  		f := s.Field(i)
   168  		if f.Exported() {
   169  			m[f.Name()] = f
   170  		}
   171  	}
   172  	return m
   173  }
   174  
   175  // exportedSelectableFields collects all the exported fields of the struct, including
   176  // exported fields of embedded structs.
   177  //
   178  // We traverse the struct breadth-first, because of the rule that a lower-depth field
   179  // shadows one at a higher depth.
   180  func exportedSelectableFields(s *types.Struct) map[string]types.Object {
   181  	var (
   182  		m    = map[string]types.Object{}
   183  		next []*types.Struct // embedded structs at the next depth
   184  		seen []*types.Struct // to handle recursive embedding
   185  	)
   186  	for cur := []*types.Struct{s}; len(cur) > 0; cur, next = next, nil {
   187  		seen = append(seen, cur...)
   188  		// We only want to consider unambiguous fields. Ambiguous fields (where there
   189  		// is more than one field of the same name at the same level) are legal, but
   190  		// cannot be selected.
   191  		for name, f := range unambiguousFields(cur) {
   192  			// Record an exported field we haven't seen before. If we have seen it,
   193  			// it occurred a lower depth, so it shadows this field.
   194  			if f.Exported() && m[name] == nil {
   195  				m[name] = f
   196  			}
   197  			// Remember embedded structs for processing at the next depth,
   198  			// but only if we haven't seen the struct at this depth or above.
   199  			if !f.Anonymous() {
   200  				continue
   201  			}
   202  			t := f.Type().Underlying()
   203  			if p, ok := t.(*types.Pointer); ok {
   204  				t = p.Elem().Underlying()
   205  			}
   206  			if t, ok := t.(*types.Struct); ok && !contains(seen, t) {
   207  				next = append(next, t)
   208  			}
   209  		}
   210  	}
   211  	return m
   212  }
   213  
   214  func contains(ts []*types.Struct, t *types.Struct) bool {
   215  	for _, s := range ts {
   216  		if types.Identical(s, t) {
   217  			return true
   218  		}
   219  	}
   220  	return false
   221  }
   222  
   223  // Given a set of structs at the same depth, the unambiguous fields are the ones whose
   224  // names appear exactly once.
   225  func unambiguousFields(structs []*types.Struct) map[string]*types.Var {
   226  	fields := map[string]*types.Var{}
   227  	seen := map[string]bool{}
   228  	for _, s := range structs {
   229  		for i := 0; i < s.NumFields(); i++ {
   230  			f := s.Field(i)
   231  			name := f.Name()
   232  			if seen[name] {
   233  				delete(fields, name)
   234  			} else {
   235  				seen[name] = true
   236  				fields[name] = f
   237  			}
   238  		}
   239  	}
   240  	return fields
   241  }
   242  
   243  // Anything removed or change from the old set is an incompatible change.
   244  // Anything added to the new set is a compatible change.
   245  func (d *differ) checkCompatibleObjectSets(obj objectWithSide, old, new map[string]types.Object) {
   246  	for name, oldo := range old {
   247  		newo := new[name]
   248  		if newo == nil {
   249  			d.incompatible(obj, name, "removed")
   250  		} else {
   251  			d.checkCorrespondence(obj, name, oldo.Type(), newo.Type())
   252  		}
   253  	}
   254  	for name := range new {
   255  		if old[name] == nil {
   256  			d.compatible(obj, name, "added")
   257  		}
   258  	}
   259  }
   260  
   261  func (d *differ) checkCompatibleDefined(otn *types.TypeName, old *types.Named, new types.Type) {
   262  	// We've already checked that old and new correspond.
   263  	d.checkCompatible(otn, old.Underlying(), new.Underlying())
   264  	// If there are different kinds of types (e.g. struct and interface), don't bother checking
   265  	// the method sets.
   266  	if reflect.TypeOf(old.Underlying()) != reflect.TypeOf(new.Underlying()) {
   267  		return
   268  	}
   269  	// Interface method sets are checked in checkCompatibleInterface.
   270  	if _, ok := old.Underlying().(*types.Interface); ok {
   271  		return
   272  	}
   273  
   274  	// A new method set is compatible with an old if the new exported methods are a superset of the old.
   275  	d.checkMethodSet(otn, old, new, additionsCompatible)
   276  	d.checkMethodSet(otn, types.NewPointer(old), types.NewPointer(new), additionsCompatible)
   277  }
   278  
   279  const (
   280  	additionsCompatible   = true
   281  	additionsIncompatible = false
   282  )
   283  
   284  func (d *differ) checkMethodSet(otn *types.TypeName, oldt, newt types.Type, addcompat bool) {
   285  	// TODO: find a way to use checkCompatibleObjectSets for this.
   286  	oldMethodSet := exportedMethods(oldt)
   287  	newMethodSet := exportedMethods(newt)
   288  	msname := otn.Name()
   289  	if _, ok := oldt.(*types.Pointer); ok {
   290  		msname = "*" + msname
   291  	}
   292  	for name, oldMethod := range oldMethodSet {
   293  		newMethod := newMethodSet[name]
   294  		if newMethod == nil {
   295  			var part string
   296  			// Due to embedding, it's possible that the method's receiver type is not
   297  			// the same as the defined type whose method set we're looking at. So for
   298  			// a type T with removed method M that is embedded in some other type U,
   299  			// we will generate two "removed" messages for T.M, one for its own type
   300  			// T and one for the embedded type U. We want both messages to appear,
   301  			// but the messageSet dedup logic will allow only one message for a given
   302  			// object. So use the part string to distinguish them.
   303  			if receiverNamedType(oldMethod).Obj() != otn {
   304  				part = fmt.Sprintf(", method set of %s", msname)
   305  			}
   306  			d.incompatible(objectWithSide{oldMethod, false}, part, "removed")
   307  		} else {
   308  			obj := objectWithSide{oldMethod, false}
   309  			// If a value method is changed to a pointer method and has a signature
   310  			// change, then we can get two messages for the same method definition: one
   311  			// for the value method set that says it's removed, and another for the
   312  			// pointer method set that says it changed. To keep both messages (since
   313  			// messageSet dedups), use newMethod for the second. (Slight hack.)
   314  			if !hasPointerReceiver(oldMethod) && hasPointerReceiver(newMethod) {
   315  				obj = objectWithSide{newMethod, true}
   316  			}
   317  			d.checkCorrespondence(obj, "", oldMethod.Type(), newMethod.Type())
   318  		}
   319  	}
   320  
   321  	// Check for added methods.
   322  	for name, newMethod := range newMethodSet {
   323  		if oldMethodSet[name] == nil {
   324  			if addcompat {
   325  				d.compatible(objectWithSide{newMethod, true}, "", "added")
   326  			} else {
   327  				d.incompatible(objectWithSide{newMethod, true}, "", "added")
   328  			}
   329  		}
   330  	}
   331  }
   332  
   333  // exportedMethods collects all the exported methods of type's method set.
   334  func exportedMethods(t types.Type) map[string]types.Object {
   335  	m := map[string]types.Object{}
   336  	ms := types.NewMethodSet(t)
   337  	for i := 0; i < ms.Len(); i++ {
   338  		obj := ms.At(i).Obj()
   339  		if obj.Exported() {
   340  			m[obj.Name()] = obj
   341  		}
   342  	}
   343  	return m
   344  }
   345  
   346  func receiverType(method types.Object) types.Type {
   347  	return method.Type().(*types.Signature).Recv().Type()
   348  }
   349  
   350  func receiverNamedType(method types.Object) *types.Named {
   351  	switch t := receiverType(method).(type) {
   352  	case *types.Pointer:
   353  		return t.Elem().(*types.Named)
   354  	case *types.Named:
   355  		return t
   356  	default:
   357  		panic("unreachable")
   358  	}
   359  }
   360  
   361  func hasPointerReceiver(method types.Object) bool {
   362  	_, ok := receiverType(method).(*types.Pointer)
   363  	return ok
   364  }