]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/goapi: new tool for tracking exported API over time
authorBrad Fitzpatrick <bradfitz@golang.org>
Thu, 26 Jan 2012 01:47:57 +0000 (17:47 -0800)
committerBrad Fitzpatrick <bradfitz@golang.org>
Thu, 26 Jan 2012 01:47:57 +0000 (17:47 -0800)
The idea is that we add files to the api/ directory which
are sets of promises for the future.  Each line in a file
is a stand-alone feature description.

When we do a release, we make sure we haven't broken or changed
any lines from the past (only added them).

We never change old files, only adding new ones. (go-1.1.txt,
etc)

R=dsymonds, adg, r, remyoudompheng, rsc
CC=golang-dev
https://golang.org/cl/5570051

src/cmd/goapi/goapi.go [new file with mode: 0644]
src/cmd/goapi/goapi_test.go [new file with mode: 0644]
src/cmd/goapi/testdata/p1/golden.txt [new file with mode: 0644]
src/cmd/goapi/testdata/p1/p1.go [new file with mode: 0644]

diff --git a/src/cmd/goapi/goapi.go b/src/cmd/goapi/goapi.go
new file mode 100644 (file)
index 0000000..a64edca
--- /dev/null
@@ -0,0 +1,722 @@
+// Copyright 2011 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Goapi computes the exported API of a set of Go packages.
+package main
+
+import (
+       "bufio"
+       "bytes"
+       "errors"
+       "flag"
+       "fmt"
+       "go/ast"
+       "go/build"
+       "go/doc"
+       "go/parser"
+       "go/printer"
+       "go/token"
+       "io/ioutil"
+       "log"
+       "os"
+       "os/exec"
+       "path/filepath"
+       "sort"
+       "strings"
+)
+
+// Flags
+var (
+       checkFile = flag.String("c", "", "optional filename to check API against")
+       verbose   = flag.Bool("v", false, "Verbose debugging")
+)
+
+func main() {
+       flag.Parse()
+
+       var pkgs []string
+       if flag.NArg() > 0 {
+               pkgs = flag.Args()
+       } else {
+               stds, err := exec.Command("go", "list", "std").Output()
+               if err != nil {
+                       log.Fatal(err)
+               }
+               pkgs = strings.Fields(string(stds))
+       }
+
+       w := NewWalker()
+       tree, _, err := build.FindTree("os") // some known package
+       if err != nil {
+               log.Fatalf("failed to find tree: %v", err)
+       }
+
+       for _, pkg := range pkgs {
+               if strings.HasPrefix(pkg, "cmd/") ||
+                       strings.HasPrefix(pkg, "exp/") ||
+                       strings.HasPrefix(pkg, "old/") {
+                       continue
+               }
+               if !tree.HasSrc(pkg) {
+                       log.Fatalf("no source in tree for package %q", pkg)
+               }
+               pkgSrcDir := filepath.Join(tree.SrcDir(), filepath.FromSlash(pkg))
+               w.WalkPackage(pkg, pkgSrcDir)
+       }
+
+       bw := bufio.NewWriter(os.Stdout)
+       defer bw.Flush()
+
+       if *checkFile != "" {
+               bs, err := ioutil.ReadFile(*checkFile)
+               if err != nil {
+                       log.Fatalf("Error reading file %s: %v", *checkFile, err)
+               }
+               v1 := strings.Split(string(bs), "\n")
+               sort.Strings(v1)
+               v2 := w.Features()
+               take := func(sl *[]string) string {
+                       s := (*sl)[0]
+                       *sl = (*sl)[1:]
+                       return s
+               }
+               for len(v1) > 0 || len(v2) > 0 {
+                       switch {
+                       case len(v2) == 0 || v1[0] < v2[0]:
+                               fmt.Fprintf(bw, "-%s\n", take(&v1))
+                       case len(v1) == 0 || v1[0] > v2[0]:
+                               fmt.Fprintf(bw, "+%s\n", take(&v2))
+                       default:
+                               take(&v1)
+                               take(&v2)
+                       }
+               }
+       } else {
+               for _, f := range w.Features() {
+                       fmt.Fprintf(bw, "%s\n", f)
+               }
+       }
+}
+
+type Walker struct {
+       fset           *token.FileSet
+       scope          []string
+       features       map[string]bool // set
+       lastConstType  string
+       curPackageName string
+       curPackage     *ast.Package
+       prevConstType  map[string]string // identifer -> "ideal-int"
+}
+
+func NewWalker() *Walker {
+       return &Walker{
+               fset:     token.NewFileSet(),
+               features: make(map[string]bool),
+       }
+}
+
+// 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 compress/gzip", "pkg compress/zlib":
+               switch name {
+               case "NoCompression", "BestSpeed", "BestCompression", "DefaultCompression":
+                       return "ideal-int", true
+               }
+       case "pkg os":
+               switch name {
+               case "WNOHANG", "WSTOPPED", "WUNTRACED":
+                       return "ideal-int", true
+               }
+       case "pkg path/filepath":
+               switch name {
+               case "Separator", "ListSeparator":
+                       return "char", true
+               }
+       case "pkg unicode/utf8":
+               switch name {
+               case "RuneError":
+                       return "char", true
+               }
+       case "pkg text/scanner":
+               // TODO: currently this tool only resolves const types
+               // that reference other constant types if they appear
+               // in the right order.  the scanner package has
+               // ScanIdents and such coming before the Ident/Int/etc
+               // tokens, hence this hack.
+               if strings.HasPrefix(name, "Scan") || name == "SkipComments" {
+                       return "ideal-int", true
+               }
+       }
+       return "", false
+}
+
+func (w *Walker) Features() (fs []string) {
+       for f := range w.features {
+               fs = append(fs, f)
+       }
+       sort.Strings(fs)
+       return
+}
+
+func (w *Walker) WalkPackage(name, dir string) {
+       log.Printf("package %s", name)
+       pop := w.pushScope("pkg " + name)
+       defer pop()
+
+       info, err := build.ScanDir(dir)
+       if err != nil {
+               log.Fatalf("pkg %q, dir %q: ScanDir: %v", name, dir, err)
+       }
+
+       apkg := &ast.Package{
+               Files: make(map[string]*ast.File),
+       }
+
+       files := append(append([]string{}, info.GoFiles...), info.CgoFiles...)
+       for _, file := range files {
+               f, err := parser.ParseFile(w.fset, filepath.Join(dir, file), nil, 0)
+               if err != nil {
+                       log.Fatalf("error parsing package %s, file %s: %v", name, file, err)
+               }
+               apkg.Files[file] = f
+       }
+
+       w.curPackageName = name
+       w.curPackage = apkg
+       w.prevConstType = map[string]string{}
+       for name, afile := range apkg.Files {
+               w.walkFile(filepath.Join(dir, name), afile)
+       }
+
+       // Now that we're done walking types, vars and consts
+       // in the *ast.Package, use go/doc to do the rest
+       // (functions and methods). This is done here because
+       // go/doc is destructive.  We can't use the
+       // *ast.Package after this.
+       dpkg := doc.New(apkg, name, 0)
+
+       for _, t := range dpkg.Types {
+               // Move funcs up to the top-level, not hiding in the Types.
+               dpkg.Funcs = append(dpkg.Funcs, t.Funcs...)
+
+               for _, m := range t.Methods {
+                       w.walkFuncDecl(m.Decl)
+               }
+       }
+
+       for _, f := range dpkg.Funcs {
+               w.walkFuncDecl(f.Decl)
+       }
+}
+
+// pushScope enters a new scope (walking a package, type, node, etc)
+// and returns a function that will leave the scope (with sanity checking
+// for mismatched pushes & pops)
+func (w *Walker) pushScope(name string) (popFunc func()) {
+       w.scope = append(w.scope, name)
+       return func() {
+               if len(w.scope) == 0 {
+                       log.Fatalf("attempt to leave scope %q with empty scope list", name)
+               }
+               if w.scope[len(w.scope)-1] != name {
+                       log.Fatalf("attempt to leave scope %q, but scope is currently %#v", name, w.scope)
+               }
+               w.scope = w.scope[:len(w.scope)-1]
+       }
+}
+
+func (w *Walker) walkFile(name string, file *ast.File) {
+       // Not entering a scope here; file boundaries aren't interesting.
+
+       for _, di := range file.Decls {
+               switch d := di.(type) {
+               case *ast.GenDecl:
+                       switch d.Tok {
+                       case token.IMPORT:
+                               continue
+                       case token.CONST:
+                               for _, sp := range d.Specs {
+                                       w.walkConst(sp.(*ast.ValueSpec))
+                               }
+                       case token.TYPE:
+                               for _, sp := range d.Specs {
+                                       w.walkTypeSpec(sp.(*ast.TypeSpec))
+                               }
+                       case token.VAR:
+                               for _, sp := range d.Specs {
+                                       w.walkVar(sp.(*ast.ValueSpec))
+                               }
+                       default:
+                               log.Fatalf("unknown token type %d in GenDecl", d.Tok)
+                       }
+               case *ast.FuncDecl:
+                       // Ignore. Handled in subsequent pass, by go/doc.
+               default:
+                       log.Printf("unhandled %T, %#v\n", di, di)
+                       printer.Fprint(os.Stderr, w.fset, di)
+                       os.Stderr.Write([]byte("\n"))
+               }
+       }
+}
+
+var constType = map[token.Token]string{
+       token.INT:    "ideal-int",
+       token.FLOAT:  "ideal-float",
+       token.STRING: "ideal-string",
+       token.CHAR:   "ideal-char",
+       token.IMAG:   "ideal-imag",
+}
+
+var varType = map[token.Token]string{
+       token.INT:    "int",
+       token.FLOAT:  "float64",
+       token.STRING: "string",
+       token.CHAR:   "rune",
+       token.IMAG:   "complex128",
+}
+
+var errTODO = errors.New("TODO")
+
+func (w *Walker) constValueType(vi interface{}) (string, error) {
+       switch v := vi.(type) {
+       case *ast.BasicLit:
+               litType, ok := constType[v.Kind]
+               if !ok {
+                       return "", fmt.Errorf("unknown basic literal kind %#v", v)
+               }
+               return litType, nil
+       case *ast.UnaryExpr:
+               return w.constValueType(v.X)
+       case *ast.SelectorExpr:
+               // e.g. compress/gzip's BestSpeed == flate.BestSpeed
+               return "", errTODO
+       case *ast.Ident:
+               if v.Name == "iota" {
+                       return "ideal-int", nil // hack.
+               }
+               if v.Name == "false" || v.Name == "true" {
+                       return "ideal-bool", nil
+               }
+               if v.Name == "intSize" && w.curPackageName == "strconv" {
+                       // Hack.
+                       return "ideal-int", nil
+               }
+               if t, ok := w.prevConstType[v.Name]; ok {
+                       return t, nil
+               }
+               return "", fmt.Errorf("can't resolve existing constant %q", v.Name)
+       case *ast.BinaryExpr:
+               left, err := w.constValueType(v.X)
+               if err != nil {
+                       return "", err
+               }
+               right, err := w.constValueType(v.Y)
+               if err != nil {
+                       return "", err
+               }
+               if left != right {
+                       if left == "ideal-int" && right == "ideal-float" {
+                               return "ideal-float", nil // math.Log2E
+                       }
+                       if left == "ideal-char" && right == "ideal-int" {
+                               return "ideal-int", nil // math/big.MaxBase
+                       }
+                       if left == "ideal-int" && right == "ideal-char" {
+                               return "ideal-int", nil // text/scanner.GoWhitespace
+                       }
+                       if left == "ideal-int" && right == "Duration" {
+                               // Hack, for package time.
+                               return "Duration", nil
+                       }
+                       return "", fmt.Errorf("in BinaryExpr, unhandled type mismatch; left=%q, right=%q", left, right)
+               }
+               return left, nil
+       case *ast.CallExpr:
+               // Not a call, but a type conversion.
+               return w.nodeString(v.Fun), nil
+       case *ast.ParenExpr:
+               return w.constValueType(v.X)
+       }
+       return "", fmt.Errorf("unknown const value type %T", vi)
+}
+
+func (w *Walker) varValueType(vi interface{}) (string, error) {
+       valStr := w.nodeString(vi)
+       if strings.HasPrefix(valStr, "errors.New(") {
+               return "error", nil
+       }
+
+       switch v := vi.(type) {
+       case *ast.BasicLit:
+               litType, ok := varType[v.Kind]
+               if !ok {
+                       return "", fmt.Errorf("unknown basic literal kind %#v", v)
+               }
+               return litType, nil
+       case *ast.CompositeLit:
+               return w.nodeString(v.Type), nil
+       case *ast.FuncLit:
+               return w.nodeString(w.namelessType(v.Type)), nil
+       case *ast.UnaryExpr:
+               if v.Op == token.AND {
+                       typ, err := w.varValueType(v.X)
+                       return "*" + typ, err
+               }
+               return "", fmt.Errorf("unknown unary expr: %#v", v)
+       case *ast.SelectorExpr:
+               return "", errTODO
+       case *ast.Ident:
+               node, _, ok := w.resolveName(v.Name)
+               if !ok {
+                       return "", fmt.Errorf("unresolved identifier: %q", v.Name)
+               }
+               return w.varValueType(node)
+       case *ast.BinaryExpr:
+               left, err := w.varValueType(v.X)
+               if err != nil {
+                       return "", err
+               }
+               right, err := w.varValueType(v.Y)
+               if err != nil {
+                       return "", err
+               }
+               if left != right {
+                       return "", fmt.Errorf("in BinaryExpr, unhandled type mismatch; left=%q, right=%q", left, right)
+               }
+               return left, nil
+       case *ast.ParenExpr:
+               return w.varValueType(v.X)
+       case *ast.CallExpr:
+               funStr := w.nodeString(v.Fun)
+               node, _, ok := w.resolveName(funStr)
+               if !ok {
+                       return "", fmt.Errorf("unresolved named %q", funStr)
+               }
+               if funcd, ok := node.(*ast.FuncDecl); ok {
+                       // Assume at the top level that all functions have exactly 1 result
+                       return w.nodeString(w.namelessType(funcd.Type.Results.List[0].Type)), nil
+               }
+               // maybe a function call; maybe a conversion.  Need to lookup type.
+               return "", fmt.Errorf("resolved name %q to a %T: %#v", funStr, node, node)
+       default:
+               return "", fmt.Errorf("unknown const value type %T", vi)
+       }
+       panic("unreachable")
+}
+
+// resolveName finds a top-level node named name and returns the node
+// v and its type t, if known.
+func (w *Walker) resolveName(name string) (v interface{}, t interface{}, ok bool) {
+       for _, file := range w.curPackage.Files {
+               for _, di := range file.Decls {
+                       switch d := di.(type) {
+                       case *ast.FuncDecl:
+                               if d.Name.Name == name {
+                                       return d, d.Type, true
+                               }
+                       case *ast.GenDecl:
+                               switch d.Tok {
+                               case token.TYPE:
+                                       for _, sp := range d.Specs {
+                                               ts := sp.(*ast.TypeSpec)
+                                               if ts.Name.Name == name {
+                                                       return ts, ts.Type, true
+                                               }
+                                       }
+                               case token.VAR:
+                                       for _, sp := range d.Specs {
+                                               vs := sp.(*ast.ValueSpec)
+                                               for i, vname := range vs.Names {
+                                                       if vname.Name == name {
+                                                               if len(vs.Values) > i {
+                                                                       return vs.Values[i], vs.Type, true
+                                                               }
+                                                               return nil, vs.Type, true
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+       return nil, nil, false
+}
+
+func (w *Walker) walkConst(vs *ast.ValueSpec) {
+       for _, ident := range vs.Names {
+               if !ast.IsExported(ident.Name) {
+                       continue
+               }
+               litType := ""
+               if vs.Type != nil {
+                       litType = w.nodeString(vs.Type)
+               } else {
+                       litType = w.lastConstType
+                       if vs.Values != nil {
+                               if len(vs.Values) != 1 {
+                                       log.Fatalf("const %q, values: %#v", ident.Name, vs.Values)
+                               }
+                               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)
+                                       }
+                               }
+                       }
+               }
+               if litType == "" {
+                       log.Fatalf("unknown kind in const %q", ident.Name)
+               }
+               w.lastConstType = litType
+
+               w.emitFeature(fmt.Sprintf("const %s %s", ident, litType))
+               w.prevConstType[ident.Name] = litType
+       }
+}
+
+func (w *Walker) walkVar(vs *ast.ValueSpec) {
+       for i, ident := range vs.Names {
+               if !ast.IsExported(ident.Name) {
+                       continue
+               }
+
+               typ := ""
+               if vs.Type != nil {
+                       typ = w.nodeString(vs.Type)
+               } else {
+                       if len(vs.Values) == 0 {
+                               log.Fatalf("no values for var %q", ident.Name)
+                       }
+                       if len(vs.Values) > 1 {
+                               log.Fatalf("more than 1 values in ValueSpec not handled, var %q", ident.Name)
+                       }
+                       var err error
+                       typ, err = w.varValueType(vs.Values[i])
+                       if err != nil {
+                               log.Fatalf("unknown type of variable %q, type %T, error = %v\ncode: %s",
+                                       ident.Name, vs.Values[i], err, w.nodeString(vs.Values[i]))
+                       }
+               }
+               w.emitFeature(fmt.Sprintf("var %s %s", ident, typ))
+       }
+}
+
+func (w *Walker) nodeString(node interface{}) string {
+       if node == nil {
+               return ""
+       }
+       var b bytes.Buffer
+       printer.Fprint(&b, w.fset, node)
+       return b.String()
+}
+
+func (w *Walker) nodeDebug(node interface{}) string {
+       if node == nil {
+               return ""
+       }
+       var b bytes.Buffer
+       ast.Fprint(&b, w.fset, node, nil)
+       return b.String()
+}
+
+func (w *Walker) walkTypeSpec(ts *ast.TypeSpec) {
+       name := ts.Name.Name
+       if !ast.IsExported(name) {
+               return
+       }
+
+       switch t := ts.Type.(type) {
+       case *ast.StructType:
+               w.walkStructType(name, t)
+       case *ast.InterfaceType:
+               w.walkInterfaceType(name, t)
+       default:
+               w.emitFeature(fmt.Sprintf("type %s %s", name, w.nodeString(ts.Type)))
+               //log.Fatalf("unknown typespec %T", ts.Type)
+       }
+}
+
+func (w *Walker) walkStructType(name string, t *ast.StructType) {
+       typeStruct := fmt.Sprintf("type %s struct", name)
+       w.emitFeature(typeStruct)
+       pop := w.pushScope(typeStruct)
+       defer pop()
+       for _, f := range t.Fields.List {
+               typ := f.Type
+               for _, name := range f.Names {
+                       if ast.IsExported(name.Name) {
+                               w.emitFeature(fmt.Sprintf("%s %s", name, w.nodeString(w.namelessType(typ))))
+                       }
+               }
+               if f.Names == nil {
+                       switch v := typ.(type) {
+                       case *ast.Ident:
+                               if ast.IsExported(v.Name) {
+                                       w.emitFeature(fmt.Sprintf("embedded %s", v.Name))
+                               }
+                       case *ast.StarExpr:
+                               switch vv := v.X.(type) {
+                               case *ast.Ident:
+                                       if ast.IsExported(vv.Name) {
+                                               w.emitFeature(fmt.Sprintf("embedded *%s", vv.Name))
+                                       }
+                               case *ast.SelectorExpr:
+                                       w.emitFeature(fmt.Sprintf("embedded %s", w.nodeString(typ)))
+                               default:
+                                       log.Fatal("unable to handle embedded starexpr before %T", typ)
+                               }
+                       case *ast.SelectorExpr:
+                               w.emitFeature(fmt.Sprintf("embedded %s", w.nodeString(typ)))
+                       default:
+                               log.Fatalf("unable to handle embedded %T", typ)
+                       }
+               }
+       }
+}
+
+func (w *Walker) walkInterfaceType(name string, t *ast.InterfaceType) {
+       methods := []string{}
+
+       pop := w.pushScope("type " + name + " interface")
+       for _, f := range t.Methods.List {
+               typ := f.Type
+               for _, name := range f.Names {
+                       if ast.IsExported(name.Name) {
+                               ft := typ.(*ast.FuncType)
+                               w.emitFeature(fmt.Sprintf("%s%s", name, w.funcSigString(ft)))
+                               methods = append(methods, name.Name)
+                       }
+               }
+       }
+       pop()
+
+       sort.Strings(methods)
+       if len(methods) == 0 {
+               w.emitFeature(fmt.Sprintf("type %s interface {}", name))
+       } else {
+               w.emitFeature(fmt.Sprintf("type %s interface { %s }", name, strings.Join(methods, ", ")))
+       }
+}
+
+func (w *Walker) walkFuncDecl(f *ast.FuncDecl) {
+       if !ast.IsExported(f.Name.Name) {
+               return
+       }
+       if f.Recv != nil {
+               // Method.
+               recvType := w.nodeString(f.Recv.List[0].Type)
+               keep := ast.IsExported(recvType) ||
+                       (strings.HasPrefix(recvType, "*") &&
+                               ast.IsExported(recvType[1:]))
+               if !keep {
+                       return
+               }
+               w.emitFeature(fmt.Sprintf("method (%s) %s%s", recvType, f.Name.Name, w.funcSigString(f.Type)))
+               return
+       }
+       // Else, a function
+       w.emitFeature(fmt.Sprintf("func %s%s", f.Name.Name, w.funcSigString(f.Type)))
+}
+
+func (w *Walker) funcSigString(ft *ast.FuncType) string {
+       var b bytes.Buffer
+       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)))
+               }
+       }
+       b.WriteByte(')')
+       if ft.Results != nil {
+               if nr := len(ft.Results.List); nr > 0 {
+                       b.WriteByte(' ')
+                       if nr > 1 {
+                               b.WriteByte('(')
+                       }
+                       for i, f := range ft.Results.List {
+                               if i > 0 {
+                                       b.WriteString(", ")
+                               }
+                               b.WriteString(w.nodeString(w.namelessType(f.Type)))
+                       }
+                       if nr > 1 {
+                               b.WriteByte(')')
+                       }
+               }
+       }
+       return b.String()
+}
+
+// namelessType returns a type node that lacks any variable names.
+func (w *Walker) namelessType(t interface{}) interface{} {
+       ft, ok := t.(*ast.FuncType)
+       if !ok {
+               return t
+       }
+       return &ast.FuncType{
+               Params:  w.namelessFieldList(ft.Params),
+               Results: w.namelessFieldList(ft.Results),
+       }
+}
+
+// namelessFieldList returns a deep clone of fl, with the cloned fields
+// lacking names.
+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))
+               }
+       }
+       return fl2
+}
+
+// namelessField clones f, but not preserving the names of fields.
+// (comments and tags are also ignored)
+func (w *Walker) namelessField(f *ast.Field) *ast.Field {
+       return &ast.Field{
+               Type: f.Type,
+       }
+}
+
+func (w *Walker) emitFeature(feature string) {
+       f := strings.Join(w.scope, ", ") + ", " + feature
+       if _, dup := w.features[f]; dup {
+               panic("duplicate feature inserted: " + f)
+       }
+
+       if strings.Contains(f, "\n") {
+               // TODO: for now, just skip over the
+               // runtime.MemStatsType.BySize type, which this tool
+               // doesn't properly handle. It's pretty low-level,
+               // though, so not super important to protect against.
+               if strings.HasPrefix(f, "pkg runtime") && strings.Contains(f, "BySize [61]struct") {
+                       return
+               }
+               panic("feature contains newlines: " + f)
+       }
+       w.features[f] = true
+       if *verbose {
+               log.Printf("feature: %s", f)
+       }
+}
+
+func strListContains(l []string, s string) bool {
+       for _, v := range l {
+               if v == s {
+                       return true
+               }
+       }
+       return false
+}
diff --git a/src/cmd/goapi/goapi_test.go b/src/cmd/goapi/goapi_test.go
new file mode 100644 (file)
index 0000000..1f23b1d
--- /dev/null
@@ -0,0 +1,73 @@
+// Copyright 2011 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+       "flag"
+       "fmt"
+       "io/ioutil"
+       "os"
+       "path/filepath"
+       "sort"
+       "strings"
+       "testing"
+)
+
+var (
+       updateGolden = flag.Bool("updategolden", false, "update golden files")
+)
+
+func TestGolden(t *testing.T) {
+       td, err := os.Open("testdata")
+       if err != nil {
+               t.Fatal(err)
+       }
+       fis, err := td.Readdir(0)
+       if err != nil {
+               t.Fatal(err)
+       }
+       for _, fi := range fis {
+               if !fi.IsDir() {
+                       continue
+               }
+               w := NewWalker()
+               goldenFile := filepath.Join("testdata", fi.Name(), "golden.txt")
+
+               w.WalkPackage(fi.Name(), filepath.Join("testdata", fi.Name()))
+
+               if *updateGolden {
+                       os.Remove(goldenFile)
+                       f, err := os.Create(goldenFile)
+                       if err != nil {
+                               t.Fatal(err)
+                       }
+                       for _, feat := range w.Features() {
+                               fmt.Fprintf(f, "%s\n", feat)
+                       }
+                       f.Close()
+               }
+
+               bs, err := ioutil.ReadFile(goldenFile)
+               if err != nil {
+                       t.Fatalf("opening golden.txt for package %q: %v", fi.Name(), err)
+               }
+               wanted := strings.Split(string(bs), "\n")
+               sort.Strings(wanted)
+               for _, feature := range wanted {
+                       if feature == "" {
+                               continue
+                       }
+                       _, ok := w.features[feature]
+                       if !ok {
+                               t.Errorf("package %s: missing feature %q", fi.Name(), feature)
+                       }
+                       delete(w.features, feature)
+               }
+
+               for _, feature := range w.Features() {
+                       t.Errorf("package %s: extra feature not in golden file: %q", fi.Name(), feature)
+               }
+       }
+}
diff --git a/src/cmd/goapi/testdata/p1/golden.txt b/src/cmd/goapi/testdata/p1/golden.txt
new file mode 100644 (file)
index 0000000..5b2aff5
--- /dev/null
@@ -0,0 +1,57 @@
+pkg p1, const A ideal-int
+pkg p1, const A64 int64
+pkg p1, const B ideal-int
+pkg p1, const ConversionConst MyInt
+pkg p1, const FloatConst ideal-float
+pkg p1, const StrConst ideal-string
+pkg p1, func Bar(int8, int16, int64)
+pkg p1, func Bar1(int8, int16, int64) uint64
+pkg p1, func Bar2(int8, int16, int64) (uint8, uint64)
+pkg p1, func TakesFunc(func(int) int)
+pkg p1, method (*B) JustOnB()
+pkg p1, method (*B) OnBothTandBPtr()
+pkg p1, method (*Embedded) OnEmbedded()
+pkg p1, method (*S2) SMethod(int8, int16, int64)
+pkg p1, method (*T) JustOnT()
+pkg p1, method (*T) OnBothTandBPtr()
+pkg p1, method (B) OnBothTandBVal()
+pkg p1, method (S) StructValueMethod()
+pkg p1, method (S) StructValueMethodNamedRecv()
+pkg p1, method (S2) StructValueMethod()
+pkg p1, method (S2) StructValueMethodNamedRecv()
+pkg p1, method (T) OnBothTandBVal()
+pkg p1, method (TPtrExported) OnEmbedded()
+pkg p1, method (TPtrUnexported) OnBothTandBPtr()
+pkg p1, method (TPtrUnexported) OnBothTandBVal()
+pkg p1, type B struct
+pkg p1, type Codec struct
+pkg p1, type Codec struct, Func func(int, int) int
+pkg p1, type EmbedSelector struct
+pkg p1, type EmbedSelector struct, embedded time.Time
+pkg p1, type EmbedURLPtr struct
+pkg p1, type EmbedURLPtr struct, embedded *url.URL
+pkg p1, type Embedded struct
+pkg p1, type I interface { Get, GetNamed, Set }
+pkg p1, type I interface, Get(string) int64
+pkg p1, type I interface, GetNamed(string) int64
+pkg p1, type I interface, Set(string, int64)
+pkg p1, type MyInt int
+pkg p1, type S struct
+pkg p1, type S struct, Public *int
+pkg p1, type S struct, PublicTime time.Time
+pkg p1, type S2 struct
+pkg p1, type S2 struct, Extra bool
+pkg p1, type S2 struct, embedded S
+pkg p1, type SI struct
+pkg p1, type SI struct, I int
+pkg p1, type T struct
+pkg p1, type TPtrExported struct
+pkg p1, type TPtrExported struct, embedded *Embedded
+pkg p1, type TPtrUnexported struct
+pkg p1, var ChecksumError error
+pkg p1, var SIPtr *SI
+pkg p1, var SIPtr2 *SI
+pkg p1, var SIVal SI
+pkg p1, var X I
+pkg p1, var X int64
+pkg p1, var Y int
diff --git a/src/cmd/goapi/testdata/p1/p1.go b/src/cmd/goapi/testdata/p1/p1.go
new file mode 100644 (file)
index 0000000..67a0ed9
--- /dev/null
@@ -0,0 +1,123 @@
+package foo
+
+import (
+       "time"
+       "url"
+)
+
+const (
+       A         = 1
+       a         = 11
+       A64 int64 = 1
+)
+
+const (
+       ConversionConst = MyInt(5)
+)
+
+var ChecksumError = errors.New("gzip checksum error")
+
+const B = 2
+const StrConst = "foo"
+const FloatConst = 1.5
+
+type myInt int
+
+type MyInt int
+
+type S struct {
+       Public     *int
+       private    *int
+       PublicTime time.Time
+}
+
+type EmbedURLPtr struct {
+       *url.URL
+}
+
+type S2 struct {
+       S
+       Extra bool
+}
+
+var X int64
+
+var (
+       Y int
+       X I // todo: resolve this to foo.I? probably doesn't matter.
+)
+
+type I interface {
+       Set(name string, balance int64)
+       Get(string) int64
+       GetNamed(string) (balance int64)
+       private()
+}
+
+func (myInt) privateTypeMethod()           {}
+func (myInt) CapitalMethodUnexportedType() {}
+
+func (s *S2) SMethod(x int8, y int16, z int64) {}
+
+type s struct{}
+
+func (s) method()
+func (s) Method()
+
+func (S) StructValueMethod()
+func (ignored S) StructValueMethodNamedRecv()
+
+func (s *S2) unexported(x int8, y int16, z int64) {}
+
+func Bar(x int8, y int16, z int64)                  {}
+func Bar1(x int8, y int16, z int64) uint64          {}
+func Bar2(x int8, y int16, z int64) (uint8, uint64) {}
+
+func unexported(x int8, y int16, z int64) {}
+
+func TakesFunc(f func(dontWantName int) int)
+
+type Codec struct {
+       Func func(x int, y int) (z int)
+}
+
+type SI struct {
+       I int
+}
+
+var SIVal = SI{}
+var SIPtr = &SI{}
+var SIPtr2 *SI
+
+type T struct {
+       common
+}
+
+type B struct {
+       common
+}
+
+type common struct {
+       i int
+}
+
+type TPtrUnexported struct {
+       *common
+}
+
+type TPtrExported struct {
+       *Embedded
+}
+
+type Embedded struct{}
+
+func (*Embedded) OnEmbedded() {}
+
+func (*T) JustOnT()             {}
+func (*B) JustOnB()             {}
+func (*common) OnBothTandBPtr() {}
+func (common) OnBothTandBVal()  {}
+
+type EmbedSelector struct {
+       time.Time
+}