mirror of
https://github.com/golang/go
synced 2024-10-06 07:31:22 -06:00
5c4e2570c1
Remove VERSION, which was forcing version to 'devel'. Old: $ go version go version devel New: $ go version go version devel +0a3866d6cc6b Mon Sep 24 20:08:05 2012 -0400 The date and time (and time zone) is that of the most recent commit, not the time of the build itself. With some effort we could normalize the zone, but I don't think it's worth the effort (more C coding, since Mercurial is unhelpful). R=r, dsymonds CC=golang-dev https://golang.org/cl/6569049
1135 lines
29 KiB
Go
1135 lines
29 KiB
Go
// 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.
|
|
|
|
// 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.
|
|
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"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Flags
|
|
var (
|
|
// TODO(bradfitz): once Go 1.1 comes out, allow the -c flag to take a comma-separated
|
|
// list of files, rather than just one.
|
|
checkFile = flag.String("c", "", "optional filename to check API against")
|
|
allowNew = flag.Bool("allow_new", true, "allow API additions")
|
|
nextFile = flag.String("next", "", "optional filename of tentative upcoming API features for the next release. This file can be lazily maintained. It only affects the delta warnings from the -c file printed on success.")
|
|
verbose = flag.Bool("v", false, "verbose debugging")
|
|
forceCtx = flag.String("contexts", "", "optional comma-separated list of <goos>-<goarch>[-cgo] to override default contexts.")
|
|
)
|
|
|
|
// contexts are the default contexts which are scanned, unless
|
|
// overridden by the -contexts flag.
|
|
var contexts = []*build.Context{
|
|
{GOOS: "linux", GOARCH: "386", CgoEnabled: true},
|
|
{GOOS: "linux", GOARCH: "386"},
|
|
{GOOS: "linux", GOARCH: "amd64", CgoEnabled: true},
|
|
{GOOS: "linux", GOARCH: "amd64"},
|
|
{GOOS: "linux", GOARCH: "arm"},
|
|
{GOOS: "darwin", GOARCH: "386", CgoEnabled: true},
|
|
{GOOS: "darwin", GOARCH: "386"},
|
|
{GOOS: "darwin", GOARCH: "amd64", CgoEnabled: true},
|
|
{GOOS: "darwin", GOARCH: "amd64"},
|
|
{GOOS: "windows", GOARCH: "amd64"},
|
|
{GOOS: "windows", GOARCH: "386"},
|
|
{GOOS: "freebsd", GOARCH: "amd64"},
|
|
{GOOS: "freebsd", GOARCH: "386"},
|
|
}
|
|
|
|
func contextName(c *build.Context) string {
|
|
s := c.GOOS + "-" + c.GOARCH
|
|
if c.CgoEnabled {
|
|
return s + "-cgo"
|
|
}
|
|
return s
|
|
}
|
|
|
|
func parseContext(c string) *build.Context {
|
|
parts := strings.Split(c, "-")
|
|
if len(parts) < 2 {
|
|
log.Fatalf("bad context: %q", c)
|
|
}
|
|
bc := &build.Context{
|
|
GOOS: parts[0],
|
|
GOARCH: parts[1],
|
|
}
|
|
if len(parts) == 3 {
|
|
if parts[2] == "cgo" {
|
|
bc.CgoEnabled = true
|
|
} else {
|
|
log.Fatalf("bad context: %q", c)
|
|
}
|
|
}
|
|
return bc
|
|
}
|
|
|
|
func setContexts() {
|
|
contexts = []*build.Context{}
|
|
for _, c := range strings.Split(*forceCtx, ",") {
|
|
contexts = append(contexts, parseContext(c))
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
if !strings.Contains(runtime.Version(), "weekly") && !strings.Contains(runtime.Version(), "devel") {
|
|
if *nextFile != "" {
|
|
fmt.Printf("Go version is %q, ignoring -next %s\n", runtime.Version(), *nextFile)
|
|
*nextFile = ""
|
|
}
|
|
}
|
|
|
|
if *forceCtx != "" {
|
|
setContexts()
|
|
}
|
|
for _, c := range contexts {
|
|
c.Compiler = build.Default.Compiler
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
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/") ||
|
|
strings.HasPrefix(pkg, "exp/") ||
|
|
strings.HasPrefix(pkg, "old/") {
|
|
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.WalkPackage(pkg)
|
|
}
|
|
ctxName := contextName(context)
|
|
for _, f := range w.Features() {
|
|
if featureCtx[f] == nil {
|
|
featureCtx[f] = make(map[string]bool)
|
|
}
|
|
featureCtx[f][ctxName] = true
|
|
}
|
|
}
|
|
|
|
var features []string
|
|
for f, cmap := range featureCtx {
|
|
if len(cmap) == len(contexts) {
|
|
features = append(features, f)
|
|
continue
|
|
}
|
|
comma := strings.Index(f, ",")
|
|
for cname := range cmap {
|
|
f2 := fmt.Sprintf("%s (%s)%s", f[:comma], cname, f[comma:])
|
|
features = append(features, f2)
|
|
}
|
|
}
|
|
sort.Strings(features)
|
|
|
|
fail := false
|
|
defer func() {
|
|
if fail {
|
|
os.Exit(1)
|
|
}
|
|
}()
|
|
|
|
bw := bufio.NewWriter(os.Stdout)
|
|
defer bw.Flush()
|
|
|
|
if *checkFile == "" {
|
|
for _, f := range features {
|
|
fmt.Fprintf(bw, "%s\n", f)
|
|
}
|
|
return
|
|
}
|
|
|
|
var required []string
|
|
for _, filename := range []string{*checkFile} {
|
|
required = append(required, fileFeatures(filename)...)
|
|
}
|
|
sort.Strings(required)
|
|
|
|
var optional = make(map[string]bool) // feature => true
|
|
if *nextFile != "" {
|
|
for _, feature := range fileFeatures(*nextFile) {
|
|
optional[feature] = true
|
|
}
|
|
}
|
|
|
|
take := func(sl *[]string) string {
|
|
s := (*sl)[0]
|
|
*sl = (*sl)[1:]
|
|
return s
|
|
}
|
|
|
|
for len(required) > 0 || len(features) > 0 {
|
|
switch {
|
|
case len(features) == 0 || required[0] < features[0]:
|
|
fmt.Fprintf(bw, "-%s\n", take(&required))
|
|
fail = true // broke compatibility
|
|
case len(required) == 0 || required[0] > features[0]:
|
|
newFeature := take(&features)
|
|
if optional[newFeature] {
|
|
// Known added feature to the upcoming release.
|
|
// Delete it from the map so we can detect any upcoming features
|
|
// which were never seen. (so we can clean up the nextFile)
|
|
delete(optional, newFeature)
|
|
} else {
|
|
fmt.Fprintf(bw, "+%s\n", newFeature)
|
|
if !*allowNew {
|
|
fail = true // we're in lock-down mode for next release
|
|
}
|
|
}
|
|
default:
|
|
take(&required)
|
|
take(&features)
|
|
}
|
|
}
|
|
|
|
var missing []string
|
|
for feature := range optional {
|
|
missing = append(missing, feature)
|
|
}
|
|
sort.Strings(missing)
|
|
for _, feature := range missing {
|
|
fmt.Fprintf(bw, "(in next file, but not in API) -%s\n", feature)
|
|
}
|
|
}
|
|
|
|
func fileFeatures(filename string) []string {
|
|
bs, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
log.Fatalf("Error reading file %s: %v", filename, err)
|
|
}
|
|
text := strings.TrimSpace(string(bs))
|
|
if text == "" {
|
|
return nil
|
|
}
|
|
return strings.Split(text, "\n")
|
|
}
|
|
|
|
// pkgSymbol represents a symbol in a package
|
|
type pkgSymbol struct {
|
|
pkg string // "net/http"
|
|
symbol string // "RoundTripper"
|
|
}
|
|
|
|
type Walker struct {
|
|
context *build.Context
|
|
root string
|
|
fset *token.FileSet
|
|
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
|
|
}
|
|
|
|
func NewWalker() *Walker {
|
|
return &Walker{
|
|
fset: token.NewFileSet(),
|
|
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"),
|
|
}
|
|
}
|
|
|
|
// loadState is the state of a package's parsing.
|
|
type loadState int
|
|
|
|
const (
|
|
notLoaded loadState = iota
|
|
loading
|
|
loaded
|
|
)
|
|
|
|
// hardCodedConstantType is a hack until the type checker is sufficient for our needs.
|
|
// Rather than litter the code with unnecessary type annotations, we'll hard-code
|
|
// the cases we can't handle yet.
|
|
func (w *Walker) hardCodedConstantType(name string) (typ string, ok bool) {
|
|
switch w.scope[0] {
|
|
case "pkg syscall":
|
|
switch name {
|
|
case "darwinAMD64":
|
|
return "bool", true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func (w *Walker) Features() (fs []string) {
|
|
for f := range w.features {
|
|
fs = append(fs, f)
|
|
}
|
|
sort.Strings(fs)
|
|
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)
|
|
if err != nil {
|
|
log.Fatalf("error unquoting import string %q: %v", is.Path.Value, err)
|
|
}
|
|
if fpkg != "C" {
|
|
pkgs = append(pkgs, fpkg)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// 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
|
|
}
|
|
w.packageState[name] = loading
|
|
defer func() {
|
|
w.packageState[name] = loaded
|
|
}()
|
|
dir := filepath.Join(w.root, filepath.FromSlash(name))
|
|
|
|
ctxt := w.context
|
|
if ctxt == nil {
|
|
ctxt = &build.Default
|
|
}
|
|
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)
|
|
}
|
|
|
|
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
|
|
|
|
for _, dep := range fileDeps(f) {
|
|
w.WalkPackage(dep)
|
|
}
|
|
}
|
|
|
|
if *verbose {
|
|
log.Printf("package %s", name)
|
|
}
|
|
pop := w.pushScope("pkg " + name)
|
|
defer pop()
|
|
|
|
w.curPackageName = name
|
|
w.curPackage = apkg
|
|
w.constDep = map[string]string{}
|
|
|
|
for _, afile := range apkg.Files {
|
|
w.recordTypes(afile)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, afile := range apkg.Files {
|
|
w.walkFile(afile)
|
|
}
|
|
|
|
w.resolveConstantDeps()
|
|
|
|
// 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)
|
|
|
|
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) 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.
|
|
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:
|
|
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:
|
|
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)
|
|
}
|
|
|
|
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("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.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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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 {
|
|
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 strings.HasPrefix(litType, constDepPrefix) {
|
|
dep := litType[len(constDepPrefix):]
|
|
w.constDep[ident.Name] = dep
|
|
continue
|
|
}
|
|
if litType == "" {
|
|
log.Fatalf("unknown kind in const %q", ident.Name)
|
|
}
|
|
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))
|
|
}
|
|
}
|
|
}
|
|
|
|
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) 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) 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)
|
|
default:
|
|
w.emitFeature(fmt.Sprintf("type %s %s", name, w.nodeString(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.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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// method is a method of an interface.
|
|
type method struct {
|
|
name string // "Read"
|
|
sig string // "([]byte) (int, error)", from funcSigString
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (w *Walker) walkInterfaceType(name string, t *ast.InterfaceType) {
|
|
methNames := []string{}
|
|
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))
|
|
}
|
|
if !complete {
|
|
// The method set has unexported methods, so all the
|
|
// implementations are provided by the same package,
|
|
// so the method set can be extended. Instead of recording
|
|
// the full set of names (below), record only that there were
|
|
// unexported methods. (If the interface shrinks, we will notice
|
|
// because a method signature emitted during the last loop,
|
|
// will disappear.)
|
|
w.emitFeature("unexported methods")
|
|
}
|
|
pop()
|
|
|
|
if !complete {
|
|
return
|
|
}
|
|
|
|
sort.Strings(methNames)
|
|
if len(methNames) == 0 {
|
|
w.emitFeature(fmt.Sprintf("type %s interface {}", name))
|
|
} else {
|
|
w.emitFeature(fmt.Sprintf("type %s interface { %s }", name, strings.Join(methNames, ", ")))
|
|
}
|
|
}
|
|
|
|
func (w *Walker) peekFuncDecl(f *ast.FuncDecl) {
|
|
if f.Recv != nil {
|
|
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()
|
|
}
|
|
|
|
// 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) {
|
|
if !w.wantedPkg[w.curPackageName] {
|
|
return
|
|
}
|
|
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
|
|
}
|