golang.org/x/exp@v0.0.0-20240506185415-9bf2ced13842/apidiff/apidiff.go (about)

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