mirror of
https://github.com/golang/go
synced 2024-11-21 22:44:40 -07:00
cmd/goapi: new tool for tracking exported API over time
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
This commit is contained in:
parent
c21b343438
commit
5c04272ff3
722
src/cmd/goapi/goapi.go
Normal file
722
src/cmd/goapi/goapi.go
Normal file
@ -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
|
||||
}
|
73
src/cmd/goapi/goapi_test.go
Normal file
73
src/cmd/goapi/goapi_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
57
src/cmd/goapi/testdata/p1/golden.txt
vendored
Normal file
57
src/cmd/goapi/testdata/p1/golden.txt
vendored
Normal file
@ -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
|
123
src/cmd/goapi/testdata/p1/p1.go
vendored
Normal file
123
src/cmd/goapi/testdata/p1/p1.go
vendored
Normal file
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user