apicheck.go raw

   1  package compiler
   2  
   3  // API stability enforcement for Moxie's export model.
   4  //
   5  // All identifiers are accessible cross-package. Capital/# prefix marks
   6  // a symbol as part of the stable API contract. Removing or changing the
   7  // signature of a stable symbol without bumping the major version in
   8  // moxie.mod is a compile error when building against a cached .mxh.
   9  
  10  import (
  11  	"go/token"
  12  	"go/types"
  13  	"os"
  14  	"strconv"
  15  	"strings"
  16  
  17  	"moxie/loader"
  18  )
  19  
  20  // checkAPIStability compares the current package's stable symbols against
  21  // a previously cached .mxh. Called from CompilePackage after SSA build.
  22  func (c *compilerContext) checkAPIStability(pkg *loader.Package) {
  23  	if !isUserPackageByPath(pkg.Pkg.Path()) {
  24  		return
  25  	}
  26  	prevHash := loader.MXHHashForImport(pkg.Pkg.Path())
  27  	if prevHash == "" {
  28  		return // no cached .mxh, nothing to compare
  29  	}
  30  
  31  	prevPkg, err := loader.SynthMXHPackageForCheck(pkg.Pkg.Path())
  32  	if err != nil {
  33  		return // can't load previous .mxh, skip check
  34  	}
  35  
  36  	// Collect stable symbols from current package.
  37  	currentStable := collectStableSymbols(pkg.Pkg)
  38  	// Collect stable symbols from cached .mxh package.
  39  	prevStable := collectStableSymbols(prevPkg)
  40  
  41  	// Find removals and signature changes.
  42  	var breaks []string
  43  	for name, prevObj := range prevStable {
  44  		curObj, exists := currentStable[name]
  45  		if !exists {
  46  			breaks = append(breaks, "removed: "+name)
  47  			continue
  48  		}
  49  		// Check signature compatibility for functions.
  50  		prevFn, prevIsFn := prevObj.(*types.Func)
  51  		curFn, curIsFn := curObj.(*types.Func)
  52  		if prevIsFn && curIsFn {
  53  			prevSig := prevFn.Type().(*types.Signature)
  54  			curSig := curFn.Type().(*types.Signature)
  55  			if !signaturesCompatible(prevSig, curSig) {
  56  				breaks = append(breaks, "signature changed: "+name)
  57  			}
  58  		}
  59  	}
  60  
  61  	if len(breaks) == 0 {
  62  		return
  63  	}
  64  
  65  	// Check if major version was bumped.
  66  	prevVersion := loader.MXHVersionForImport(pkg.Pkg.Path())
  67  	wd, _ := os.Getwd()
  68  	curVersion := loader.ParseMoxieModVersion(wd)
  69  	if majorVersionBumped(prevVersion, curVersion) {
  70  		return // major version bumped, API breaks are allowed
  71  	}
  72  
  73  	for _, b := range breaks {
  74  		c.addError(token.NoPos, "moxie: API stability violation: "+b+" (bump major version in moxie.mod)")
  75  	}
  76  }
  77  
  78  // collectStableSymbols returns a map of all stable (exported) symbols
  79  // in a package's scope. Stable means Capital letter or # prefix.
  80  func collectStableSymbols(pkg *types.Package) map[string]types.Object {
  81  	result := map[string]types.Object{}
  82  	scope := pkg.Scope()
  83  	for _, name := range scope.Names() {
  84  		obj := scope.Lookup(name)
  85  		if obj == nil {
  86  			continue
  87  		}
  88  		if token.IsExported(name) || strings.HasPrefix(name, "#") {
  89  			result[name] = obj
  90  		}
  91  	}
  92  	return result
  93  }
  94  
  95  // majorVersionBumped returns true if curVersion has a higher major version
  96  // than prevVersion. Versions are semver format "major.minor.patch".
  97  // Returns false if either version is empty or unparseable.
  98  func majorVersionBumped(prev, cur string) bool {
  99  	if prev == "" || cur == "" {
 100  		return false
 101  	}
 102  	prevMajor := parseMajorVersion(prev)
 103  	curMajor := parseMajorVersion(cur)
 104  	return curMajor > prevMajor
 105  }
 106  
 107  func parseMajorVersion(v string) int {
 108  	parts := strings.SplitN(v, ".", 2)
 109  	n, err := strconv.Atoi(parts[0])
 110  	if err != nil {
 111  		return -1
 112  	}
 113  	return n
 114  }
 115  
 116  // signaturesCompatible returns true if two function signatures are
 117  // structurally identical (same parameter and return types).
 118  func signaturesCompatible(a, b *types.Signature) bool {
 119  	if a.Params().Len() != b.Params().Len() {
 120  		return false
 121  	}
 122  	if a.Results().Len() != b.Results().Len() {
 123  		return false
 124  	}
 125  	for i := 0; i < a.Params().Len(); i++ {
 126  		if !types.Identical(a.Params().At(i).Type(), b.Params().At(i).Type()) {
 127  			return false
 128  		}
 129  	}
 130  	for i := 0; i < a.Results().Len(); i++ {
 131  		if !types.Identical(a.Results().At(i).Type(), b.Results().At(i).Type()) {
 132  			return false
 133  		}
 134  	}
 135  	return true
 136  }
 137