summaryrefslogtreecommitdiff
path: root/src/cmd/api/goapi.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/api/goapi.go')
-rw-r--r--src/cmd/api/goapi.go1221
1 files changed, 442 insertions, 779 deletions
diff --git a/src/cmd/api/goapi.go b/src/cmd/api/goapi.go
index ff75f00e3..a62c87421 100644
--- a/src/cmd/api/goapi.go
+++ b/src/cmd/api/goapi.go
@@ -2,38 +2,32 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// Api computes the exported API of a set of Go packages.
-//
-// BUG(bradfitz): Note that this tool is only currently suitable
-// for use on the Go standard library, not arbitrary packages.
-// Once the Go AST has type information, this tool will be more
-// reliable without hard-coded hacks throughout.
+// +build api_tool
+
+// Binary api 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"
"io/ioutil"
"log"
"os"
"os/exec"
- "path"
"path/filepath"
"regexp"
"runtime"
"sort"
- "strconv"
"strings"
+
+ "code.google.com/p/go.tools/go/types"
)
// Flags
@@ -53,6 +47,7 @@ var contexts = []*build.Context{
{GOOS: "linux", GOARCH: "386"},
{GOOS: "linux", GOARCH: "amd64", CgoEnabled: true},
{GOOS: "linux", GOARCH: "amd64"},
+ {GOOS: "linux", GOARCH: "arm", CgoEnabled: true},
{GOOS: "linux", GOARCH: "arm"},
{GOOS: "darwin", GOARCH: "386", CgoEnabled: true},
{GOOS: "darwin", GOARCH: "386"},
@@ -60,8 +55,22 @@ var contexts = []*build.Context{
{GOOS: "darwin", GOARCH: "amd64"},
{GOOS: "windows", GOARCH: "amd64"},
{GOOS: "windows", GOARCH: "386"},
- {GOOS: "freebsd", GOARCH: "amd64"},
+ {GOOS: "freebsd", GOARCH: "386", CgoEnabled: true},
{GOOS: "freebsd", GOARCH: "386"},
+ {GOOS: "freebsd", GOARCH: "amd64", CgoEnabled: true},
+ {GOOS: "freebsd", GOARCH: "amd64"},
+ {GOOS: "freebsd", GOARCH: "arm", CgoEnabled: true},
+ {GOOS: "freebsd", GOARCH: "arm"},
+ {GOOS: "netbsd", GOARCH: "386", CgoEnabled: true},
+ {GOOS: "netbsd", GOARCH: "386"},
+ {GOOS: "netbsd", GOARCH: "amd64", CgoEnabled: true},
+ {GOOS: "netbsd", GOARCH: "amd64"},
+ {GOOS: "netbsd", GOARCH: "arm", CgoEnabled: true},
+ {GOOS: "netbsd", GOARCH: "arm"},
+ {GOOS: "openbsd", GOARCH: "386", CgoEnabled: true},
+ {GOOS: "openbsd", GOARCH: "386"},
+ {GOOS: "openbsd", GOARCH: "amd64", CgoEnabled: true},
+ {GOOS: "openbsd", GOARCH: "amd64"},
}
func contextName(c *build.Context) string {
@@ -115,35 +124,35 @@ func main() {
c.Compiler = build.Default.Compiler
}
- var pkgs []string
+ var pkgNames []string
if flag.NArg() > 0 {
- pkgs = flag.Args()
+ pkgNames = flag.Args()
} else {
stds, err := exec.Command("go", "list", "std").Output()
if err != nil {
log.Fatal(err)
}
- pkgs = strings.Fields(string(stds))
+ pkgNames = strings.Fields(string(stds))
}
var featureCtx = make(map[string]map[string]bool) // feature -> context name -> true
for _, context := range contexts {
- w := NewWalker()
- w.context = context
-
- for _, pkg := range pkgs {
- w.wantedPkg[pkg] = true
- }
-
- for _, pkg := range pkgs {
- if strings.HasPrefix(pkg, "cmd/") {
- continue
- }
- if fi, err := os.Stat(filepath.Join(w.root, pkg)); err != nil || !fi.IsDir() {
- log.Fatalf("no source in tree for package %q", pkg)
+ w := NewWalker(context, filepath.Join(build.Default.GOROOT, "src/pkg"))
+
+ for _, name := range pkgNames {
+ // - Package "unsafe" contains special signatures requiring
+ // extra care when printing them - ignore since it is not
+ // going to change w/o a language change.
+ // - We don't care about the API of commands.
+ if name != "unsafe" && !strings.HasPrefix(name, "cmd/") {
+ if name == "runtime/cgo" && !context.CgoEnabled {
+ // w.Import(name) will return nil
+ continue
+ }
+ w.export(w.Import(name))
}
- w.WalkPackage(pkg)
}
+
ctxName := contextName(context)
for _, f := range w.Features() {
if featureCtx[f] == nil {
@@ -179,7 +188,7 @@ func main() {
if *checkFile == "" {
sort.Strings(features)
for _, f := range features {
- fmt.Fprintf(bw, "%s\n", f)
+ fmt.Fprintln(bw, f)
}
return
}
@@ -193,6 +202,22 @@ func main() {
fail = !compareAPI(bw, features, required, optional, exception)
}
+// export emits the exported package features.
+func (w *Walker) export(pkg *types.Package) {
+ if *verbose {
+ log.Println(pkg)
+ }
+ pop := w.pushScope("pkg " + pkg.Path())
+ w.current = pkg
+ scope := pkg.Scope()
+ for _, name := range scope.Names() {
+ if ast.IsExported(name) {
+ w.emitObj(scope.Lookup(name))
+ }
+ }
+ pop()
+}
+
func set(items []string) map[string]bool {
s := make(map[string]bool)
for _, v := range items {
@@ -231,7 +256,12 @@ func compareAPI(w io.Writer, features, required, optional, exception []string) (
case len(features) == 0 || (len(required) > 0 && required[0] < features[0]):
feature := take(&required)
if exceptionSet[feature] {
- fmt.Fprintf(w, "~%s\n", feature)
+ // An "unfortunate" case: the feature was once
+ // included in the API (e.g. go1.txt), but was
+ // subsequently removed. These are already
+ // acknowledged by being in the file
+ // "api/except.txt". No need to print them out
+ // here.
} else if featureSet[featureWithoutContext(feature)] {
// okay.
} else {
@@ -284,53 +314,26 @@ func fileFeatures(filename string) []string {
return strings.Split(text, "\n")
}
-// pkgSymbol represents a symbol in a package
-type pkgSymbol struct {
- pkg string // "net/http"
- symbol string // "RoundTripper"
-}
-
var fset = token.NewFileSet()
type Walker struct {
- context *build.Context
- root string
- scope []string
- features map[string]bool // set
- lastConstType string
- curPackageName string
- curPackage *ast.Package
- prevConstType map[pkgSymbol]string
- constDep map[string]string // key's const identifier has type of future value const identifier
- packageState map[string]loadState
- interfaces map[pkgSymbol]*ast.InterfaceType
- functionTypes map[pkgSymbol]string // symbol => return type
- selectorFullPkg map[string]string // "http" => "net/http", updated by imports
- wantedPkg map[string]bool // packages requested on the command line
+ context *build.Context
+ root string
+ scope []string
+ current *types.Package
+ features map[string]bool // set
+ imported map[string]*types.Package // packages already imported
}
-func NewWalker() *Walker {
+func NewWalker(context *build.Context, root string) *Walker {
return &Walker{
- features: make(map[string]bool),
- packageState: make(map[string]loadState),
- interfaces: make(map[pkgSymbol]*ast.InterfaceType),
- functionTypes: make(map[pkgSymbol]string),
- selectorFullPkg: make(map[string]string),
- wantedPkg: make(map[string]bool),
- prevConstType: make(map[pkgSymbol]string),
- root: filepath.Join(build.Default.GOROOT, "src/pkg"),
+ context: context,
+ root: root,
+ features: map[string]bool{},
+ imported: map[string]*types.Package{"unsafe": types.Unsafe},
}
}
-// loadState is the state of a package's parsing.
-type loadState int
-
-const (
- notLoaded loadState = iota
- loading
- loaded
-)
-
func (w *Walker) Features() (fs []string) {
for f := range w.features {
fs = append(fs, f)
@@ -339,127 +342,187 @@ func (w *Walker) Features() (fs []string) {
return
}
-// fileDeps returns the imports in a file.
-func fileDeps(f *ast.File) (pkgs []string) {
- for _, is := range f.Imports {
- fpkg, err := strconv.Unquote(is.Path.Value)
+var parsedFileCache = make(map[string]*ast.File)
+
+func (w *Walker) parseFile(dir, file string) (*ast.File, error) {
+ filename := filepath.Join(dir, file)
+ f, _ := parsedFileCache[filename]
+ if f != nil {
+ return f, nil
+ }
+
+ var err error
+
+ // generate missing context-dependent files.
+
+ if w.context != nil && file == fmt.Sprintf("zgoos_%s.go", w.context.GOOS) {
+ src := fmt.Sprintf("package runtime; const theGoos = `%s`", w.context.GOOS)
+ f, err = parser.ParseFile(fset, filename, src, 0)
if err != nil {
- log.Fatalf("error unquoting import string %q: %v", is.Path.Value, err)
- }
- if fpkg != "C" {
- pkgs = append(pkgs, fpkg)
+ log.Fatalf("incorrect generated file: %s", err)
}
}
- return
-}
-var parsedFileCache = make(map[string]*ast.File)
+ if w.context != nil && file == fmt.Sprintf("zgoarch_%s.go", w.context.GOARCH) {
+ src := fmt.Sprintf("package runtime; const theGoarch = `%s`", w.context.GOARCH)
+ f, err = parser.ParseFile(fset, filename, src, 0)
+ if err != nil {
+ log.Fatalf("incorrect generated file: %s", err)
+ }
+ }
-func parseFile(filename string) (*ast.File, error) {
- f, ok := parsedFileCache[filename]
- if !ok {
- var err error
+ if f == nil {
f, err = parser.ParseFile(fset, filename, nil, 0)
if err != nil {
return nil, err
}
- parsedFileCache[filename] = f
}
- return clone(f).(*ast.File), nil
+
+ parsedFileCache[filename] = f
+ return f, nil
}
-// WalkPackage walks all files in package `name'.
-// WalkPackage does nothing if the package has already been loaded.
-func (w *Walker) WalkPackage(name string) {
- switch w.packageState[name] {
- case loading:
- log.Fatalf("import cycle loading package %q?", name)
- case loaded:
- return
+func contains(list []string, s string) bool {
+ for _, t := range list {
+ if t == s {
+ return true
+ }
}
- w.packageState[name] = loading
- defer func() {
- w.packageState[name] = loaded
- }()
- dir := filepath.Join(w.root, filepath.FromSlash(name))
+ return false
+}
- ctxt := w.context
- if ctxt == nil {
- ctxt = &build.Default
+var (
+ pkgCache = map[string]*types.Package{} // map tagKey to package
+ pkgTags = map[string][]string{} // map import dir to list of relevant tags
+)
+
+// tagKey returns the tag-based key to use in the pkgCache.
+// It is a comma-separated string; the first part is dir, the rest tags.
+// The satisfied tags are derived from context but only those that
+// matter (the ones listed in the tags argument) are used.
+// The tags list, which came from go/build's Package.AllTags,
+// is known to be sorted.
+func tagKey(dir string, context *build.Context, tags []string) string {
+ ctags := map[string]bool{
+ context.GOOS: true,
+ context.GOARCH: true,
}
- info, err := ctxt.ImportDir(dir, 0)
- if err != nil {
- if strings.Contains(err.Error(), "no Go source files") {
- return
- }
- log.Fatalf("pkg %q, dir %q: ScanDir: %v", name, dir, err)
+ if context.CgoEnabled {
+ ctags["cgo"] = true
}
-
- apkg := &ast.Package{
- Files: make(map[string]*ast.File),
+ for _, tag := range context.BuildTags {
+ ctags[tag] = true
}
-
- files := append(append([]string{}, info.GoFiles...), info.CgoFiles...)
- for _, file := range files {
- f, err := parseFile(filepath.Join(dir, file))
- if err != nil {
- log.Fatalf("error parsing package %s, file %s: %v", name, file, err)
+ // TODO: ReleaseTags (need to load default)
+ key := dir
+ for _, tag := range tags {
+ if ctags[tag] {
+ key += "," + tag
}
- apkg.Files[file] = f
+ }
+ return key
+}
- for _, dep := range fileDeps(f) {
- w.WalkPackage(dep)
+// Importing is a sentinel taking the place in Walker.imported
+// for a package that is in the process of being imported.
+var importing types.Package
+
+func (w *Walker) Import(name string) (pkg *types.Package) {
+ pkg = w.imported[name]
+ if pkg != nil {
+ if pkg == &importing {
+ log.Fatalf("cycle importing package %q", name)
}
+ return pkg
}
+ w.imported[name] = &importing
- if *verbose {
- log.Printf("package %s", name)
+ // Determine package files.
+ dir := filepath.Join(w.root, filepath.FromSlash(name))
+ if fi, err := os.Stat(dir); err != nil || !fi.IsDir() {
+ log.Fatalf("no source in tree for package %q", pkg)
}
- pop := w.pushScope("pkg " + name)
- defer pop()
- w.curPackageName = name
- w.curPackage = apkg
- w.constDep = map[string]string{}
+ context := w.context
+ if context == nil {
+ context = &build.Default
+ }
- for _, afile := range apkg.Files {
- w.recordTypes(afile)
+ // Look in cache.
+ // If we've already done an import with the same set
+ // of relevant tags, reuse the result.
+ var key string
+ if tags, ok := pkgTags[dir]; ok {
+ key = tagKey(dir, context, tags)
+ if pkg := pkgCache[key]; pkg != nil {
+ w.imported[name] = pkg
+ return pkg
+ }
}
- // Register all function declarations first.
- for _, afile := range apkg.Files {
- for _, di := range afile.Decls {
- if d, ok := di.(*ast.FuncDecl); ok {
- w.peekFuncDecl(d)
- }
+ info, err := context.ImportDir(dir, 0)
+ if err != nil {
+ if _, nogo := err.(*build.NoGoError); nogo {
+ return
}
+ log.Fatalf("pkg %q, dir %q: ScanDir: %v", name, dir, err)
}
- for _, afile := range apkg.Files {
- w.walkFile(afile)
+ // Save tags list first time we see a directory.
+ if _, ok := pkgTags[dir]; !ok {
+ pkgTags[dir] = info.AllTags
+ key = tagKey(dir, context, info.AllTags)
}
- w.resolveConstantDeps()
+ filenames := append(append([]string{}, info.GoFiles...), info.CgoFiles...)
- // 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, doc.AllMethods)
+ // Certain files only exist when building for the specified context.
+ // Add them manually.
+ if name == "runtime" {
+ n := fmt.Sprintf("zgoos_%s.go", w.context.GOOS)
+ if !contains(filenames, n) {
+ filenames = append(filenames, n)
+ }
- for _, t := range dpkg.Types {
- // Move funcs up to the top-level, not hiding in the Types.
- dpkg.Funcs = append(dpkg.Funcs, t.Funcs...)
+ n = fmt.Sprintf("zgoarch_%s.go", w.context.GOARCH)
+ if !contains(filenames, n) {
+ filenames = append(filenames, n)
+ }
+ }
- for _, m := range t.Methods {
- w.walkFuncDecl(m.Decl)
+ // Parse package files.
+ var files []*ast.File
+ for _, file := range filenames {
+ f, err := w.parseFile(dir, file)
+ if err != nil {
+ log.Fatalf("error parsing package %s: %s", name, err)
}
+ files = append(files, f)
}
- for _, f := range dpkg.Funcs {
- w.walkFuncDecl(f.Decl)
+ // Type-check package files.
+ conf := types.Config{
+ IgnoreFuncBodies: true,
+ FakeImportC: true,
+ Import: func(imports map[string]*types.Package, name string) (*types.Package, error) {
+ pkg := w.Import(name)
+ imports[name] = pkg
+ return pkg, nil
+ },
+ }
+ pkg, err = conf.Check(name, fset, files, nil)
+ if err != nil {
+ ctxt := "<no context>"
+ if w.context != nil {
+ ctxt = fmt.Sprintf("%s-%s", w.context.GOOS, w.context.GOARCH)
+ }
+ log.Fatalf("error typechecking package %s: %s (%s)", name, err, ctxt)
}
+
+ pkgCache[key] = pkg
+
+ w.imported[name] = pkg
+ return
}
// pushScope enters a new scope (walking a package, type, node, etc)
@@ -478,707 +541,307 @@ func (w *Walker) pushScope(name string) (popFunc func()) {
}
}
-func (w *Walker) recordTypes(file *ast.File) {
- for _, di := range file.Decls {
- switch d := di.(type) {
- case *ast.GenDecl:
- switch d.Tok {
- case token.TYPE:
- for _, sp := range d.Specs {
- ts := sp.(*ast.TypeSpec)
- name := ts.Name.Name
- if ast.IsExported(name) {
- if it, ok := ts.Type.(*ast.InterfaceType); ok {
- w.noteInterface(name, it)
- }
- }
- }
- }
- }
- }
-}
-
-func (w *Walker) walkFile(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:
- for _, sp := range d.Specs {
- is := sp.(*ast.ImportSpec)
- fpath, err := strconv.Unquote(is.Path.Value)
- if err != nil {
- log.Fatal(err)
- }
- name := path.Base(fpath)
- if is.Name != nil {
- name = is.Name.Name
- }
- w.selectorFullPkg[name] = fpath
- }
- 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.
+func sortedMethodNames(typ *types.Interface) []string {
+ n := typ.NumMethods()
+ list := make([]string, n)
+ for i := range list {
+ list[i] = typ.Method(i).Name()
+ }
+ sort.Strings(list)
+ return list
+}
+
+func (w *Walker) writeType(buf *bytes.Buffer, typ types.Type) {
+ switch typ := typ.(type) {
+ case *types.Basic:
+ s := typ.Name()
+ switch typ.Kind() {
+ case types.UnsafePointer:
+ s = "unsafe.Pointer"
+ case types.UntypedBool:
+ s = "ideal-bool"
+ case types.UntypedInt:
+ s = "ideal-int"
+ case types.UntypedRune:
+ // "ideal-char" for compatibility with old tool
+ // TODO(gri) change to "ideal-rune"
+ s = "ideal-char"
+ case types.UntypedFloat:
+ s = "ideal-float"
+ case types.UntypedComplex:
+ s = "ideal-complex"
+ case types.UntypedString:
+ s = "ideal-string"
+ case types.UntypedNil:
+ panic("should never see untyped nil type")
+ default:
+ switch s {
+ case "byte":
+ s = "uint8"
+ case "rune":
+ s = "int32"
+ }
+ }
+ buf.WriteString(s)
+
+ case *types.Array:
+ fmt.Fprintf(buf, "[%d]", typ.Len())
+ w.writeType(buf, typ.Elem())
+
+ case *types.Slice:
+ buf.WriteString("[]")
+ w.writeType(buf, typ.Elem())
+
+ case *types.Struct:
+ buf.WriteString("struct")
+
+ case *types.Pointer:
+ buf.WriteByte('*')
+ w.writeType(buf, typ.Elem())
+
+ case *types.Tuple:
+ panic("should never see a tuple type")
+
+ case *types.Signature:
+ buf.WriteString("func")
+ w.writeSignature(buf, typ)
+
+ case *types.Interface:
+ buf.WriteString("interface{")
+ if typ.NumMethods() > 0 {
+ buf.WriteByte(' ')
+ buf.WriteString(strings.Join(sortedMethodNames(typ), ", "))
+ buf.WriteByte(' ')
+ }
+ buf.WriteString("}")
+
+ case *types.Map:
+ buf.WriteString("map[")
+ w.writeType(buf, typ.Key())
+ buf.WriteByte(']')
+ w.writeType(buf, typ.Elem())
+
+ case *types.Chan:
+ var s string
+ switch typ.Dir() {
+ case ast.SEND:
+ s = "chan<- "
+ case ast.RECV:
+ s = "<-chan "
default:
- log.Printf("unhandled %T, %#v\n", di, di)
- printer.Fprint(os.Stderr, fset, di)
- os.Stderr.Write([]byte("\n"))
+ s = "chan "
}
- }
-}
-
-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")
+ buf.WriteString(s)
+ w.writeType(buf, typ.Elem())
-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)
+ case *types.Named:
+ obj := typ.Obj()
+ pkg := obj.Pkg()
+ if pkg != nil && pkg != w.current {
+ buf.WriteString(pkg.Name())
+ buf.WriteByte('.')
}
- return litType, nil
- case *ast.UnaryExpr:
- return w.constValueType(v.X)
- case *ast.SelectorExpr:
- lhs := w.nodeString(v.X)
- rhs := w.nodeString(v.Sel)
- pkg, ok := w.selectorFullPkg[lhs]
- if !ok {
- return "", fmt.Errorf("unknown constant reference; unknown package in expression %s.%s", lhs, rhs)
- }
- if t, ok := w.prevConstType[pkgSymbol{pkg, rhs}]; ok {
- return t, nil
- }
- return "", fmt.Errorf("unknown constant reference to %s.%s", lhs, rhs)
- case *ast.Ident:
- if v.Name == "iota" {
- return "ideal-int", nil // hack.
- }
- if v.Name == "false" || v.Name == "true" {
- return "bool", nil
- }
- if v.Name == "intSize" && w.curPackageName == "strconv" {
- // Hack.
- return "ideal-int", nil
- }
- if t, ok := w.prevConstType[pkgSymbol{w.curPackageName, v.Name}]; ok {
- return t, nil
- }
- 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
- }
- right, err := w.constValueType(v.Y)
- if err != nil {
- return "", err
- }
- if left != right {
- // TODO(bradfitz): encode the real rules here,
- // rather than this mess.
- 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
- }
- if left == "ideal-int" && !strings.HasPrefix(right, "ideal-") {
- return right, nil
- }
- if right == "ideal-int" && !strings.HasPrefix(left, "ideal-") {
- return left, nil
- }
- if strings.HasPrefix(left, constDepPrefix) && strings.HasPrefix(right, constDepPrefix) {
- // Just pick one.
- // e.g. text/scanner GoTokens const-dependency:ScanIdents, const-dependency:ScanFloats
- return left, 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)
-}
+ buf.WriteString(typ.Obj().Name())
-func (w *Walker) varValueType(vi interface{}) (string, error) {
- 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:
- var funSym pkgSymbol
- if selnode, ok := v.Fun.(*ast.SelectorExpr); ok {
- // assume it is not a method.
- pkg, ok := w.selectorFullPkg[w.nodeString(selnode.X)]
- if !ok {
- return "", fmt.Errorf("not a package: %s", w.nodeString(selnode.X))
- }
- funSym = pkgSymbol{pkg, selnode.Sel.Name}
- if retType, ok := w.functionTypes[funSym]; ok {
- if ast.IsExported(retType) && pkg != w.curPackageName {
- // otherpkg.F returning an exported type from otherpkg.
- return pkg + "." + retType, nil
- } else {
- return retType, nil
- }
- }
- } else {
- funSym = pkgSymbol{w.curPackageName, w.nodeString(v.Fun)}
- if retType, ok := w.functionTypes[funSym]; ok {
- return retType, nil
- }
- }
- // maybe a function call; maybe a conversion. Need to lookup type.
- // TODO(bradfitz): this is a hack, but arguably most of this tool is,
- // until the Go AST has type information.
- nodeStr := w.nodeString(v.Fun)
- switch nodeStr {
- case "string", "[]byte":
- return nodeStr, nil
- }
- return "", fmt.Errorf("not a known function %q", nodeStr)
default:
- return "", fmt.Errorf("unknown const value type %T", vi)
+ panic(fmt.Sprintf("unknown type %T", typ))
}
}
-// 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.GenDecl:
- switch d.Tok {
- 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
- }
- }
- }
- }
- }
- }
+func (w *Walker) writeSignature(buf *bytes.Buffer, sig *types.Signature) {
+ w.writeParams(buf, sig.Params(), sig.IsVariadic())
+ switch res := sig.Results(); res.Len() {
+ case 0:
+ // nothing to do
+ case 1:
+ buf.WriteByte(' ')
+ w.writeType(buf, res.At(0).Type())
+ default:
+ buf.WriteByte(' ')
+ w.writeParams(buf, res, false)
}
- return nil, nil, false
}
-// constDepPrefix is a magic prefix that is used by constValueType
-// and walkConst to signal that a type isn't known yet. These are
-// resolved at the end of walking of a package's files.
-const constDepPrefix = "const-dependency:"
-
-func (w *Walker) walkConst(vs *ast.ValueSpec) {
- for _, ident := range vs.Names {
- 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 {
- log.Fatalf("unknown kind in const %q (%T): %v", ident.Name, vs.Values[0], err)
- }
- }
- }
- if dep := strings.TrimPrefix(litType, constDepPrefix); dep != litType {
- w.constDep[ident.Name] = dep
- continue
- }
- if litType == "" {
- log.Fatalf("unknown kind in const %q", ident.Name)
+func (w *Walker) writeParams(buf *bytes.Buffer, t *types.Tuple, variadic bool) {
+ buf.WriteByte('(')
+ for i, n := 0, t.Len(); i < n; i++ {
+ if i > 0 {
+ buf.WriteString(", ")
}
- w.lastConstType = litType
-
- w.prevConstType[pkgSymbol{w.curPackageName, ident.Name}] = litType
-
- if ast.IsExported(ident.Name) {
- w.emitFeature(fmt.Sprintf("const %s %s", ident, litType))
+ typ := t.At(i).Type()
+ if variadic && i+1 == n {
+ buf.WriteString("...")
+ typ = typ.(*types.Slice).Elem()
}
+ w.writeType(buf, typ)
}
+ buf.WriteByte(')')
}
-func (w *Walker) resolveConstantDeps() {
- var findConstType func(string) string
- findConstType = func(ident string) string {
- if dep, ok := w.constDep[ident]; ok {
- return findConstType(dep)
- }
- if t, ok := w.prevConstType[pkgSymbol{w.curPackageName, ident}]; ok {
- return t
- }
- return ""
- }
- for ident := range w.constDep {
- if !ast.IsExported(ident) {
- continue
- }
- t := findConstType(ident)
- if t == "" {
- log.Fatalf("failed to resolve constant %q", ident)
- }
- w.emitFeature(fmt.Sprintf("const %s %s", ident, t))
- }
+func (w *Walker) typeString(typ types.Type) string {
+ var buf bytes.Buffer
+ w.writeType(&buf, typ)
+ return buf.String()
}
-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, fset, node)
- return b.String()
+func (w *Walker) signatureString(sig *types.Signature) string {
+ var buf bytes.Buffer
+ w.writeSignature(&buf, sig)
+ return buf.String()
}
-func (w *Walker) nodeDebug(node interface{}) string {
- if node == nil {
- return ""
+func (w *Walker) emitObj(obj types.Object) {
+ switch obj := obj.(type) {
+ case *types.Const:
+ w.emitf("const %s %s", obj.Name(), w.typeString(obj.Type()))
+ w.emitf("const %s = %s", obj.Name(), obj.Val())
+ case *types.Var:
+ w.emitf("var %s %s", obj.Name(), w.typeString(obj.Type()))
+ case *types.TypeName:
+ w.emitType(obj)
+ case *types.Func:
+ w.emitFunc(obj)
+ default:
+ panic("unknown object: " + obj.String())
}
- var b bytes.Buffer
- ast.Fprint(&b, fset, node, nil)
- return b.String()
-}
-
-func (w *Walker) noteInterface(name string, it *ast.InterfaceType) {
- w.interfaces[pkgSymbol{w.curPackageName, name}] = it
}
-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)
+func (w *Walker) emitType(obj *types.TypeName) {
+ name := obj.Name()
+ typ := obj.Type()
+ switch typ := typ.Underlying().(type) {
+ case *types.Struct:
+ w.emitStructType(name, typ)
+ case *types.Interface:
+ w.emitIfaceType(name, typ)
+ return // methods are handled by emitIfaceType
default:
- w.emitFeature(fmt.Sprintf("type %s %s", name, w.nodeString(w.namelessType(ts.Type))))
+ w.emitf("type %s %s", name, w.typeString(typ.Underlying()))
}
-}
-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))))
+ // emit methods with value receiver
+ var methodNames map[string]bool
+ vset := typ.MethodSet()
+ for i, n := 0, vset.Len(); i < n; i++ {
+ m := vset.At(i)
+ if m.Obj().IsExported() {
+ w.emitMethod(m)
+ if methodNames == nil {
+ methodNames = make(map[string]bool)
}
+ methodNames[m.Obj().Name()] = true
}
- 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.Fatalf("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)
- }
+ }
+
+ // emit methods with pointer receiver; exclude
+ // methods that we have emitted already
+ // (the method set of *T includes the methods of T)
+ pset := types.NewPointer(typ).MethodSet()
+ for i, n := 0, pset.Len(); i < n; i++ {
+ m := pset.At(i)
+ if m.Obj().IsExported() && !methodNames[m.Obj().Name()] {
+ w.emitMethod(m)
}
}
}
-// method is a method of an interface.
-type method struct {
- name string // "Read"
- sig string // "([]byte) (int, error)", from funcSigString
-}
+func (w *Walker) emitStructType(name string, typ *types.Struct) {
+ typeStruct := fmt.Sprintf("type %s struct", name)
+ w.emitf(typeStruct)
+ defer w.pushScope(typeStruct)()
-// 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, 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) {
- case *ast.FuncType:
- for _, mname := range f.Names {
- if ast.IsExported(mname.Name) {
- ft := typ.(*ast.FuncType)
- methods = append(methods, method{
- name: mname.Name,
- sig: w.funcSigString(ft),
- })
- } else {
- complete = false
- }
- }
- case *ast.Ident:
- embedded := typ.(*ast.Ident).Name
- if embedded == "error" {
- methods = append(methods, method{
- name: "Error",
- sig: "() string",
- })
- continue
- }
- if !ast.IsExported(embedded) {
- log.Fatalf("unexported embedded interface %q in exported interface %s.%s; confused",
- embedded, pkg, iname)
- }
- 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)
- fpkg, ok := w.selectorFullPkg[lhs]
- if !ok {
- log.Fatalf("can't resolve selector %q in interface %s.%s", lhs, pkg, iname)
- }
- m, c := w.interfaceMethods(fpkg, rhs)
- methods = append(methods, m...)
- complete = complete && c
- default:
- log.Fatalf("unknown type %T in interface field", typ)
+ for i := 0; i < typ.NumFields(); i++ {
+ f := typ.Field(i)
+ if !f.IsExported() {
+ continue
+ }
+ typ := f.Type()
+ if f.Anonymous() {
+ w.emitf("embedded %s", w.typeString(typ))
+ continue
}
+ w.emitf("%s %s", f.Name(), w.typeString(typ))
}
- return
}
-func (w *Walker) walkInterfaceType(name string, t *ast.InterfaceType) {
- methNames := []string{}
+func (w *Walker) emitIfaceType(name string, typ *types.Interface) {
pop := w.pushScope("type " + name + " interface")
- 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))
+
+ var methodNames []string
+ complete := true
+ mset := typ.MethodSet()
+ for i, n := 0, mset.Len(); i < n; i++ {
+ m := mset.At(i).Obj().(*types.Func)
+ if !m.IsExported() {
+ complete = false
+ continue
+ }
+ methodNames = append(methodNames, m.Name())
+ w.emitf("%s%s", m.Name(), w.signatureString(m.Type().(*types.Signature)))
}
+
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,
+ // because a method signature emitted during the last loop
// will disappear.)
- w.emitFeature("unexported methods")
+ w.emitf("unexported methods")
}
+
pop()
if !complete {
return
}
- sort.Strings(methNames)
- if len(methNames) == 0 {
- w.emitFeature(fmt.Sprintf("type %s interface {}", name))
- } else {
- w.emitFeature(fmt.Sprintf("type %s interface { %s }", name, strings.Join(methNames, ", ")))
- }
-}
-
-func (w *Walker) peekFuncDecl(f *ast.FuncDecl) {
- if f.Recv != nil {
+ if len(methodNames) == 0 {
+ w.emitf("type %s interface {}", name)
return
}
- // Record return type for later use.
- if f.Type.Results != nil && len(f.Type.Results.List) == 1 {
- retType := w.nodeString(w.namelessType(f.Type.Results.List[0].Type))
- w.functionTypes[pkgSymbol{w.curPackageName, f.Name.Name}] = retType
- }
-}
-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
- 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(", ")
- }
- writeField(&b, f)
- }
- }
- b.WriteByte(')')
- if ft.Results != nil {
- 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('(')
- }
- for i, f := range ft.Results.List {
- if i > 0 {
- b.WriteString(", ")
- }
- writeField(&b, f)
- }
- if nr > 1 {
- b.WriteByte(')')
- }
- }
- }
- return b.String()
+ sort.Strings(methodNames)
+ w.emitf("type %s interface { %s }", name, strings.Join(methodNames, ", "))
}
-// 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),
+func (w *Walker) emitFunc(f *types.Func) {
+ sig := f.Type().(*types.Signature)
+ if sig.Recv() != nil {
+ panic("method considered a regular function: " + f.String())
}
+ w.emitf("func %s%s", f.Name(), w.signatureString(sig))
}
-// 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 {
- 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))
- }
+func (w *Walker) emitMethod(m *types.Selection) {
+ sig := m.Type().(*types.Signature)
+ recv := sig.Recv().Type()
+ // report exported methods with unexported reveiver base type
+ if true {
+ base := recv
+ if p, _ := recv.(*types.Pointer); p != nil {
+ base = p.Elem()
+ }
+ if obj := base.(*types.Named).Obj(); !obj.IsExported() {
+ log.Fatalf("exported method with unexported receiver base type: %s", m)
}
}
- return fl2
+ w.emitf("method (%s) %s%s", w.typeString(recv), m.Obj().Name(), w.signatureString(sig))
}
-// 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) emitf(format string, args ...interface{}) {
+ f := strings.Join(w.scope, ", ") + ", " + fmt.Sprintf(format, args...)
+ if strings.Contains(f, "\n") {
+ panic("feature contains newlines: " + f)
}
-}
-
-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)
}
-
- 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
-}