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:
parent
04e50493df
commit
9e5445377b
624
internal/apidiff/README.md
Normal file
624
internal/apidiff/README.md
Normal 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
216
internal/apidiff/apidiff.go
Normal 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())
|
||||
}
|
166
internal/apidiff/apidiff_test.go
Normal file
166
internal/apidiff/apidiff_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
361
internal/apidiff/compatibility.go
Normal file
361
internal/apidiff/compatibility.go
Normal 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
|
||||
}
|
219
internal/apidiff/correspondence.go
Normal file
219
internal/apidiff/correspondence.go
Normal 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
|
||||
}
|
79
internal/apidiff/messageset.go
Normal file
79
internal/apidiff/messageset.go
Normal 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
|
||||
}
|
55
internal/apidiff/report.go
Normal file
55
internal/apidiff/report.go
Normal 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
|
||||
}
|
37
internal/apidiff/testdata/exported_fields/ef.go
vendored
Normal file
37
internal/apidiff/testdata/exported_fields/ef.go
vendored
Normal 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
924
internal/apidiff/testdata/tests.go
vendored
Normal 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
|
Loading…
Reference in New Issue
Block a user