From a94bd4d7c324648f1736e8f7fb1a0fd4b13bacc6 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sun, 29 Jan 2012 21:04:13 -0800 Subject: [PATCH] cmd/goapi: expand embedded interfaces Fixes #2801 R=rsc CC=golang-dev https://golang.org/cl/5576068 --- src/cmd/goapi/goapi.go | 218 +++++++++++++++--- src/cmd/goapi/goapi_test.go | 9 +- .../testdata/{ => src/pkg}/p1/golden.txt | 9 +- src/cmd/goapi/testdata/{ => src/pkg}/p1/p1.go | 18 +- src/cmd/goapi/testdata/src/pkg/p2/golden.txt | 2 + src/cmd/goapi/testdata/src/pkg/p2/p2.go | 5 + 6 files changed, 221 insertions(+), 40 deletions(-) rename src/cmd/goapi/testdata/{ => src/pkg}/p1/golden.txt (83%) rename src/cmd/goapi/testdata/{ => src/pkg}/p1/p1.go (92%) create mode 100644 src/cmd/goapi/testdata/src/pkg/p2/golden.txt create mode 100644 src/cmd/goapi/testdata/src/pkg/p2/p2.go diff --git a/src/cmd/goapi/goapi.go b/src/cmd/goapi/goapi.go index a64edcae7d9..a9fbbeac3d3 100644 --- a/src/cmd/goapi/goapi.go +++ b/src/cmd/goapi/goapi.go @@ -21,8 +21,10 @@ import ( "log" "os" "os/exec" + "path" "path/filepath" "sort" + "strconv" "strings" ) @@ -51,6 +53,11 @@ func main() { if err != nil { log.Fatalf("failed to find tree: %v", err) } + w.tree = tree + + for _, pkg := range pkgs { + w.wantedPkg[pkg] = true + } for _, pkg := range pkgs { if strings.HasPrefix(pkg, "cmd/") || @@ -61,8 +68,7 @@ func main() { 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) + w.WalkPackage(pkg) } bw := bufio.NewWriter(os.Stdout) @@ -99,23 +105,47 @@ func main() { } } +// pkgSymbol represents a symbol in a package +type pkgSymbol struct { + pkg string // "net/http" + symbol string // "RoundTripper" +} + 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" + tree *build.Tree + fset *token.FileSet + scope []string + features map[string]bool // set + lastConstType string + curPackageName string + curPackage *ast.Package + prevConstType map[string]string // identifer -> "ideal-int" + packageState map[string]loadState + interfaces map[pkgSymbol]*ast.InterfaceType + 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), + fset: token.NewFileSet(), + features: make(map[string]bool), + packageState: make(map[string]loadState), + interfaces: make(map[pkgSymbol]*ast.InterfaceType), + selectorFullPkg: make(map[string]string), + wantedPkg: make(map[string]bool), } } +// 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. @@ -162,10 +192,34 @@ func (w *Walker) Features() (fs []string) { return } -func (w *Walker) WalkPackage(name, dir string) { - log.Printf("package %s", name) - pop := w.pushScope("pkg " + name) - defer pop() +// 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.tree.SrcDir(), filepath.FromSlash(name)) info, err := build.ScanDir(dir) if err != nil { @@ -183,13 +237,26 @@ func (w *Walker) WalkPackage(name, dir string) { log.Fatalf("error parsing package %s, file %s: %v", name, file, err) } apkg.Files[file] = f + + for _, dep := range fileDeps(f) { + w.WalkPackage(dep) + } } + log.Printf("package %s", name) + pop := w.pushScope("pkg " + name) + defer pop() + w.curPackageName = name w.curPackage = apkg w.prevConstType = map[string]string{} - for name, afile := range apkg.Files { - w.walkFile(filepath.Join(dir, name), afile) + + for _, afile := range apkg.Files { + w.recordTypes(afile) + } + + for _, afile := range apkg.Files { + w.walkFile(afile) } // Now that we're done walking types, vars and consts @@ -229,7 +296,27 @@ func (w *Walker) pushScope(name string) (popFunc func()) { } } -func (w *Walker) walkFile(name string, file *ast.File) { +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 { @@ -237,7 +324,18 @@ func (w *Walker) walkFile(name string, file *ast.File) { case *ast.GenDecl: switch d.Tok { case token.IMPORT: - continue + 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)) @@ -527,12 +625,15 @@ func (w *Walker) nodeDebug(node interface{}) string { 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) @@ -540,7 +641,6 @@ func (w *Walker) walkTypeSpec(ts *ast.TypeSpec) { w.walkInterfaceType(name, t) default: w.emitFeature(fmt.Sprintf("type %s %s", name, w.nodeString(ts.Type))) - //log.Fatalf("unknown typespec %T", ts.Type) } } @@ -582,27 +682,78 @@ func (w *Walker) walkStructType(name string, t *ast.StructType) { } } -func (w *Walker) walkInterfaceType(name string, t *ast.InterfaceType) { - methods := []string{} +// 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 methods for an interface. +// pkg is the complete package name ("net/http") +// iname is the interface name. +func (w *Walker) interfaceMethods(pkg, iname string) (methods []method) { + t, ok := w.interfaces[pkgSymbol{pkg, iname}] + if !ok { + log.Fatalf("failed to find interface %s.%s", pkg, iname) + } - 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) + 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), + }) + } } + 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) + } + methods = append(methods, w.interfaceMethods(pkg, embedded)...) + 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) + } + methods = append(methods, w.interfaceMethods(fpkg, rhs)...) + 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") + for _, m := range w.interfaceMethods(w.curPackageName, name) { + methNames = append(methNames, m.name) + w.emitFeature(fmt.Sprintf("%s%s", m.name, m.sig)) + } pop() - sort.Strings(methods) - if len(methods) == 0 { + 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(methods, ", "))) + w.emitFeature(fmt.Sprintf("type %s interface { %s }", name, strings.Join(methNames, ", "))) } } @@ -691,6 +842,9 @@ func (w *Walker) namelessField(f *ast.Field) *ast.Field { } 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) diff --git a/src/cmd/goapi/goapi_test.go b/src/cmd/goapi/goapi_test.go index 1f23b1d68ab..dbbec46b0d3 100644 --- a/src/cmd/goapi/goapi_test.go +++ b/src/cmd/goapi/goapi_test.go @@ -7,6 +7,7 @@ package main import ( "flag" "fmt" + "go/build" "io/ioutil" "os" "path/filepath" @@ -20,7 +21,7 @@ var ( ) func TestGolden(t *testing.T) { - td, err := os.Open("testdata") + td, err := os.Open("testdata/src/pkg") if err != nil { t.Fatal(err) } @@ -33,9 +34,11 @@ func TestGolden(t *testing.T) { continue } w := NewWalker() - goldenFile := filepath.Join("testdata", fi.Name(), "golden.txt") + w.wantedPkg[fi.Name()] = true - w.WalkPackage(fi.Name(), filepath.Join("testdata", fi.Name())) + w.tree = &build.Tree{Path: "testdata", Goroot: true} + goldenFile := filepath.Join("testdata", "src", "pkg", fi.Name(), "golden.txt") + w.WalkPackage(fi.Name()) if *updateGolden { os.Remove(goldenFile) diff --git a/src/cmd/goapi/testdata/p1/golden.txt b/src/cmd/goapi/testdata/src/pkg/p1/golden.txt similarity index 83% rename from src/cmd/goapi/testdata/p1/golden.txt rename to src/cmd/goapi/testdata/src/pkg/p1/golden.txt index 5b2aff54852..2693240a795 100644 --- a/src/cmd/goapi/testdata/p1/golden.txt +++ b/src/cmd/goapi/testdata/src/pkg/p1/golden.txt @@ -31,11 +31,18 @@ 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 Error interface { Error, Temporary } +pkg p1, type Error interface, Error() string +pkg p1, type Error interface, Temporary() bool +pkg p1, type I interface { Get, GetNamed, Name, PackageTwoMeth, Set } pkg p1, type I interface, Get(string) int64 pkg p1, type I interface, GetNamed(string) int64 +pkg p1, type I interface, Name() string +pkg p1, type I interface, PackageTwoMeth() pkg p1, type I interface, Set(string, int64) pkg p1, type MyInt int +pkg p1, type Namer interface { Name } +pkg p1, type Namer interface, Name() string pkg p1, type S struct pkg p1, type S struct, Public *int pkg p1, type S struct, PublicTime time.Time diff --git a/src/cmd/goapi/testdata/p1/p1.go b/src/cmd/goapi/testdata/src/pkg/p1/p1.go similarity index 92% rename from src/cmd/goapi/testdata/p1/p1.go rename to src/cmd/goapi/testdata/src/pkg/p1/p1.go index 67a0ed9a4f3..3829686e6b2 100644 --- a/src/cmd/goapi/testdata/p1/p1.go +++ b/src/cmd/goapi/testdata/src/pkg/p1/p1.go @@ -1,8 +1,7 @@ -package foo +package p1 import ( - "time" - "url" + ptwo "p2" ) const ( @@ -44,16 +43,27 @@ var X int64 var ( Y int - X I // todo: resolve this to foo.I? probably doesn't matter. + X I ) +type Namer interface { + Name() string +} + type I interface { + Namer + ptwo.Twoer Set(name string, balance int64) Get(string) int64 GetNamed(string) (balance int64) private() } +type Error interface { + error + Temporary() bool +} + func (myInt) privateTypeMethod() {} func (myInt) CapitalMethodUnexportedType() {} diff --git a/src/cmd/goapi/testdata/src/pkg/p2/golden.txt b/src/cmd/goapi/testdata/src/pkg/p2/golden.txt new file mode 100644 index 00000000000..111427b4ab4 --- /dev/null +++ b/src/cmd/goapi/testdata/src/pkg/p2/golden.txt @@ -0,0 +1,2 @@ +pkg p2, type Twoer interface { PackageTwoMeth } +pkg p2, type Twoer interface, PackageTwoMeth() diff --git a/src/cmd/goapi/testdata/src/pkg/p2/p2.go b/src/cmd/goapi/testdata/src/pkg/p2/p2.go new file mode 100644 index 00000000000..695d08da6af --- /dev/null +++ b/src/cmd/goapi/testdata/src/pkg/p2/p2.go @@ -0,0 +1,5 @@ +package p2 + +type Twoer interface { + PackageTwoMeth() +}