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

     1  // Command apidiff determines whether two versions of a package are compatible
     2  package main
     3  
     4  import (
     5  	"bufio"
     6  	"flag"
     7  	"fmt"
     8  	"go/token"
     9  	"go/types"
    10  	"os"
    11  	"strings"
    12  
    13  	"golang.org/x/exp/apidiff"
    14  	"golang.org/x/tools/go/gcexportdata"
    15  	"golang.org/x/tools/go/packages"
    16  )
    17  
    18  var (
    19  	exportDataOutfile = flag.String("w", "", "file for export data")
    20  	incompatibleOnly  = flag.Bool("incompatible", false, "display only incompatible changes")
    21  	allowInternal     = flag.Bool("allow-internal", false, "allow apidiff to compare internal packages")
    22  	moduleMode        = flag.Bool("m", false, "compare modules instead of packages")
    23  )
    24  
    25  func main() {
    26  	flag.Usage = func() {
    27  		w := flag.CommandLine.Output()
    28  		fmt.Fprintf(w, "usage:\n")
    29  		fmt.Fprintf(w, "apidiff OLD NEW\n")
    30  		fmt.Fprintf(w, "   compares OLD and NEW package APIs\n")
    31  		fmt.Fprintf(w, "   where OLD and NEW are either import paths or files of export data\n")
    32  		fmt.Fprintf(w, "apidiff -m OLD NEW\n")
    33  		fmt.Fprintf(w, "   compares OLD and NEW module APIs\n")
    34  		fmt.Fprintf(w, "   where OLD and NEW are module paths\n")
    35  		fmt.Fprintf(w, "apidiff -w FILE IMPORT_PATH\n")
    36  		fmt.Fprintf(w, "   writes export data of the package at IMPORT_PATH to FILE\n")
    37  		fmt.Fprintf(w, "   NOTE: In a GOPATH-less environment, this option consults the\n")
    38  		fmt.Fprintf(w, "   module cache by default, unless used in the directory that\n")
    39  		fmt.Fprintf(w, "   contains the go.mod module definition that IMPORT_PATH belongs\n")
    40  		fmt.Fprintf(w, "   to. In most cases users want the latter behavior, so be sure\n")
    41  		fmt.Fprintf(w, "   to cd to the exact directory which contains the module\n")
    42  		fmt.Fprintf(w, "   definition of IMPORT_PATH.\n")
    43  		fmt.Fprintf(w, "apidiff -m -w FILE MODULE_PATH\n")
    44  		fmt.Fprintf(w, "   writes export data of the module at MODULE_PATH to FILE\n")
    45  		fmt.Fprintf(w, "   Same NOTE for packages applies to modules.\n")
    46  		flag.PrintDefaults()
    47  	}
    48  
    49  	flag.Parse()
    50  	if *exportDataOutfile != "" {
    51  		if len(flag.Args()) != 1 {
    52  			flag.Usage()
    53  			os.Exit(2)
    54  		}
    55  		if err := loadAndWrite(flag.Arg(0)); err != nil {
    56  			die("writing export data: %v", err)
    57  		}
    58  		os.Exit(0)
    59  	}
    60  
    61  	if len(flag.Args()) != 2 {
    62  		flag.Usage()
    63  		os.Exit(2)
    64  	}
    65  
    66  	var report apidiff.Report
    67  	if *moduleMode {
    68  		oldmod := mustLoadOrReadModule(flag.Arg(0))
    69  		newmod := mustLoadOrReadModule(flag.Arg(1))
    70  
    71  		report = apidiff.ModuleChanges(oldmod, newmod)
    72  	} else {
    73  		oldpkg := mustLoadOrReadPackage(flag.Arg(0))
    74  		newpkg := mustLoadOrReadPackage(flag.Arg(1))
    75  		if !*allowInternal {
    76  			if isInternalPackage(oldpkg.Path(), "") && isInternalPackage(newpkg.Path(), "") {
    77  				fmt.Fprintf(os.Stderr, "Ignoring internal package %s\n", oldpkg.Path())
    78  				os.Exit(0)
    79  			}
    80  		}
    81  		report = apidiff.Changes(oldpkg, newpkg)
    82  	}
    83  
    84  	var err error
    85  	if *incompatibleOnly {
    86  		err = report.TextIncompatible(os.Stdout, false)
    87  	} else {
    88  		err = report.Text(os.Stdout)
    89  	}
    90  	if err != nil {
    91  		die("writing report: %v", err)
    92  	}
    93  }
    94  
    95  func loadAndWrite(path string) error {
    96  	if *moduleMode {
    97  		module := mustLoadModule(path)
    98  		return writeModuleExportData(module, *exportDataOutfile)
    99  	}
   100  
   101  	// Loading and writing data for only a single package.
   102  	pkg := mustLoadPackage(path)
   103  	return writePackageExportData(pkg, *exportDataOutfile)
   104  }
   105  
   106  func mustLoadOrReadPackage(importPathOrFile string) *types.Package {
   107  	fileInfo, err := os.Stat(importPathOrFile)
   108  	if err == nil && fileInfo.Mode().IsRegular() {
   109  		pkg, err := readPackageExportData(importPathOrFile)
   110  		if err != nil {
   111  			die("reading export data from %s: %v", importPathOrFile, err)
   112  		}
   113  		return pkg
   114  	} else {
   115  		return mustLoadPackage(importPathOrFile).Types
   116  	}
   117  }
   118  
   119  func mustLoadPackage(importPath string) *packages.Package {
   120  	pkg, err := loadPackage(importPath)
   121  	if err != nil {
   122  		die("loading %s: %v", importPath, err)
   123  	}
   124  	return pkg
   125  }
   126  
   127  func loadPackage(importPath string) (*packages.Package, error) {
   128  	cfg := &packages.Config{Mode: packages.LoadTypes |
   129  		packages.NeedName | packages.NeedTypes | packages.NeedImports | packages.NeedDeps,
   130  	}
   131  	pkgs, err := packages.Load(cfg, importPath)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	if len(pkgs) == 0 {
   136  		return nil, fmt.Errorf("found no packages for import %s", importPath)
   137  	}
   138  	if len(pkgs[0].Errors) > 0 {
   139  		// TODO: use errors.Join once Go 1.21 is released.
   140  		return nil, pkgs[0].Errors[0]
   141  	}
   142  	return pkgs[0], nil
   143  }
   144  
   145  func mustLoadOrReadModule(modulePathOrFile string) *apidiff.Module {
   146  	var module *apidiff.Module
   147  	fileInfo, err := os.Stat(modulePathOrFile)
   148  	if err == nil && fileInfo.Mode().IsRegular() {
   149  		module, err = readModuleExportData(modulePathOrFile)
   150  		if err != nil {
   151  			die("reading export data from %s: %v", modulePathOrFile, err)
   152  		}
   153  	} else {
   154  		module = mustLoadModule(modulePathOrFile)
   155  	}
   156  
   157  	filterInternal(module, *allowInternal)
   158  
   159  	return module
   160  }
   161  
   162  func mustLoadModule(modulepath string) *apidiff.Module {
   163  	module, err := loadModule(modulepath)
   164  	if err != nil {
   165  		die("loading %s: %v", modulepath, err)
   166  	}
   167  	return module
   168  }
   169  
   170  func loadModule(modulepath string) (*apidiff.Module, error) {
   171  	cfg := &packages.Config{Mode: packages.LoadTypes |
   172  		packages.NeedName | packages.NeedTypes | packages.NeedImports | packages.NeedDeps | packages.NeedModule,
   173  	}
   174  	loaded, err := packages.Load(cfg, fmt.Sprintf("%s/...", modulepath))
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  	if len(loaded) == 0 {
   179  		return nil, fmt.Errorf("found no packages for module %s", modulepath)
   180  	}
   181  	var tpkgs []*types.Package
   182  	for _, p := range loaded {
   183  		if len(p.Errors) > 0 {
   184  			// TODO: use errors.Join once Go 1.21 is released.
   185  			return nil, p.Errors[0]
   186  		}
   187  		tpkgs = append(tpkgs, p.Types)
   188  	}
   189  
   190  	return &apidiff.Module{Path: loaded[0].Module.Path, Packages: tpkgs}, nil
   191  }
   192  
   193  func readModuleExportData(filename string) (*apidiff.Module, error) {
   194  	f, err := os.Open(filename)
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  	defer f.Close()
   199  	r := bufio.NewReader(f)
   200  	modPath, err := r.ReadString('\n')
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  	modPath = modPath[:len(modPath)-1] // remove delimiter
   205  	m := map[string]*types.Package{}
   206  	pkgs, err := gcexportdata.ReadBundle(r, token.NewFileSet(), m)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  
   211  	return &apidiff.Module{Path: modPath, Packages: pkgs}, nil
   212  }
   213  
   214  func writeModuleExportData(module *apidiff.Module, filename string) error {
   215  	f, err := os.Create(filename)
   216  	if err != nil {
   217  		return err
   218  	}
   219  	fmt.Fprintln(f, module.Path)
   220  	// TODO: Determine if token.NewFileSet is appropriate here.
   221  	if err := gcexportdata.WriteBundle(f, token.NewFileSet(), module.Packages); err != nil {
   222  		return err
   223  	}
   224  	return f.Close()
   225  }
   226  
   227  func readPackageExportData(filename string) (*types.Package, error) {
   228  	f, err := os.Open(filename)
   229  	if err != nil {
   230  		return nil, err
   231  	}
   232  	defer f.Close()
   233  	r := bufio.NewReader(f)
   234  	m := map[string]*types.Package{}
   235  	pkgPath, err := r.ReadString('\n')
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  	pkgPath = pkgPath[:len(pkgPath)-1] // remove delimiter
   240  	return gcexportdata.Read(r, token.NewFileSet(), m, pkgPath)
   241  }
   242  
   243  func writePackageExportData(pkg *packages.Package, filename string) error {
   244  	f, err := os.Create(filename)
   245  	if err != nil {
   246  		return err
   247  	}
   248  	// Include the package path in the file. The exportdata format does
   249  	// not record the path of the package being written.
   250  	fmt.Fprintln(f, pkg.PkgPath)
   251  	err1 := gcexportdata.Write(f, pkg.Fset, pkg.Types)
   252  	err2 := f.Close()
   253  	if err1 != nil {
   254  		return err1
   255  	}
   256  	return err2
   257  }
   258  
   259  func die(format string, args ...interface{}) {
   260  	fmt.Fprintf(os.Stderr, format+"\n", args...)
   261  	os.Exit(1)
   262  }
   263  
   264  func filterInternal(m *apidiff.Module, allow bool) {
   265  	if allow {
   266  		return
   267  	}
   268  
   269  	var nonInternal []*types.Package
   270  	for _, p := range m.Packages {
   271  		if !isInternalPackage(p.Path(), m.Path) {
   272  			nonInternal = append(nonInternal, p)
   273  		} else {
   274  			fmt.Fprintf(os.Stderr, "Ignoring internal package %s\n", p.Path())
   275  		}
   276  	}
   277  	m.Packages = nonInternal
   278  }
   279  
   280  func isInternalPackage(pkgPath, modulePath string) bool {
   281  	pkgPath = strings.TrimPrefix(pkgPath, modulePath)
   282  	switch {
   283  	case strings.HasSuffix(pkgPath, "/internal"):
   284  		return true
   285  	case strings.Contains(pkgPath, "/internal/"):
   286  		return true
   287  	case pkgPath == "internal":
   288  		return true
   289  	case strings.HasPrefix(pkgPath, "internal/"):
   290  		return true
   291  	}
   292  	return false
   293  }