golang.org/x/exp@v0.0.0-20240506185415-9bf2ced13842/apidiff/apidiff.go (about) 1 // TODO: test swap corresponding types (e.g. u1 <-> u2 and u2 <-> u1) 2 // TODO: test exported alias refers to something in another package -- does correspondence work then? 3 // TODO: CODE COVERAGE 4 // TODO: note that we may miss correspondences because we bail early when we compare a signature (e.g. when lengths differ; we could do up to the shorter) 5 // TODO: if you add an unexported method to an exposed interface, you have to check that 6 // every exposed type that previously implemented the interface still does. Otherwise 7 // an external assignment of the exposed type to the interface type could fail. 8 // TODO: check constant values: large values aren't representable by some types. 9 // TODO: Document all the incompatibilities we don't check for. 10 11 package apidiff 12 13 import ( 14 "fmt" 15 "go/constant" 16 "go/token" 17 "go/types" 18 "strings" 19 20 "golang.org/x/tools/go/types/typeutil" 21 ) 22 23 // Changes reports on the differences between the APIs of the old and new packages. 24 // It classifies each difference as either compatible or incompatible (breaking.) For 25 // a detailed discussion of what constitutes an incompatible change, see the README. 26 func Changes(old, new *types.Package) Report { 27 return changesInternal(old, new, old.Path(), new.Path()) 28 } 29 30 // changesInternal contains the core logic for comparing a single package, shared 31 // between Changes and ModuleChanges. The root package path arguments refer to the 32 // context of this apidiff invocation - when diffing a single package, they will be 33 // that package, but when diffing a whole module, they will be the root path of the 34 // module. This is used to give change messages appropriate context for object names. 35 // The old and new root must be tracked independently, since each side of the diff 36 // operation may be a different path. 37 func changesInternal(old, new *types.Package, oldRootPackagePath, newRootPackagePath string) Report { 38 d := newDiffer(old, new) 39 d.checkPackage(oldRootPackagePath) 40 r := Report{} 41 for _, m := range d.incompatibles.collect(oldRootPackagePath, newRootPackagePath) { 42 r.Changes = append(r.Changes, Change{Message: m, Compatible: false}) 43 } 44 for _, m := range d.compatibles.collect(oldRootPackagePath, newRootPackagePath) { 45 r.Changes = append(r.Changes, Change{Message: m, Compatible: true}) 46 } 47 return r 48 } 49 50 // ModuleChanges reports on the differences between the APIs of the old and new 51 // modules. It classifies each difference as either compatible or incompatible 52 // (breaking). This includes the addition and removal of entire packages. For a 53 // detailed discussion of what constitutes an incompatible change, see the README. 54 func ModuleChanges(old, new *Module) Report { 55 var r Report 56 57 oldPkgs := make(map[string]*types.Package) 58 for _, p := range old.Packages { 59 oldPkgs[old.relativePath(p)] = p 60 } 61 62 newPkgs := make(map[string]*types.Package) 63 for _, p := range new.Packages { 64 newPkgs[new.relativePath(p)] = p 65 } 66 67 for n, op := range oldPkgs { 68 if np, ok := newPkgs[n]; ok { 69 // shared package, compare surfaces 70 rr := changesInternal(op, np, old.Path, new.Path) 71 r.Changes = append(r.Changes, rr.Changes...) 72 } else { 73 // old package was removed 74 r.Changes = append(r.Changes, packageChange(op, "removed", false)) 75 } 76 } 77 78 for n, np := range newPkgs { 79 if _, ok := oldPkgs[n]; !ok { 80 // new package was added 81 r.Changes = append(r.Changes, packageChange(np, "added", true)) 82 } 83 } 84 85 return r 86 } 87 88 func packageChange(p *types.Package, change string, compatible bool) Change { 89 return Change{ 90 Message: fmt.Sprintf("package %s: %s", p.Path(), change), 91 Compatible: compatible, 92 } 93 } 94 95 // Module is a convenience type for representing a Go module with a path and a 96 // slice of Packages contained within. 97 type Module struct { 98 Path string 99 Packages []*types.Package 100 } 101 102 // relativePath computes the module-relative package path of the given Package. 103 func (m *Module) relativePath(p *types.Package) string { 104 return strings.TrimPrefix(p.Path(), m.Path) 105 } 106 107 type differ struct { 108 old, new *types.Package 109 // Correspondences between named types. 110 // Even though it is the named types (*types.Named) that correspond, we use 111 // *types.TypeName as a map key because they are canonical. 112 // The values can be either named types or basic types. 113 correspondMap typeutil.Map 114 115 // Messages. 116 incompatibles messageSet 117 compatibles messageSet 118 } 119 120 func newDiffer(old, new *types.Package) *differ { 121 return &differ{ 122 old: old, 123 new: new, 124 incompatibles: messageSet{}, 125 compatibles: messageSet{}, 126 } 127 } 128 129 func (d *differ) incompatible(obj objectWithSide, part, format string, args ...interface{}) { 130 addMessage(d.incompatibles, obj, part, format, args) 131 } 132 133 func (d *differ) compatible(obj objectWithSide, part, format string, args ...interface{}) { 134 addMessage(d.compatibles, obj, part, format, args) 135 } 136 137 func addMessage(ms messageSet, obj objectWithSide, part, format string, args []interface{}) { 138 ms.add(obj, part, fmt.Sprintf(format, args...)) 139 } 140 141 func (d *differ) checkPackage(oldRootPackagePath string) { 142 // Determine what has changed between old and new. 143 144 // First, establish correspondences between types with the same name, before 145 // looking at aliases. This will avoid confusing messages like "T: changed 146 // from T to T", which can happen if a correspondence between an alias 147 // and a named type is established first. 148 // See testdata/order.go. 149 for _, name := range d.old.Scope().Names() { 150 oldobj := d.old.Scope().Lookup(name) 151 if tn, ok := oldobj.(*types.TypeName); ok { 152 if oldn, ok := tn.Type().(*types.Named); ok { 153 if !oldn.Obj().Exported() { 154 continue 155 } 156 // Does new have a named type of the same name? Look up using 157 // the old named type's name, oldn.Obj().Name(), not the 158 // TypeName tn, which may be an alias. 159 newobj := d.new.Scope().Lookup(oldn.Obj().Name()) 160 if newobj != nil { 161 d.checkObjects(oldobj, newobj) 162 } 163 } 164 } 165 } 166 167 // Next, look at all exported symbols in the old world and compare them 168 // with the same-named symbols in the new world. 169 for _, name := range d.old.Scope().Names() { 170 oldobj := d.old.Scope().Lookup(name) 171 if !oldobj.Exported() { 172 continue 173 } 174 newobj := d.new.Scope().Lookup(name) 175 if newobj == nil { 176 d.incompatible(objectWithSide{oldobj, false}, "", "removed") 177 continue 178 } 179 d.checkObjects(oldobj, newobj) 180 } 181 182 // Now look at what has been added in the new package. 183 for _, name := range d.new.Scope().Names() { 184 newobj := d.new.Scope().Lookup(name) 185 if newobj.Exported() && d.old.Scope().Lookup(name) == nil { 186 d.compatible(objectWithSide{newobj, true}, "", "added") 187 } 188 } 189 190 // Whole-package satisfaction. 191 // For every old exposed interface oIface and its corresponding new interface nIface... 192 d.correspondMap.Iterate(func(k1 types.Type, v1 any) { 193 ot1 := k1.(*types.Named) 194 otn1 := ot1.Obj() 195 nt1 := v1.(types.Type) 196 oIface, ok := otn1.Type().Underlying().(*types.Interface) 197 if !ok { 198 return 199 } 200 nIface, ok := nt1.Underlying().(*types.Interface) 201 if !ok { 202 // If nt1 isn't an interface but otn1 is, then that's an incompatibility that 203 // we've already noticed, so there's no need to do anything here. 204 return 205 } 206 // For every old type that implements oIface, its corresponding new type must implement 207 // nIface. 208 d.correspondMap.Iterate(func(k2 types.Type, v2 any) { 209 ot2 := k2.(*types.Named) 210 otn2 := ot2.Obj() 211 nt2 := v2.(types.Type) 212 if otn1 == otn2 { 213 return 214 } 215 if types.Implements(otn2.Type(), oIface) && !types.Implements(nt2, nIface) { 216 // TODO(jba): the type name is not sufficient information here; we need the type args 217 // if this is an instantiated generic type. 218 d.incompatible(objectWithSide{otn2, false}, "", "no longer implements %s", objectString(otn1, oldRootPackagePath)) 219 } 220 }) 221 }) 222 } 223 224 func (d *differ) checkObjects(old, new types.Object) { 225 switch old := old.(type) { 226 case *types.Const: 227 if new, ok := new.(*types.Const); ok { 228 d.constChanges(old, new) 229 return 230 } 231 case *types.Var: 232 if new, ok := new.(*types.Var); ok { 233 d.checkCorrespondence(objectWithSide{old, false}, "", old.Type(), new.Type()) 234 return 235 } 236 case *types.Func: 237 switch new := new.(type) { 238 case *types.Func: 239 d.checkCorrespondence(objectWithSide{old, false}, "", old.Type(), new.Type()) 240 return 241 case *types.Var: 242 d.compatible(objectWithSide{old, false}, "", "changed from func to var") 243 d.checkCorrespondence(objectWithSide{old, false}, "", old.Type(), new.Type()) 244 return 245 246 } 247 case *types.TypeName: 248 if new, ok := new.(*types.TypeName); ok { 249 d.checkCorrespondence(objectWithSide{old, false}, "", old.Type(), new.Type()) 250 return 251 } 252 default: 253 panic("unexpected obj type") 254 } 255 // Here if kind of type changed. 256 d.incompatible(objectWithSide{old, false}, "", "changed from %s to %s", 257 objectKindString(old), objectKindString(new)) 258 } 259 260 // Compare two constants. 261 func (d *differ) constChanges(old, new *types.Const) { 262 ot := old.Type() 263 nt := new.Type() 264 // Check for change of type. 265 if !d.correspond(ot, nt) { 266 d.typeChanged(objectWithSide{old, false}, "", ot, nt) 267 return 268 } 269 // Check for change of value. 270 // We know the types are the same, so constant.Compare shouldn't panic. 271 if !constant.Compare(old.Val(), token.EQL, new.Val()) { 272 d.incompatible(objectWithSide{old, false}, "", "value changed from %s to %s", old.Val(), new.Val()) 273 } 274 } 275 276 func objectKindString(obj types.Object) string { 277 switch obj.(type) { 278 case *types.Const: 279 return "const" 280 case *types.Var: 281 return "var" 282 case *types.Func: 283 return "func" 284 case *types.TypeName: 285 return "type" 286 default: 287 return "???" 288 } 289 } 290 291 func (d *differ) checkCorrespondence(obj objectWithSide, part string, old, new types.Type) { 292 if !d.correspond(old, new) { 293 d.typeChanged(obj, part, old, new) 294 } 295 } 296 297 func (d *differ) typeChanged(obj objectWithSide, part string, old, new types.Type) { 298 old = removeNamesFromSignature(old) 299 new = removeNamesFromSignature(new) 300 olds := types.TypeString(old, types.RelativeTo(d.old)) 301 news := types.TypeString(new, types.RelativeTo(d.new)) 302 d.incompatible(obj, part, "changed from %s to %s", olds, news) 303 } 304 305 // go/types always includes the argument and result names when formatting a signature. 306 // Since these can change without affecting compatibility, we don't want users to 307 // be distracted by them, so we remove them. 308 func removeNamesFromSignature(t types.Type) types.Type { 309 sig, ok := t.(*types.Signature) 310 if !ok { 311 return t 312 } 313 314 dename := func(p *types.Tuple) *types.Tuple { 315 var vars []*types.Var 316 for i := 0; i < p.Len(); i++ { 317 v := p.At(i) 318 vars = append(vars, types.NewVar(v.Pos(), v.Pkg(), "", v.Type())) 319 } 320 return types.NewTuple(vars...) 321 } 322 323 return types.NewSignature(sig.Recv(), dename(sig.Params()), dename(sig.Results()), sig.Variadic()) 324 }