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 }