1
0
mirror of https://github.com/golang/go synced 2024-11-05 16:46:10 -07:00

internal/apidiff: diffs of package APIs

This is copied unchanged from x/exp.

Change-Id: I944b912212f7fd844a4bea81605433baf4bcc9a2
Reviewed-on: https://go-review.googlesource.com/c/tools/+/170862
Reviewed-by: Jay Conrod <jayconrod@google.com>
This commit is contained in:
Jonathan Amsterdam 2019-04-08 19:46:42 -04:00
parent 04e50493df
commit 9e5445377b
9 changed files with 2681 additions and 0 deletions

624
internal/apidiff/README.md Normal file
View File

@ -0,0 +1,624 @@
# Checking Go Package API Compatibility
The `apidiff` tool in this directory determines whether two versions of the same
package are compatible. The goal is to help the developer make an informed
choice of semantic version after they have changed the code of their module.
`apidiff` reports two kinds of changes: incompatible ones, which require
incrementing the major part of the semantic version, and compatible ones, which
require a minor version increment. If no API changes are reported but there are
code changes that could affect client code, then the patch version should
be incremented.
Because `apidiff` ignores package import paths, it may be used to display API
differences between any two packages, not just different versions of the same
package.
The current version of `apidiff` compares only packages, not modules.
## Compatibility Desiderata
Any tool that checks compatibility can offer only an approximation. No tool can
detect behavioral changes; and even if it could, whether a behavioral change is
a breaking change or not depends on many factors, such as whether it closes a
security hole or fixes a bug. Even a change that causes some code to fail to
compile may not be considered a breaking change by the developers or their
users. It may only affect code marked as experimental or unstable, for
example, or the break may only manifest in unlikely cases.
For a tool to be useful, its notion of compatibility must be relaxed enough to
allow reasonable changes, like adding a field to a struct, but strict enough to
catch significant breaking changes. A tool that is too lax will miss important
incompatibilities, and users will stop trusting it; one that is too strict may
generate so much noise that users will ignore it.
To a first approximation, this tool reports a change as incompatible if it could
cause client code to stop compiling. But `apidiff` ignores five ways in which
code may fail to compile after a change. Three of them are mentioned in the
[Go 1 Compatibility Guarantee](https://golang.org/doc/go1compat).
### Unkeyed Struct Literals
Code that uses an unkeyed struct literal would fail to compile if a field was
added to the struct, making any such addition an incompatible change. An example:
```
// old
type Point struct { X, Y int }
// new
type Point struct { X, Y, Z int }
// client
p := pkg.Point{1, 2} // fails in new because there are more fields than expressions
```
Here and below, we provide three snippets: the code in the old version of the
package, the code in the new version, and the code written in a client of the package,
which refers to it by the name `pkg`. The client code compiles against the old
code but not the new.
### Embedding and Shadowing
Adding an exported field to a struct can break code that embeds that struct,
because the newly added field may conflict with an identically named field
at the same struct depth. A selector referring to the latter would become
ambiguous and thus erroneous.
```
// old
type Point struct { X, Y int }
// new
type Point struct { X, Y, Z int }
// client
type z struct { Z int }
var v struct {
pkg.Point
z
}
_ = v.Z // fails in new
```
In the new version, the last line fails to compile because there are two embedded `Z`
fields at the same depth, one from `z` and one from `pkg.Point`.
### Using an Identical Type Externally
If it is possible for client code to write a type expression representing the
underlying type of a defined type in a package, then external code can use it in
assignments involving the package type, making any change to that type incompatible.
```
// old
type Point struct { X, Y int }
// new
type Point struct { X, Y, Z int }
// client
var p struct { X, Y int } = pkg.Point{} // fails in new because of Point's extra field
```
Here, the external code could have used the provided name `Point`, but chose not
to. I'll have more to say about this and related examples later.
### unsafe.Sizeof and Friends
Since `unsafe.Sizeof`, `unsafe.Offsetof` and `unsafe.Alignof` are constant
expressions, they can be used in an array type literal:
```
// old
type S struct{ X int }
// new
type S struct{ X, y int }
// client
var a [unsafe.Sizeof(pkg.S{})]int = [8]int{} // fails in new because S's size is not 8
```
Use of these operations could make many changes to a type potentially incompatible.
### Type Switches
A package change that merges two different types (with same underlying type)
into a single new type may break type switches in clients that refer to both
original types:
```
// old
type T1 int
type T2 int
// new
type T1 int
type T2 = T1
// client
switch x.(type) {
case T1:
case T2:
} // fails with new because two cases have the same type
```
This sort of incompatibility is sufficiently esoteric to ignore; the tool allows
merging types.
## First Attempt at a Definition
Our first attempt at defining compatibility captures the idea that all the
exported names in the old package must have compatible equivalents in the new
package.
A new package is compatible with an old one if and only if:
- For every exported package-level name in the old package, the same name is
declared in the new at package level, and
- the names denote the same kind of object (e.g. both are variables), and
- the types of the objects are compatible.
We will work out the details (and make some corrections) below, but it is clear
already that we will need to determine what makes two types compatible. And
whatever the definition of type compatibility, it's certainly true that if two
types are the same, they are compatible. So we will need to decide what makes an
old and new type the same. We will call this sameness relation _correspondence_.
## Type Correspondence
Go already has a definition of when two types are the same:
[type identity](https://golang.org/ref/spec#Type_identity).
But identity isn't adequate for our purpose: it says that two defined
types are identical if they arise from the same definition, but it's unclear
what "same" means when talking about two different packages (or two versions of
a single package).
The obvious change to the definition of identity is to require that old and new
[defined types](https://golang.org/ref/spec#Type_definitions)
have the same name instead. But that doesn't work either, for two
reasons. First, type aliases can equate two defined types with different names:
```
// old
type E int
// new
type t int
type E = t
```
Second, an unexported type can be renamed:
```
// old
type u1 int
var V u1
// new
type u2 int
var V u2
```
Here, even though `u1` and `u2` are unexported, their exported fields and
methods are visible to clients, so they are part of the API. But since the name
`u1` is not visible to clients, it can be changed compatibly. We say that `u1`
and `u2` are _exposed_: a type is exposed if a client package can declare variables of that type.
We will say that an old defined type _corresponds_ to a new one if they have the
same name, or one can be renamed to the other without otherwise changing the
API. In the first example above, old `E` and new `t` correspond. In the second,
old `u1` and new `u2` correspond.
Two or more old defined types can correspond to a single new type: we consider
"merging" two types into one to be a compatible change. As mentioned above,
code that uses both names in a type switch will fail, but we deliberately ignore
this case. However, a single old type can correspond to only one new type.
So far, we've explained what correspondence means for defined types. To extend
the definition to all types, we parallel the language's definition of type
identity. So, for instance, an old and a new slice type correspond if their
element types correspond.
## Definition of Compatibility
We can now present the definition of compatibility used by `apidiff`.
### Package Compatibility
> A new package is compatible with an old one if:
>1. Each exported name in the old package's scope also appears in the new
>package's scope, and the object (constant, variable, function or type) denoted
>by that name in the old package is compatible with the object denoted by the
>name in the new package, and
>2. For every exposed type that implements an exposed interface in the old package,
> its corresponding type should implement the corresponding interface in the new package.
>
>Otherwise the packages are incompatible.
As an aside, the tool also finds exported names in the new package that are not
exported in the old, and marks them as compatible changes.
Clause 2 is discussed further in "Whole-Package Compatibility."
### Object Compatibility
This section provides compatibility rules for constants, variables, functions
and types.
#### Constants
>A new exported constant is compatible with an old one of the same name if and only if
>1. Their types correspond, and
>2. Their values are identical.
It is tempting to allow changing a typed constant to an untyped one. That may
seem harmless, but it can break code like this:
```
// old
const C int64 = 1
// new
const C = 1
// client
var x = C // old type is int64, new is int
var y int64 = x // fails with new: different types in assignment
```
A change to the value of a constant can break compatiblity if the value is used
in an array type:
```
// old
const C = 1
// new
const C = 2
// client
var a [C]int = [1]int{} // fails with new because [2]int and [1]int are different types
```
Changes to constant values are rare, and determining whether they are compatible
or not is better left to the user, so the tool reports them.
#### Variables
>A new exported variable is compatible with an old one of the same name if and
>only if their types correspond.
Correspondence doesn't look past names, so this rule does not prevent adding a
field to `MyStruct` if the package declares `var V MyStruct`. It does, however, mean that
```
var V struct { X int }
```
is incompatible with
```
var V struct { X, Y int }
```
I discuss this at length below in the section "Compatibility, Types and Names."
#### Functions
>A new exported function or variable is compatible with an old function of the
>same name if and only if their types (signatures) correspond.
This rule captures the fact that, although many signature changes are compatible
for all call sites, none are compatible for assignment:
```
var v func(int) = pkg.F
```
Here, `F` must be of type `func(int)` and not, for instance, `func(...int)` or `func(interface{})`.
Note that the rule permits changing a function to a variable. This is a common
practice, usually done for test stubbing, and cannot break any code at compile
time.
#### Exported Types
> A new exported type is compatible with an old one if and only if their
> names are the same and their types correspond.
This rule seems far too strict. But, ignoring aliases for the moment, it demands only
that the old and new _defined_ types correspond. Consider:
```
// old
type T struct { X int }
// new
type T struct { X, Y int }
```
The addition of `Y` is a compatible change, because this rule does not require
that the struct literals have to correspond, only that the defined types
denoted by `T` must correspond. (Remember that correspondence stops at type
names.)
If one type is an alias that refers to the corresponding defined type, the
situation is the same:
```
// old
type T struct { X int }
// new
type u struct { X, Y int }
type T = u
```
Here, the only requirement is that old `T` corresponds to new `u`, not that the
struct types correspond. (We can't tell from this snippet that the old `T` and
the new `u` do correspond; that depends on whether `u` replaces `T` throughout
the API.)
However, the following change is incompatible, because the names do not
denote corresponding types:
```
// old
type T = struct { X int }
// new
type T = struct { X, Y int }
```
### Type Literal Compatibility
Only five kinds of types can differ compatibly: defined types, structs,
interfaces, channels and numeric types. We only consider the compatibility of
the last four when they are the underlying type of a defined type. See
"Compatibility, Types and Names" for a rationale.
We justify the compatibility rules by enumerating all the ways a type
can be used, and by showing that the allowed changes cannot break any code that
uses values of the type in those ways.
Values of all types can be used in assignments (including argument passing and
function return), but we do not require that old and new types are assignment
compatible. That is because we assume that the old and new packages are never
used together: any given binary will link in either the old package or the new.
So in describing how a type can be used in the sections below, we omit
assignment.
Any type can also be used in a type assertion or conversion. The changes we allow
below may affect the run-time behavior of these operations, but they cannot affect
whether they compile. The only such breaking change would be to change
the type `T` in an assertion `x.T` so that it no longer implements the interface
type of `x`; but the rules for interfaces below disallow that.
> A new type is compatible with an old one if and only if they correspond, or
> one of the cases below applies.
#### Defined Types
Other than assignment, the only ways to use a defined type are to access its
methods, or to make use of the properties of its underlying type. Rule 2 below
covers the latter, and rules 3 and 4 cover the former.
> A new defined type is compatible with an old one if and only if all of the
> following hold:
>1. They correspond.
>2. Their underlying types are compatible.
>3. The new exported value method set is a superset of the old.
>4. The new exported pointer method set is a superset of the old.
An exported method set is a method set with all unexported methods removed.
When comparing methods of a method set, we require identical names and
corresponding signatures.
Removing an exported method is clearly a breaking change. But removing an
unexported one (or changing its signature) can be breaking as well, if it
results in the type no longer implementing an interface. See "Whole-Package
Compatibility," below.
#### Channels
> A new channel type is compatible with an old one if
> 1. The element types correspond, and
> 2. Either the directions are the same, or the new type has no direction.
Other than assignment, the only ways to use values of a channel type are to send
and receive on them, to close them, and to use them as map keys. Changes to a
channel type cannot cause code that closes a channel or uses it as a map key to
fail to compile, so we need not consider those operations.
Rule 1 ensures that any operations on the values sent or received will compile.
Rule 2 captures the fact that any program that compiles with a directed channel
must use either only sends, or only receives, so allowing the other operation
by removing the channel direction cannot break any code.
#### Interfaces
> A new interface is compatible with an old one if and only if:
> 1. The old interface does not have an unexported method, and it corresponds
> to the new interfaces (i.e. they have the same method set), or
> 2. The old interface has an unexported method and the new exported method set is a
> superset of the old.
Other than assignment, the only ways to use an interface are to implement it,
embed it, or call one of its methods. (Interface values can also be used as map
keys, but that cannot cause a compile-time error.)
Certainly, removing an exported method from an interface could break a client
call, so neither rule allows it.
Rule 1 also disallows adding a method to an interface without an existing unexported
method. Such an interface can be implemented in client code. If adding a method
were allowed, a type that implements the old interface could fail to implement
the new one:
```
type I interface { M1() } // old
type I interface { M1(); M2() } // new
// client
type t struct{}
func (t) M1() {}
var i pkg.I = t{} // fails with new, because t lacks M2
```
Rule 2 is based on the observation that if an interface has an unexported
method, the only way a client can implement it is to embed it.
Adding a method is compatible in this case, because the embedding struct will
continue to implement the interface. Adding a method also cannot break any call
sites, since no program that compiles could have any such call sites.
#### Structs
> A new struct is compatible with an old one if all of the following hold:
> 1. The new set of top-level exported fields is a superset of the old.
> 2. The new set of _selectable_ exported fields is a superset of the old.
> 3. If the old struct is comparable, so is the new one.
The set of selectable exported fields is the set of exported fields `F`
such that `x.F` is a valid selector expression for a value `x` of the struct
type. `F` may be at the top level of the struct, or it may be a field of an
embedded struct.
Two fields are the same if they have the same name and corresponding types.
Other than assignment, there are only four ways to use a struct: write a struct
literal, select a field, use a value of the struct as a map key, or compare two
values for equality. The first clause ensures that struct literals compile; the
second, that selections compile; and the third, that equality expressions and
map index expressions compile.
#### Numeric Types
> A new numeric type is compatible with an old one if and only if they are
> both unsigned integers, both signed integers, both floats or both complex
> types, and the new one is at least as large as the old on both 32-bit and
> 64-bit architectures.
Other than in assignments, numeric types appear in arithmetic and comparison
expressions. Since all arithmetic operations but shifts (see below) require that
operand types be identical, and by assumption the old and new types underly
defined types (see "Compatibility, Types and Names," below), there is no way for
client code to write an arithmetic expression that compiles with operands of the
old type but not the new.
Numeric types can also appear in type switches and type assertions. Again, since
the old and new types underly defined types, type switches and type assertions
that compiled using the old defined type will continue to compile with the new
defined type.
Going from an unsigned to a signed integer type is an incompatible change for
the sole reason that only an unsigned type can appear as the right operand of a
shift. If this rule is relaxed, then changes from an unsigned type to a larger
signed type would be compatible. See [this
issue](https://github.com/golang/go/issues/19113).
Only integer types can be used in bitwise and shift operations, and for indexing
slices and arrays. That is why switching from an integer to a floating-point
type--even one that can represent all values of the integer type--is an
incompatible change.
Conversions from floating-point to complex types or vice versa are not permitted
(the predeclared functions real, imag, and complex must be used instead). To
prevent valid floating-point or complex conversions from becoming invalid,
changing a floating-point type to a complex type or vice versa is considered an
incompatible change.
Although conversions between any two integer types are valid, assigning a
constant value to a variable of integer type that is too small to represent the
constant is not permitted. That is why the only compatible changes are to
a new type whose values are a superset of the old. The requirement that the new
set of values must include the old on both 32-bit and 64-bit machines allows
conversions from `int32` to `int` and from `int` to `int64`, but not the other
direction; and similarly for `uint`.
Changing a type to or from `uintptr` is considered an incompatible change. Since
its size is not specified, there is no way to know whether the new type's values
are a superset of the old type's.
## Whole-Package Compatibility
Some changes that are compatible for a single type are not compatible when the
package is considered as a whole. For example, if you remove an unexported
method on a defined type, it may no longer implement an interface of the
package. This can break client code:
```
// old
type T int
func (T) m() {}
type I interface { m() }
// new
type T int // no method m anymore
// client
var i pkg.I = pkg.T{} // fails with new because T lacks m
```
Similarly, adding a method to an interface can cause defined types
in the package to stop implementing it.
The second clause in the definition for package compatibility handles these
cases. To repeat:
> 2. For every exposed type that implements an exposed interface in the old package,
> its corresponding type should implement the corresponding interface in the new package.
Recall that a type is exposed if it is part of the package's API, even if it is
unexported.
Other incompatibilities that involve more than one type in the package can arise
whenever two types with identical underlying types exist in the old or new
package. Here, a change "splits" an identical underlying type into two, breaking
conversions:
```
// old
type B struct { X int }
type C struct { X int }
// new
type B struct { X int }
type C struct { X, Y int }
// client
var b B
_ = C(b) // fails with new: cannot convert B to C
```
Finally, changes that are compatible for the package in which they occur can
break downstream packages. That can happen even if they involve unexported
methods, thanks to embedding.
The definitions given here don't account for these sorts of problems.
## Compatibility, Types and Names
The above definitions state that the only types that can differ compatibly are
defined types and the types that underly them. Changes to other type literals
are considered incompatible. For instance, it is considered an incompatible
change to add a field to the struct in this variable declaration:
```
var V struct { X int }
```
or this alias definition:
```
type T = struct { X int }
```
We make this choice to keep the definition of compatibility (relatively) simple.
A more precise definition could, for instance, distinguish between
```
func F(struct { X int })
```
where any changes to the struct are incompatible, and
```
func F(struct { X, u int })
```
where adding a field is compatible (since clients cannot write the signature,
and thus cannot assign `F` to a variable of the signature type). The definition
should then also allow other function signature changes that only require
call-site compatibility, like
```
func F(struct { X, u int }, ...int)
```
The result would be a much more complex definition with little benefit, since
the examples in this section rarely arise in practice.

216
internal/apidiff/apidiff.go Normal file
View File

@ -0,0 +1,216 @@
// 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"
)
// 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 package
// documentation.
func Changes(old, new *types.Package) Report {
d := newDiffer(old, new)
d.checkPackage()
return Report{
Incompatible: d.incompatibles.collect(),
Compatible: d.compatibles.collect(),
}
}
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 map[*types.TypeName]types.Type
// Messages.
incompatibles messageSet
compatibles messageSet
}
func newDiffer(old, new *types.Package) *differ {
return &differ{
old: old,
new: new,
correspondMap: map[*types.TypeName]types.Type{},
incompatibles: messageSet{},
compatibles: messageSet{},
}
}
func (d *differ) incompatible(obj types.Object, part, format string, args ...interface{}) {
addMessage(d.incompatibles, obj, part, format, args)
}
func (d *differ) compatible(obj types.Object, part, format string, args ...interface{}) {
addMessage(d.compatibles, obj, part, format, args)
}
func addMessage(ms messageSet, obj types.Object, part, format string, args []interface{}) {
ms.add(obj, part, fmt.Sprintf(format, args...))
}
func (d *differ) checkPackage() {
// Old changes.
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(oldobj, "", "removed")
continue
}
d.checkObjects(oldobj, newobj)
}
// New additions.
for _, name := range d.new.Scope().Names() {
newobj := d.new.Scope().Lookup(name)
if newobj.Exported() && d.old.Scope().Lookup(name) == nil {
d.compatible(newobj, "", "added")
}
}
// Whole-package satisfaction.
// For every old exposed interface oIface and its corresponding new interface nIface...
for otn1, nt1 := range d.correspondMap {
oIface, ok := otn1.Type().Underlying().(*types.Interface)
if !ok {
continue
}
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.
continue
}
// For every old type that implements oIface, its corresponding new type must implement
// nIface.
for otn2, nt2 := range d.correspondMap {
if otn1 == otn2 {
continue
}
if types.Implements(otn2.Type(), oIface) && !types.Implements(nt2, nIface) {
d.incompatible(otn2, "", "no longer implements %s", objectString(otn1))
}
}
}
}
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(old, "", old.Type(), new.Type())
return
}
case *types.Func:
switch new := new.(type) {
case *types.Func:
d.checkCorrespondence(old, "", old.Type(), new.Type())
return
case *types.Var:
d.compatible(old, "", "changed from func to var")
d.checkCorrespondence(old, "", old.Type(), new.Type())
return
}
case *types.TypeName:
if new, ok := new.(*types.TypeName); ok {
d.checkCorrespondence(old, "", old.Type(), new.Type())
return
}
default:
panic("unexpected obj type")
}
// Here if kind of type changed.
d.incompatible(old, "", "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(old, "", 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(old, "", "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 types.Object, part string, old, new types.Type) {
if !d.correspond(old, new) {
d.typeChanged(obj, part, old, new)
}
}
func (d *differ) typeChanged(obj types.Object, part string, old, new types.Type) {
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())
}

View File

@ -0,0 +1,166 @@
package apidiff
import (
"bufio"
"fmt"
"go/types"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
"golang.org/x/tools/go/packages"
)
func TestChanges(t *testing.T) {
dir, err := ioutil.TempDir("", "apidiff_test")
if err != nil {
t.Fatal(err)
}
dir = filepath.Join(dir, "go")
wanti, wantc := splitIntoPackages(t, dir)
defer os.RemoveAll(dir)
sort.Strings(wanti)
sort.Strings(wantc)
oldpkg, err := load("apidiff/old", dir)
if err != nil {
t.Fatal(err)
}
newpkg, err := load("apidiff/new", dir)
if err != nil {
t.Fatal(err)
}
report := Changes(oldpkg.Types, newpkg.Types)
if !reflect.DeepEqual(report.Incompatible, wanti) {
t.Errorf("incompatibles: got %v\nwant %v\n", report.Incompatible, wanti)
}
if !reflect.DeepEqual(report.Compatible, wantc) {
t.Errorf("compatibles: got %v\nwant %v\n", report.Compatible, wantc)
}
}
func splitIntoPackages(t *testing.T, dir string) (incompatibles, compatibles []string) {
// Read the input file line by line.
// Write a line into the old or new package,
// dependent on comments.
// Also collect expected messages.
f, err := os.Open("testdata/tests.go")
if err != nil {
t.Fatal(err)
}
defer f.Close()
if err := os.MkdirAll(filepath.Join(dir, "src", "apidiff"), 0700); err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(filepath.Join(dir, "src", "apidiff", "go.mod"), []byte("module apidiff\n"), 0666); err != nil {
t.Fatal(err)
}
oldd := filepath.Join(dir, "src/apidiff/old")
newd := filepath.Join(dir, "src/apidiff/new")
if err := os.MkdirAll(oldd, 0700); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(newd, 0700); err != nil && !os.IsExist(err) {
t.Fatal(err)
}
oldf, err := os.Create(filepath.Join(oldd, "old.go"))
if err != nil {
t.Fatal(err)
}
newf, err := os.Create(filepath.Join(newd, "new.go"))
if err != nil {
t.Fatal(err)
}
wl := func(f *os.File, line string) {
if _, err := fmt.Fprintln(f, line); err != nil {
t.Fatal(err)
}
}
writeBoth := func(line string) { wl(oldf, line); wl(newf, line) }
writeln := writeBoth
s := bufio.NewScanner(f)
for s.Scan() {
line := s.Text()
tl := strings.TrimSpace(line)
switch {
case tl == "// old":
writeln = func(line string) { wl(oldf, line) }
case tl == "// new":
writeln = func(line string) { wl(newf, line) }
case tl == "// both":
writeln = writeBoth
case strings.HasPrefix(tl, "// i "):
incompatibles = append(incompatibles, strings.TrimSpace(tl[4:]))
case strings.HasPrefix(tl, "// c "):
compatibles = append(compatibles, strings.TrimSpace(tl[4:]))
default:
writeln(line)
}
}
if s.Err() != nil {
t.Fatal(s.Err())
}
return
}
func load(importPath, goPath string) (*packages.Package, error) {
cfg := &packages.Config{
Mode: packages.LoadTypes,
}
if goPath != "" {
cfg.Env = append(os.Environ(), "GOPATH="+goPath)
cfg.Dir = filepath.Join(goPath, "src", filepath.FromSlash(importPath))
}
pkgs, err := packages.Load(cfg, importPath)
if err != nil {
return nil, err
}
if len(pkgs[0].Errors) > 0 {
return nil, pkgs[0].Errors[0]
}
return pkgs[0], nil
}
func TestExportedFields(t *testing.T) {
pkg, err := load("golang.org/x/exp/apidiff/testdata/exported_fields", "")
if err != nil {
t.Fatal(err)
}
typeof := func(name string) types.Type {
return pkg.Types.Scope().Lookup(name).Type()
}
s := typeof("S")
su := s.(*types.Named).Underlying().(*types.Struct)
ef := exportedSelectableFields(su)
wants := []struct {
name string
typ types.Type
}{
{"A1", typeof("A1")},
{"D", types.Typ[types.Bool]},
{"E", types.Typ[types.Int]},
{"F", typeof("F")},
{"S", types.NewPointer(s)},
}
if got, want := len(ef), len(wants); got != want {
t.Errorf("got %d fields, want %d\n%+v", got, want, ef)
}
for _, w := range wants {
if got := ef[w.name]; got != nil && !types.Identical(got.Type(), w.typ) {
t.Errorf("%s: got %v, want %v", w.name, got.Type(), w.typ)
}
}
}

View File

@ -0,0 +1,361 @@
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
}

View File

@ -0,0 +1,219 @@
package apidiff
import (
"go/types"
"sort"
)
// Two types are correspond if they are identical except for defined types,
// which must correspond.
//
// Two defined types correspond if they can be interchanged in the old and new APIs,
// possibly after a renaming.
//
// This is not a pure function. If we come across named types while traversing,
// we establish correspondence.
func (d *differ) correspond(old, new types.Type) bool {
return d.corr(old, new, nil)
}
// corr determines whether old and new correspond. The argument p is a list of
// known interface identities, to avoid infinite recursion.
//
// corr calls itself recursively as much as possible, to establish more
// correspondences and so check more of the API. E.g. if the new function has more
// parameters than the old, compare all the old ones before returning false.
//
// Compare this to the implementation of go/types.Identical.
func (d *differ) corr(old, new types.Type, p *ifacePair) bool {
// Structure copied from types.Identical.
switch old := old.(type) {
case *types.Basic:
return types.Identical(old, new)
case *types.Array:
if new, ok := new.(*types.Array); ok {
return d.corr(old.Elem(), new.Elem(), p) && old.Len() == new.Len()
}
case *types.Slice:
if new, ok := new.(*types.Slice); ok {
return d.corr(old.Elem(), new.Elem(), p)
}
case *types.Map:
if new, ok := new.(*types.Map); ok {
return d.corr(old.Key(), new.Key(), p) && d.corr(old.Elem(), new.Elem(), p)
}
case *types.Chan:
if new, ok := new.(*types.Chan); ok {
return d.corr(old.Elem(), new.Elem(), p) && old.Dir() == new.Dir()
}
case *types.Pointer:
if new, ok := new.(*types.Pointer); ok {
return d.corr(old.Elem(), new.Elem(), p)
}
case *types.Signature:
if new, ok := new.(*types.Signature); ok {
pe := d.corr(old.Params(), new.Params(), p)
re := d.corr(old.Results(), new.Results(), p)
return old.Variadic() == new.Variadic() && pe && re
}
case *types.Tuple:
if new, ok := new.(*types.Tuple); ok {
for i := 0; i < old.Len(); i++ {
if i >= new.Len() || !d.corr(old.At(i).Type(), new.At(i).Type(), p) {
return false
}
}
return old.Len() == new.Len()
}
case *types.Struct:
if new, ok := new.(*types.Struct); ok {
for i := 0; i < old.NumFields(); i++ {
if i >= new.NumFields() {
return false
}
of := old.Field(i)
nf := new.Field(i)
if of.Anonymous() != nf.Anonymous() ||
old.Tag(i) != new.Tag(i) ||
!d.corr(of.Type(), nf.Type(), p) ||
!d.corrFieldNames(of, nf) {
return false
}
}
return old.NumFields() == new.NumFields()
}
case *types.Interface:
if new, ok := new.(*types.Interface); ok {
// Deal with circularity. See the comment in types.Identical.
q := &ifacePair{old, new, p}
for p != nil {
if p.identical(q) {
return true // same pair was compared before
}
p = p.prev
}
oldms := d.sortedMethods(old)
newms := d.sortedMethods(new)
for i, om := range oldms {
if i >= len(newms) {
return false
}
nm := newms[i]
if d.methodID(om) != d.methodID(nm) || !d.corr(om.Type(), nm.Type(), q) {
return false
}
}
return old.NumMethods() == new.NumMethods()
}
case *types.Named:
if new, ok := new.(*types.Named); ok {
return d.establishCorrespondence(old, new)
}
if new, ok := new.(*types.Basic); ok {
// Basic types are defined types, too, so we have to support them.
return d.establishCorrespondence(old, new)
}
default:
panic("unknown type kind")
}
return false
}
// Compare old and new field names. We are determining correspondence across packages,
// so just compare names, not packages. For an unexported, embedded field of named
// type (non-named embedded fields are possible with aliases), we check that the type
// names correspond. We check the types for correspondence before this is called, so
// we've established correspondence.
func (d *differ) corrFieldNames(of, nf *types.Var) bool {
if of.Anonymous() && nf.Anonymous() && !of.Exported() && !nf.Exported() {
if on, ok := of.Type().(*types.Named); ok {
nn := nf.Type().(*types.Named)
return d.establishCorrespondence(on, nn)
}
}
return of.Name() == nf.Name()
}
// Establish that old corresponds with new if it does not already
// correspond to something else.
func (d *differ) establishCorrespondence(old *types.Named, new types.Type) bool {
oldname := old.Obj()
oldc := d.correspondMap[oldname]
if oldc == nil {
// For now, assume the types don't correspond unless they are from the old
// and new packages, respectively.
//
// This is too conservative. For instance,
// [old] type A = q.B; [new] type A q.C
// could be OK if in package q, B is an alias for C.
// Or, using p as the name of the current old/new packages:
// [old] type A = q.B; [new] type A int
// could be OK if in q,
// [old] type B int; [new] type B = p.A
// In this case, p.A and q.B name the same type in both old and new worlds.
// Note that this case doesn't imply circular package imports: it's possible
// that in the old world, p imports q, but in the new, q imports p.
//
// However, if we didn't do something here, then we'd incorrectly allow cases
// like the first one above in which q.B is not an alias for q.C
//
// What we should do is check that the old type, in the new world's package
// of the same path, doesn't correspond to something other than the new type.
// That is a bit hard, because there is no easy way to find a new package
// matching an old one.
if newn, ok := new.(*types.Named); ok {
if old.Obj().Pkg() != d.old || newn.Obj().Pkg() != d.new {
return old.Obj().Id() == newn.Obj().Id()
}
}
// If there is no correspondence, create one.
d.correspondMap[oldname] = new
// Check that the corresponding types are compatible.
d.checkCompatibleDefined(oldname, old, new)
return true
}
return types.Identical(oldc, new)
}
func (d *differ) sortedMethods(iface *types.Interface) []*types.Func {
ms := make([]*types.Func, iface.NumMethods())
for i := 0; i < iface.NumMethods(); i++ {
ms[i] = iface.Method(i)
}
sort.Slice(ms, func(i, j int) bool { return d.methodID(ms[i]) < d.methodID(ms[j]) })
return ms
}
func (d *differ) methodID(m *types.Func) string {
// If the method belongs to one of the two packages being compared, use
// just its name even if it's unexported. That lets us treat unexported names
// from the old and new packages as equal.
if m.Pkg() == d.old || m.Pkg() == d.new {
return m.Name()
}
return m.Id()
}
// Copied from the go/types package:
// An ifacePair is a node in a stack of interface type pairs compared for identity.
type ifacePair struct {
x, y *types.Interface
prev *ifacePair
}
func (p *ifacePair) identical(q *ifacePair) bool {
return p.x == q.x && p.y == q.y || p.x == q.y && p.y == q.x
}

View File

@ -0,0 +1,79 @@
// TODO: show that two-non-empty dotjoin can happen, by using an anon struct as a field type
// TODO: don't report removed/changed methods for both value and pointer method sets?
package apidiff
import (
"fmt"
"go/types"
"sort"
"strings"
)
// There can be at most one message for each object or part thereof.
// Parts include interface methods and struct fields.
//
// The part thing is necessary. Method (Func) objects have sufficient info, but field
// Vars do not: they just have a field name and a type, without the enclosing struct.
type messageSet map[types.Object]map[string]string
// Add a message for obj and part, overwriting a previous message
// (shouldn't happen).
// obj is required but part can be empty.
func (m messageSet) add(obj types.Object, part, msg string) {
s := m[obj]
if s == nil {
s = map[string]string{}
m[obj] = s
}
if f, ok := s[part]; ok && f != msg {
fmt.Printf("! second, different message for obj %s, part %q\n", obj, part)
fmt.Printf(" first: %s\n", f)
fmt.Printf(" second: %s\n", msg)
}
s[part] = msg
}
func (m messageSet) collect() []string {
var s []string
for obj, parts := range m {
// Format each object name relative to its own package.
objstring := objectString(obj)
for part, msg := range parts {
var p string
if strings.HasPrefix(part, ",") {
p = objstring + part
} else {
p = dotjoin(objstring, part)
}
s = append(s, p+": "+msg)
}
}
sort.Strings(s)
return s
}
func objectString(obj types.Object) string {
if f, ok := obj.(*types.Func); ok {
sig := f.Type().(*types.Signature)
if recv := sig.Recv(); recv != nil {
tn := types.TypeString(recv.Type(), types.RelativeTo(obj.Pkg()))
if tn[0] == '*' {
tn = "(" + tn + ")"
}
return fmt.Sprintf("%s.%s", tn, obj.Name())
}
}
return obj.Name()
}
func dotjoin(s1, s2 string) string {
if s1 == "" {
return s2
}
if s2 == "" {
return s1
}
return s1 + "." + s2
}

View File

@ -0,0 +1,55 @@
package apidiff
import (
"bytes"
"fmt"
"io"
)
// Report describes the changes detected by Changes.
type Report struct {
Incompatible, Compatible []string
}
func (r Report) String() string {
var buf bytes.Buffer
if err := r.Text(&buf); err != nil {
return fmt.Sprintf("!!%v", err)
}
return buf.String()
}
func (r Report) Text(w io.Writer) error {
if err := r.TextIncompatible(w, true); err != nil {
return err
}
return r.TextCompatible(w)
}
func (r Report) TextIncompatible(w io.Writer, withHeader bool) error {
if withHeader {
return r.writeMessages(w, "Incompatible changes:", r.Incompatible)
}
return r.writeMessages(w, "", r.Incompatible)
}
func (r Report) TextCompatible(w io.Writer) error {
return r.writeMessages(w, "Compatible changes:", r.Compatible)
}
func (r Report) writeMessages(w io.Writer, header string, msgs []string) error {
if len(msgs) == 0 {
return nil
}
if header != "" {
if _, err := fmt.Fprintf(w, "%s\n", header); err != nil {
return err
}
}
for _, m := range msgs {
if _, err := fmt.Fprintf(w, "- %s\n", m); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,37 @@
package exported_fields
// Used for testing exportedFields.
// Its exported fields are:
// A1 [1]int
// D bool
// E int
// F F
// S *S
type (
S struct {
int
*embed2
embed
E int // shadows embed.E
alias
A1
*S
}
A1 [1]int
embed struct {
E string
}
embed2 struct {
embed3
F // shadows embed3.F
}
embed3 struct {
F bool
}
alias = struct{ D bool }
F int
)

924
internal/apidiff/testdata/tests.go vendored Normal file
View File

@ -0,0 +1,924 @@
// This file is split into two packages, old and new.
// It is syntactically valid Go so that gofmt can process it.
//
// If a comment begins with: Then:
// old write subsequent lines to the "old" package
// new write subsequent lines to the "new" package
// both write subsequent lines to both packages
// c expect a compatible error with the following text
// i expect an incompatible error with the following text
package ignore
// both
import "io"
//////////////// Basics
//// Same type in both: OK.
// both
type A int
//// Changing the type is an incompatible change.
// old
type B int
// new
// i B: changed from int to string
type B string
//// Adding a new type, whether alias or not, is a compatible change.
// new
// c AA: added
type AA = A
// c B1: added
type B1 bool
//// Change of type for an unexported name doesn't matter...
// old
type t int
// new
type t string // OK: t isn't part of the API
//// ...unless it is exposed.
// both
var V2 u
// old
type u string
// new
// i u: changed from string to int
type u int
//// An exposed, unexported type can be renamed.
// both
type u2 int
// old
type u1 int
var V5 u1
// new
var V5 u2 // OK: V5 has changed type, but old u1 corresopnds to new u2
//// Splitting a single type into two is an incompatible change.
// both
type u3 int
// old
type (
Split1 = u1
Split2 = u1
)
// new
type (
Split1 = u2 // OK, since old u1 corresponds to new u2
// This tries to make u1 correspond to u3
// i Split2: changed from u1 to u3
Split2 = u3
)
//// Merging two types into one is OK.
// old
type (
GoodMerge1 = u2
GoodMerge2 = u3
)
// new
type (
GoodMerge1 = u3
GoodMerge2 = u3
)
//// Merging isn't OK here because a method is lost.
// both
type u4 int
func (u4) M() {}
// old
type (
BadMerge1 = u3
BadMerge2 = u4
)
// new
type (
BadMerge1 = u3
// i u4.M: removed
// What's really happening here is that old u4 corresponds to new u3,
// and new u3's method set is not a superset of old u4's.
BadMerge2 = u3
)
// old
type Rem int
// new
// i Rem: removed
//////////////// Constants
//// type changes
// old
const (
C1 = 1
C2 int = 2
C3 = 3
C4 u1 = 4
)
var V8 int
// new
const (
// i C1: changed from untyped int to untyped string
C1 = "1"
// i C2: changed from int to untyped int
C2 = -1
// i C3: changed from untyped int to int
C3 int = 3
// i V8: changed from var to const
V8 int = 1
C4 u2 = 4 // OK: u1 corresponds to u2
)
// value change
// old
const (
Cr1 = 1
Cr2 = "2"
Cr3 = 3.5
Cr4 = complex(0, 4.1)
)
// new
const (
// i Cr1: value changed from 1 to -1
Cr1 = -1
// i Cr2: value changed from "2" to "3"
Cr2 = "3"
// i Cr3: value changed from 3.5 to 3.8
Cr3 = 3.8
// i Cr4: value changed from (0 + 4.1i) to (4.1 + 0i)
Cr4 = complex(4.1, 0)
)
//////////////// Variables
//// simple type changes
// old
var (
V1 string
V3 A
V7 <-chan int
)
// new
var (
// i V1: changed from string to []string
V1 []string
V3 A // OK: same
// i V7: changed from <-chan int to chan int
V7 chan int
)
//// interface type changes
// old
var (
V9 interface{ M() }
V10 interface{ M() }
V11 interface{ M() }
)
// new
var (
// i V9: changed from interface{M()} to interface{}
V9 interface{}
// i V10: changed from interface{M()} to interface{M(); M2()}
V10 interface {
M2()
M()
}
// i V11: changed from interface{M()} to interface{M(int)}
V11 interface{ M(int) }
)
//// struct type changes
// old
var (
VS1 struct{ A, B int }
VS2 struct{ A, B int }
VS3 struct{ A, B int }
VS4 struct {
A int
u1
}
)
// new
var (
// i VS1: changed from struct{A int; B int} to struct{B int; A int}
VS1 struct{ B, A int }
// i VS2: changed from struct{A int; B int} to struct{A int}
VS2 struct{ A int }
// i VS3: changed from struct{A int; B int} to struct{A int; B int; C int}
VS3 struct{ A, B, C int }
VS4 struct {
A int
u2
}
)
//////////////// Types
// old
const C5 = 3
type (
A1 [1]int
A2 [2]int
A3 [C5]int
)
// new
// i C5: value changed from 3 to 4
const C5 = 4
type (
A1 [1]int
// i A2: changed from [2]int to [2]bool
A2 [2]bool
// i A3: changed from [3]int to [4]int
A3 [C5]int
)
// old
type (
Sl []int
P1 *int
P2 *u1
)
// new
type (
// i Sl: changed from []int to []string
Sl []string
// i P1: changed from *int to **bool
P1 **bool
P2 *u2 // OK: u1 corresponds to u2
)
// old
type Bc1 int32
type Bc2 uint
type Bc3 float32
type Bc4 complex64
// new
// c Bc1: changed from int32 to int
type Bc1 int
// c Bc2: changed from uint to uint64
type Bc2 uint64
// c Bc3: changed from float32 to float64
type Bc3 float64
// c Bc4: changed from complex64 to complex128
type Bc4 complex128
// old
type Bi1 int32
type Bi2 uint
type Bi3 float64
type Bi4 complex128
// new
// i Bi1: changed from int32 to int16
type Bi1 int16
// i Bi2: changed from uint to uint32
type Bi2 uint32
// i Bi3: changed from float64 to float32
type Bi3 float32
// i Bi4: changed from complex128 to complex64
type Bi4 complex64
// old
type (
M1 map[string]int
M2 map[string]int
M3 map[string]int
)
// new
type (
M1 map[string]int
// i M2: changed from map[string]int to map[int]int
M2 map[int]int
// i M3: changed from map[string]int to map[string]string
M3 map[string]string
)
// old
type (
Ch1 chan int
Ch2 <-chan int
Ch3 chan int
Ch4 <-chan int
)
// new
type (
// i Ch1, element type: changed from int to bool
Ch1 chan bool
// i Ch2: changed direction
Ch2 chan<- int
// i Ch3: changed direction
Ch3 <-chan int
// c Ch4: removed direction
Ch4 chan int
)
// old
type I1 interface {
M1()
M2()
}
// new
type I1 interface {
// M1()
// i I1.M1: removed
M2(int)
// i I1.M2: changed from func() to func(int)
M3()
// i I1.M3: added
m()
// i I1.m: added unexported method
}
// old
type I2 interface {
M1()
m()
}
// new
type I2 interface {
M1()
// m() Removing an unexported method is OK.
m2() // OK, because old already had an unexported method
// c I2.M2: added
M2()
}
// old
type I3 interface {
io.Reader
M()
}
// new
// OK: what matters is the method set; the name of the embedded
// interface isn't important.
type I3 interface {
M()
Read([]byte) (int, error)
}
// old
type I4 io.Writer
// new
// OK: in both, I4 is a distinct type from io.Writer, and
// the old and new I4s have the same method set.
type I4 interface {
Write([]byte) (int, error)
}
// old
type I5 = io.Writer
// new
// i I5: changed from io.Writer to I5
// In old, I5 and io.Writer are the same type; in new,
// they are different. That can break something like:
// var _ func(io.Writer) = func(pkg.I6) {}
type I5 io.Writer
// old
type I6 interface{ Write([]byte) (int, error) }
// new
// i I6: changed from I6 to io.Writer
// Similar to the above.
type I6 = io.Writer
//// correspondence with a basic type
// Basic types are technically defined types, but they aren't
// represented that way in go/types, so the cases below are special.
// both
type T1 int
// old
var VT1 T1
// new
// i VT1: changed from T1 to int
// This fails because old T1 corresponds to both int and new T1.
var VT1 int
// old
type t2 int
var VT2 t2
// new
// OK: t2 corresponds to int. It's fine that old t2
// doesn't exist in new.
var VT2 int
// both
type t3 int
func (t3) M() {}
// old
var VT3 t3
// new
// i t3.M: removed
// Here the change from t3 to int is incompatible
// because old t3 has an exported method.
var VT3 int
// old
var VT4 int
// new
type t4 int
// i VT4: changed from int to t4
// This is incompatible because of code like
// VT4 + int(1)
// which works in old but fails in new.
// The difference from the above cases is that
// in those, we were merging two types into one;
// here, we are splitting int into t4 and int.
var VT4 t4
//////////////// Functions
// old
func F1(a int, b string) map[u1]A { return nil }
func F2(int) {}
func F3(int) {}
func F4(int) int { return 0 }
func F5(int) int { return 0 }
func F6(int) {}
func F7(interface{}) {}
// new
func F1(c int, d string) map[u2]AA { return nil } //OK: same (since u1 corresponds to u2)
// i F2: changed from func(int) to func(int) bool
func F2(int) bool { return true }
// i F3: changed from func(int) to func(int, int)
func F3(int, int) {}
// i F4: changed from func(int) int to func(bool) int
func F4(bool) int { return 0 }
// i F5: changed from func(int) int to func(int) string
func F5(int) string { return "" }
// i F6: changed from func(int) to func(...int)
func F6(...int) {}
// i F7: changed from func(interface{}) to func(interface{x()})
func F7(a interface{ x() }) {}
// old
func F8(bool) {}
// new
// c F8: changed from func to var
var F8 func(bool)
// old
var F9 func(int)
// new
// i F9: changed from var to func
func F9(int) {}
// both
// OK, even though new S1 is incompatible with old S1 (see below)
func F10(S1) {}
//////////////// Structs
// old
type S1 struct {
A int
B string
C bool
d float32
}
// new
type S1 = s1
type s1 struct {
C chan int
// i S1.C: changed from bool to chan int
A int
// i S1.B: removed
// i S1: old is comparable, new is not
x []int
d float32
E bool
// c S1.E: added
}
// old
type embed struct {
E string
}
type S2 struct {
A int
embed
}
// new
type embedx struct {
E string
}
type S2 struct {
embedx // OK: the unexported embedded field changed names, but the exported field didn't
A int
}
// both
type F int
// old
type S3 struct {
A int
embed
}
// new
type embed struct{ F int }
type S3 struct {
// i S3.E: removed
embed
// c S3.F: added
A int
}
// old
type embed2 struct {
embed3
F // shadows embed3.F
}
type embed3 struct {
F bool
}
type alias = struct{ D bool }
type S4 struct {
int
*embed2
embed
E int // shadows embed.E
alias
A1
*S4
}
// new
type S4 struct {
// OK: removed unexported fields
// D and F marked as added because they are now part of the immediate fields
D bool
// c S4.D: added
E int // OK: same as in old
F F
// c S4.F: added
A1 // OK: same
*S4 // OK: same (recursive embedding)
}
//// Difference between exported selectable fields and exported immediate fields.
// both
type S5 struct{ A int }
// old
// Exported immediate fields: A, S5
// Exported selectable fields: A int, S5 S5
type S6 struct {
S5 S5
A int
}
// new
// Exported immediate fields: S5
// Exported selectable fields: A int, S5 S5.
// i S6.A: removed
type S6 struct {
S5
}
//// Ambiguous fields can exist; they just can't be selected.
// both
type (
embed7a struct{ E int }
embed7b struct{ E bool }
)
// old
type S7 struct { // legal, but no selectable fields
embed7a
embed7b
}
// new
type S7 struct {
embed7a
embed7b
// c S7.E: added
E string
}
//////////////// Method sets
// old
type SM struct {
embedm
Embedm
}
func (SM) V1() {}
func (SM) V2() {}
func (SM) V3() {}
func (SM) V4() {}
func (SM) v() {}
func (*SM) P1() {}
func (*SM) P2() {}
func (*SM) P3() {}
func (*SM) P4() {}
func (*SM) p() {}
type embedm int
func (embedm) EV1() {}
func (embedm) EV2() {}
func (embedm) EV3() {}
func (*embedm) EP1() {}
func (*embedm) EP2() {}
func (*embedm) EP3() {}
type Embedm struct {
A int
}
func (Embedm) FV() {}
func (*Embedm) FP() {}
type RepeatEmbedm struct {
Embedm
}
// new
type SM struct {
embedm2
embedm3
Embedm
// i SM.A: changed from int to bool
}
// c SMa: added
type SMa = SM
func (SM) V1() {} // OK: same
// func (SM) V2() {}
// i SM.V2: removed
// i SM.V3: changed from func() to func(int)
func (SM) V3(int) {}
// c SM.V5: added
func (SM) V5() {}
func (SM) v(int) {} // OK: unexported method change
func (SM) v2() {} // OK: unexported method added
func (*SM) P1() {} // OK: same
//func (*SM) P2() {}
// i (*SM).P2: removed
// i (*SM).P3: changed from func() to func(int)
func (*SMa) P3(int) {}
// c (*SM).P5: added
func (*SM) P5() {}
// func (*SM) p() {} // OK: unexported method removed
// Changing from a value to a pointer receiver or vice versa
// just looks like adding and removing a method.
// i SM.V4: removed
// i (*SM).V4: changed from func() to func(int)
func (*SM) V4(int) {}
// c SM.P4: added
// P4 is not removed from (*SM) because value methods
// are in the pointer method set.
func (SM) P4() {}
type embedm2 int
// i embedm.EV1: changed from func() to func(int)
func (embedm2) EV1(int) {}
// i embedm.EV2, method set of SM: removed
// i embedm.EV2, method set of *SM: removed
// i (*embedm).EP2, method set of *SM: removed
func (*embedm2) EP1() {}
type embedm3 int
func (embedm3) EV3() {} // OK: compatible with old embedm.EV3
func (*embedm3) EP3() {} // OK: compatible with old (*embedm).EP3
type Embedm struct {
// i Embedm.A: changed from int to bool
A bool
}
// i Embedm.FV: changed from func() to func(int)
func (Embedm) FV(int) {}
func (*Embedm) FP() {}
type RepeatEmbedm struct {
// i RepeatEmbedm.A: changed from int to bool
Embedm
}
//////////////// Whole-package interface satisfaction
// old
type WI1 interface {
M1()
m1()
}
type WI2 interface {
M2()
m2()
}
type WS1 int
func (WS1) M1() {}
func (WS1) m1() {}
type WS2 int
func (WS2) M2() {}
func (WS2) m2() {}
// new
type WI1 interface {
M1()
m()
}
type WS1 int
func (WS1) M1() {}
// i WS1: no longer implements WI1
//func (WS1) m1() {}
type WI2 interface {
M2()
m2()
// i WS2: no longer implements WI2
m3()
}
type WS2 int
func (WS2) M2() {}
func (WS2) m2() {}
//////////////// Miscellany
// This verifies that the code works even through
// multiple levels of unexported typed.
// old
var Z w
type w []x
type x []z
type z int
// new
var Z w
type w []x
type x []z
// i z: changed from int to bool
type z bool
// old
type H struct{}
func (H) M() {}
// new
// i H: changed from struct{} to interface{M()}
type H interface {
M()
}
//// Splitting types
//// OK: in both old and new, {J1, K1, L1} name the same type.
// old
type (
J1 = K1
K1 = L1
L1 int
)
// new
type (
J1 = K1
K1 int
L1 = J1
)
//// Old has one type, K2; new has J2 and K2.
// both
type K2 int
// old
type J2 = K2
// new
// i K2: changed from K2 to K2
type J2 K2 // old K2 corresponds with new J2
// old K2 also corresponds with new K2: problem
// both
type k3 int
var Vj3 j3 // expose j3
// old
type j3 = k3
// new
// OK: k3 isn't exposed
type j3 k3
// both
type k4 int
var Vj4 j4 // expose j4
var VK4 k4 // expose k4
// old
type j4 = k4
// new
// i Vj4: changed from k4 to j4
// e.g. p.Vj4 = p.Vk4
type j4 k4