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