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