diff options
Diffstat (limited to 'src/cmd/api/goapi.go')
-rw-r--r-- | src/cmd/api/goapi.go | 225 |
1 files changed, 160 insertions, 65 deletions
diff --git a/src/cmd/api/goapi.go b/src/cmd/api/goapi.go index ad1c6bb8c..0d76b0cdb 100644 --- a/src/cmd/api/goapi.go +++ b/src/cmd/api/goapi.go @@ -22,12 +22,14 @@ import ( "go/parser" "go/printer" "go/token" + "io" "io/ioutil" "log" "os" "os/exec" "path" "path/filepath" + "regexp" "runtime" "sort" "strconv" @@ -38,11 +40,12 @@ import ( var ( // TODO(bradfitz): once Go 1.1 comes out, allow the -c flag to take a comma-separated // list of files, rather than just one. - checkFile = flag.String("c", "", "optional filename to check API against") - allowNew = flag.Bool("allow_new", true, "allow API additions") - nextFile = flag.String("next", "", "optional filename of tentative upcoming API features for the next release. This file can be lazily maintained. It only affects the delta warnings from the -c file printed on success.") - verbose = flag.Bool("v", false, "verbose debugging") - forceCtx = flag.String("contexts", "", "optional comma-separated list of <goos>-<goarch>[-cgo] to override default contexts.") + checkFile = flag.String("c", "", "optional filename to check API against") + allowNew = flag.Bool("allow_new", true, "allow API additions") + exceptFile = flag.String("except", "", "optional filename of packages that are allowed to change without triggering a failure in the tool") + nextFile = flag.String("next", "", "optional filename of tentative upcoming API features for the next release. This file can be lazily maintained. It only affects the delta warnings from the -c file printed on success.") + verbose = flag.Bool("v", false, "verbose debugging") + forceCtx = flag.String("contexts", "", "optional comma-separated list of <goos>-<goarch>[-cgo] to override default contexts.") ) // contexts are the default contexts which are scanned, unless @@ -100,7 +103,7 @@ func setContexts() { func main() { flag.Parse() - if !strings.Contains(runtime.Version(), "weekly") { + if !strings.Contains(runtime.Version(), "weekly") && !strings.Contains(runtime.Version(), "devel") { if *nextFile != "" { fmt.Printf("Go version is %q, ignoring -next %s\n", runtime.Version(), *nextFile) *nextFile = "" @@ -166,7 +169,6 @@ func main() { features = append(features, f2) } } - sort.Strings(features) fail := false defer func() { @@ -179,24 +181,45 @@ func main() { defer bw.Flush() if *checkFile == "" { + sort.Strings(features) for _, f := range features { fmt.Fprintf(bw, "%s\n", f) } return } - var required []string - for _, filename := range []string{*checkFile} { - required = append(required, fileFeatures(filename)...) + required := fileFeatures(*checkFile) + optional := fileFeatures(*nextFile) + exception := fileFeatures(*exceptFile) + fail = !compareAPI(bw, features, required, optional, exception) +} + +func set(items []string) map[string]bool { + s := make(map[string]bool) + for _, v := range items { + s[v] = true } - sort.Strings(required) + return s +} - var optional = make(map[string]bool) // feature => true - if *nextFile != "" { - for _, feature := range fileFeatures(*nextFile) { - optional[feature] = true - } +var spaceParensRx = regexp.MustCompile(` \(\S+?\)`) + +func featureWithoutContext(f string) string { + if !strings.Contains(f, "(") { + return f } + return spaceParensRx.ReplaceAllString(f, "") +} + +func compareAPI(w io.Writer, features, required, optional, exception []string) (ok bool) { + ok = true + + optionalSet := set(optional) + exceptionSet := set(exception) + featureSet := set(features) + + sort.Strings(features) + sort.Strings(required) take := func(sl *[]string) string { s := (*sl)[0] @@ -206,20 +229,27 @@ func main() { for len(required) > 0 || len(features) > 0 { switch { - case len(features) == 0 || required[0] < features[0]: - fmt.Fprintf(bw, "-%s\n", take(&required)) - fail = true // broke compatibility - case len(required) == 0 || required[0] > features[0]: + case len(features) == 0 || (len(required) > 0 && required[0] < features[0]): + feature := take(&required) + if exceptionSet[feature] { + fmt.Fprintf(w, "~%s\n", feature) + } else if featureSet[featureWithoutContext(feature)] { + // okay. + } else { + fmt.Fprintf(w, "-%s\n", feature) + ok = false // broke compatibility + } + case len(required) == 0 || (len(features) > 0 && required[0] > features[0]): newFeature := take(&features) - if optional[newFeature] { + if optionalSet[newFeature] { // Known added feature to the upcoming release. // Delete it from the map so we can detect any upcoming features // which were never seen. (so we can clean up the nextFile) - delete(optional, newFeature) + delete(optionalSet, newFeature) } else { - fmt.Fprintf(bw, "+%s\n", newFeature) + fmt.Fprintf(w, "+%s\n", newFeature) if !*allowNew { - fail = true // we're in lock-down mode for next release + ok = false // we're in lock-down mode for next release } } default: @@ -228,17 +258,22 @@ func main() { } } + // In next file, but not in API. var missing []string - for feature := range optional { + for feature := range optionalSet { missing = append(missing, feature) } sort.Strings(missing) for _, feature := range missing { - fmt.Fprintf(bw, "(in next file, but not in API) -%s\n", feature) + fmt.Fprintf(w, "±%s\n", feature) } + return } func fileFeatures(filename string) []string { + if filename == "" { + return nil + } bs, err := ioutil.ReadFile(filename) if err != nil { log.Fatalf("Error reading file %s: %v", filename, err) @@ -256,10 +291,11 @@ type pkgSymbol struct { symbol string // "RoundTripper" } +var fset = token.NewFileSet() + type Walker struct { context *build.Context root string - fset *token.FileSet scope []string features map[string]bool // set lastConstType string @@ -276,7 +312,6 @@ type Walker struct { func NewWalker() *Walker { return &Walker{ - fset: token.NewFileSet(), features: make(map[string]bool), packageState: make(map[string]loadState), interfaces: make(map[pkgSymbol]*ast.InterfaceType), @@ -297,20 +332,6 @@ const ( loaded ) -// hardCodedConstantType is a hack until the type checker is sufficient for our needs. -// Rather than litter the code with unnecessary type annotations, we'll hard-code -// the cases we can't handle yet. -func (w *Walker) hardCodedConstantType(name string) (typ string, ok bool) { - switch w.scope[0] { - case "pkg syscall": - switch name { - case "darwinAMD64": - return "bool", true - } - } - return "", false -} - func (w *Walker) Features() (fs []string) { for f := range w.features { fs = append(fs, f) @@ -333,6 +354,21 @@ func fileDeps(f *ast.File) (pkgs []string) { return } +var parsedFileCache = make(map[string]*ast.File) + +func parseFile(filename string) (*ast.File, error) { + f, ok := parsedFileCache[filename] + if !ok { + var err error + f, err = parser.ParseFile(fset, filename, nil, 0) + if err != nil { + return nil, err + } + parsedFileCache[filename] = f + } + return clone(f).(*ast.File), nil +} + // WalkPackage walks all files in package `name'. // WalkPackage does nothing if the package has already been loaded. func (w *Walker) WalkPackage(name string) { @@ -366,7 +402,7 @@ func (w *Walker) WalkPackage(name string) { files := append(append([]string{}, info.GoFiles...), info.CgoFiles...) for _, file := range files { - f, err := parser.ParseFile(w.fset, filepath.Join(dir, file), nil, 0) + f, err := parseFile(filepath.Join(dir, file)) if err != nil { log.Fatalf("error parsing package %s, file %s: %v", name, file, err) } @@ -501,7 +537,7 @@ func (w *Walker) walkFile(file *ast.File) { // Ignore. Handled in subsequent pass, by go/doc. default: log.Printf("unhandled %T, %#v\n", di, di) - printer.Fprint(os.Stderr, w.fset, di) + printer.Fprint(os.Stderr, fset, di) os.Stderr.Write([]byte("\n")) } } @@ -562,6 +598,10 @@ func (w *Walker) constValueType(vi interface{}) (string, error) { } return constDepPrefix + v.Name, nil case *ast.BinaryExpr: + switch v.Op { + case token.EQL, token.LSS, token.GTR, token.NOT, token.NEQ, token.LEQ, token.GEQ: + return "bool", nil + } left, err := w.constValueType(v.X) if err != nil { return "", err @@ -734,17 +774,11 @@ func (w *Walker) walkConst(vs *ast.ValueSpec) { var err error litType, err = w.constValueType(vs.Values[0]) if err != nil { - if t, ok := w.hardCodedConstantType(ident.Name); ok { - litType = t - err = nil - } else { - log.Fatalf("unknown kind in const %q (%T): %v", ident.Name, vs.Values[0], err) - } + log.Fatalf("unknown kind in const %q (%T): %v", ident.Name, vs.Values[0], err) } } } - if strings.HasPrefix(litType, constDepPrefix) { - dep := litType[len(constDepPrefix):] + if dep := strings.TrimPrefix(litType, constDepPrefix); dep != litType { w.constDep[ident.Name] = dep continue } @@ -816,7 +850,7 @@ func (w *Walker) nodeString(node interface{}) string { return "" } var b bytes.Buffer - printer.Fprint(&b, w.fset, node) + printer.Fprint(&b, fset, node) return b.String() } @@ -825,7 +859,7 @@ func (w *Walker) nodeDebug(node interface{}) string { return "" } var b bytes.Buffer - ast.Fprint(&b, w.fset, node, nil) + ast.Fprint(&b, fset, node, nil) return b.String() } @@ -844,7 +878,7 @@ func (w *Walker) walkTypeSpec(ts *ast.TypeSpec) { case *ast.InterfaceType: w.walkInterfaceType(name, t) default: - w.emitFeature(fmt.Sprintf("type %s %s", name, w.nodeString(ts.Type))) + w.emitFeature(fmt.Sprintf("type %s %s", name, w.nodeString(w.namelessType(ts.Type)))) } } @@ -892,15 +926,18 @@ type method struct { sig string // "([]byte) (int, error)", from funcSigString } -// interfaceMethods returns the expanded list of methods for an interface. +// interfaceMethods returns the expanded list of exported methods for an interface. +// The boolean complete reports whether the list contains all methods (that is, the +// interface has no unexported methods). // pkg is the complete package name ("net/http") // iname is the interface name. -func (w *Walker) interfaceMethods(pkg, iname string) (methods []method) { +func (w *Walker) interfaceMethods(pkg, iname string) (methods []method, complete bool) { t, ok := w.interfaces[pkgSymbol{pkg, iname}] if !ok { log.Fatalf("failed to find interface %s.%s", pkg, iname) } + complete = true for _, f := range t.Methods.List { typ := f.Type switch tv := typ.(type) { @@ -912,6 +949,8 @@ func (w *Walker) interfaceMethods(pkg, iname string) (methods []method) { name: mname.Name, sig: w.funcSigString(ft), }) + } else { + complete = false } } case *ast.Ident: @@ -927,7 +966,9 @@ func (w *Walker) interfaceMethods(pkg, iname string) (methods []method) { log.Fatalf("unexported embedded interface %q in exported interface %s.%s; confused", embedded, pkg, iname) } - methods = append(methods, w.interfaceMethods(pkg, embedded)...) + m, c := w.interfaceMethods(pkg, embedded) + methods = append(methods, m...) + complete = complete && c case *ast.SelectorExpr: lhs := w.nodeString(tv.X) rhs := w.nodeString(tv.Sel) @@ -935,7 +976,9 @@ func (w *Walker) interfaceMethods(pkg, iname string) (methods []method) { if !ok { log.Fatalf("can't resolve selector %q in interface %s.%s", lhs, pkg, iname) } - methods = append(methods, w.interfaceMethods(fpkg, rhs)...) + m, c := w.interfaceMethods(fpkg, rhs) + methods = append(methods, m...) + complete = complete && c default: log.Fatalf("unknown type %T in interface field", typ) } @@ -945,14 +988,28 @@ func (w *Walker) interfaceMethods(pkg, iname string) (methods []method) { func (w *Walker) walkInterfaceType(name string, t *ast.InterfaceType) { methNames := []string{} - pop := w.pushScope("type " + name + " interface") - for _, m := range w.interfaceMethods(w.curPackageName, name) { + methods, complete := w.interfaceMethods(w.curPackageName, name) + for _, m := range methods { methNames = append(methNames, m.name) w.emitFeature(fmt.Sprintf("%s%s", m.name, m.sig)) } + if !complete { + // The method set has unexported methods, so all the + // implementations are provided by the same package, + // so the method set can be extended. Instead of recording + // the full set of names (below), record only that there were + // unexported methods. (If the interface shrinks, we will notice + // because a method signature emitted during the last loop, + // will disappear.) + w.emitFeature("unexported methods") + } pop() + if !complete { + return + } + sort.Strings(methNames) if len(methNames) == 0 { w.emitFeature(fmt.Sprintf("type %s interface {}", name)) @@ -994,18 +1051,38 @@ func (w *Walker) walkFuncDecl(f *ast.FuncDecl) { func (w *Walker) funcSigString(ft *ast.FuncType) string { var b bytes.Buffer + writeField := func(b *bytes.Buffer, f *ast.Field) { + if n := len(f.Names); n > 1 { + for i := 0; i < n; i++ { + if i > 0 { + b.WriteString(", ") + } + b.WriteString(w.nodeString(w.namelessType(f.Type))) + } + } else { + b.WriteString(w.nodeString(w.namelessType(f.Type))) + } + } b.WriteByte('(') if ft.Params != nil { for i, f := range ft.Params.List { if i > 0 { b.WriteString(", ") } - b.WriteString(w.nodeString(w.namelessType(f.Type))) + writeField(&b, f) } } b.WriteByte(')') if ft.Results != nil { - if nr := len(ft.Results.List); nr > 0 { + nr := 0 + for _, f := range ft.Results.List { + if n := len(f.Names); n > 1 { + nr += n + } else { + nr++ + } + } + if nr > 0 { b.WriteByte(' ') if nr > 1 { b.WriteByte('(') @@ -1014,7 +1091,7 @@ func (w *Walker) funcSigString(ft *ast.FuncType) string { if i > 0 { b.WriteString(", ") } - b.WriteString(w.nodeString(w.namelessType(f.Type))) + writeField(&b, f) } if nr > 1 { b.WriteByte(')') @@ -1042,7 +1119,13 @@ func (w *Walker) namelessFieldList(fl *ast.FieldList) *ast.FieldList { fl2 := &ast.FieldList{} if fl != nil { for _, f := range fl.List { - fl2.List = append(fl2.List, w.namelessField(f)) + repeats := 1 + if len(f.Names) > 1 { + repeats = len(f.Names) + } + for i := 0; i < repeats; i++ { + fl2.List = append(fl2.List, w.namelessField(f)) + } } } return fl2 @@ -1056,10 +1139,21 @@ func (w *Walker) namelessField(f *ast.Field) *ast.Field { } } +var ( + byteRx = regexp.MustCompile(`\bbyte\b`) + runeRx = regexp.MustCompile(`\brune\b`) +) + func (w *Walker) emitFeature(feature string) { if !w.wantedPkg[w.curPackageName] { return } + if strings.Contains(feature, "byte") { + feature = byteRx.ReplaceAllString(feature, "uint8") + } + if strings.Contains(feature, "rune") { + feature = runeRx.ReplaceAllString(feature, "int32") + } f := strings.Join(w.scope, ", ") + ", " + feature if _, dup := w.features[f]; dup { panic("duplicate feature inserted: " + f) @@ -1075,6 +1169,7 @@ func (w *Walker) emitFeature(feature string) { } panic("feature contains newlines: " + f) } + w.features[f] = true if *verbose { log.Printf("feature: %s", f) |