go.tools/cmd/godex: a tool to dump export information

Initial implementation. Lots of missing pieces.

Example use:
        godex math
        godex math.Sin
        godex math.Sin math.Cos

LGTM=adonovan
R=adonovan
CC=golang-codereviews
https://golang.org/cl/76890044
diff --git a/cmd/godex/doc.go b/cmd/godex/doc.go
new file mode 100644
index 0000000..3570a2d
--- /dev/null
+++ b/cmd/godex/doc.go
@@ -0,0 +1,38 @@
+// Copyright 2014 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.
+
+// The godex command prints (dumps) exported information of packages
+// or selected package objects.
+//
+// In contrast to godoc, godex extracts this information from compiled
+// object files. Hence the exported data is truly what a compiler will
+// see, at the cost of missing commentary.
+//
+// Usage: godex [flags] {path|qualifiedIdent}
+//
+// Each argument must be a package path, or a qualified identifier.
+//
+// The flags are:
+//
+//	-s=src
+//		only consider packages from src, where src is one of the supported compilers
+//	-v
+//		verbose mode
+//
+// The following sources are supported:
+//
+//	gc
+//		gc-generated object files
+//	gccgo
+//		gccgo-generated object files
+//	gccgo-new
+//		gccgo-generated object files using a condensed format (experimental)
+//	source
+//		(uncompiled) source code (not yet implemented)
+//
+// If no -s argument is provided, godex will try to find a matching source.
+//
+// TODO(gri) expand this documentation
+//
+package main
diff --git a/cmd/godex/gc.go b/cmd/godex/gc.go
new file mode 100644
index 0000000..ef7d450
--- /dev/null
+++ b/cmd/godex/gc.go
@@ -0,0 +1,15 @@
+// Copyright 2014 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.
+
+// This file implements access to gc-generated export data.
+
+package main
+
+import (
+	"code.google.com/p/go.tools/go/gcimporter"
+)
+
+func init() {
+	register("gc", protect(gcimporter.Import))
+}
diff --git a/cmd/godex/gccgo.go b/cmd/godex/gccgo.go
new file mode 100644
index 0000000..8c200e6
--- /dev/null
+++ b/cmd/godex/gccgo.go
@@ -0,0 +1,88 @@
+// Copyright 2014 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.
+
+// This file implements access to gccgo-generated export data.
+
+package main
+
+import (
+	"debug/elf"
+	"fmt"
+	"io"
+	"os"
+
+	"code.google.com/p/go.tools/go/gccgoimporter"
+	"code.google.com/p/go.tools/go/importer"
+	"code.google.com/p/go.tools/go/types"
+)
+
+func init() {
+	// importer for default gccgo
+	var inst gccgoimporter.GccgoInstallation
+	inst.InitFromDriver("gccgo")
+	register("gccgo", protect(inst.GetImporter(nil)))
+
+	// importer for gccgo using condensed export format (experimental)
+	register("gccgo-new", protect(gccgoNewImporter))
+}
+
+func gccgoNewImporter(packages map[string]*types.Package, path string) (*types.Package, error) {
+	reader, closer, err := openGccgoExportFile(path)
+	if err != nil {
+		return nil, err
+	}
+	defer closer.Close()
+
+	// TODO(gri) importer.ImportData takes a []byte instead of an io.Reader;
+	// hence the need to read some amount of data. At the same time we don't
+	// want to read the entire, potentially very large object file. For now,
+	// read 10K. Fix this!
+	var data = make([]byte, 10<<10)
+	n, err := reader.Read(data)
+	if err != nil && err != io.EOF {
+		return nil, err
+	}
+
+	return importer.ImportData(packages, data[:n])
+}
+
+// openGccgoExportFile was copied from gccgoimporter.
+func openGccgoExportFile(fpath string) (reader io.ReadSeeker, closer io.Closer, err error) {
+	f, err := os.Open(fpath)
+	if err != nil {
+		return
+	}
+	defer func() {
+		if err != nil {
+			f.Close()
+		}
+	}()
+	closer = f
+
+	var magic [4]byte
+	_, err = f.ReadAt(magic[:], 0)
+	if err != nil {
+		return
+	}
+
+	if string(magic[:]) == "v1;\n" {
+		// Raw export data.
+		reader = f
+		return
+	}
+
+	ef, err := elf.NewFile(f)
+	if err != nil {
+		return
+	}
+
+	sec := ef.Section(".go_export")
+	if sec == nil {
+		err = fmt.Errorf("%s: .go_export section not found", fpath)
+		return
+	}
+
+	reader = sec.Open()
+	return
+}
diff --git a/cmd/godex/godex.go b/cmd/godex/godex.go
new file mode 100644
index 0000000..21f7552
--- /dev/null
+++ b/cmd/godex/godex.go
@@ -0,0 +1,143 @@
+// Copyright 2014 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 (
+	"errors"
+	"flag"
+	"fmt"
+	"os"
+	"strings"
+
+	"code.google.com/p/go.tools/go/types"
+)
+
+var (
+	source  = flag.String("s", "", "only consider packages from this source")
+	verbose = flag.Bool("v", false, "verbose mode")
+)
+
+var (
+	importFailed = errors.New("import failed")
+	importers    = make(map[string]types.Importer)
+	packages     = make(map[string]*types.Package)
+)
+
+func usage() {
+	fmt.Fprintln(os.Stderr, "usage: godex [flags] {path|qualifiedIdent}")
+	flag.PrintDefaults()
+	os.Exit(2)
+}
+
+func report(msg string) {
+	fmt.Fprintln(os.Stderr, "error: "+msg)
+	os.Exit(2)
+}
+
+func main() {
+	flag.Usage = usage
+	flag.Parse()
+
+	if flag.NArg() == 0 {
+		report("no package name, path, or file provided")
+	}
+
+	imp := tryImport
+	if *source != "" {
+		imp = importers[*source]
+		if imp == nil {
+			report("source must be one of: " + importersList())
+		}
+	}
+
+	for _, arg := range flag.Args() {
+		if *verbose {
+			fmt.Fprintf(os.Stderr, "(processing %s)\n", arg)
+		}
+
+		// determine import path, object name
+		var path, name string
+		elems := strings.Split(arg, ".")
+		switch len(elems) {
+		case 2:
+			name = elems[1]
+			fallthrough
+		case 1:
+			path = elems[0]
+		default:
+			fmt.Fprintf(os.Stderr, "ignoring %q: invalid path or (qualified) identifier\n", arg)
+			continue
+		}
+
+		// import package
+		pkg, err := imp(packages, path)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "ignoring %q: %s\n", path, err)
+			continue
+		}
+
+		// filter objects if needed
+		filter := exportFilter
+		if name != "" {
+			f := filter
+			filter = func(obj types.Object) bool {
+				// TODO(gri) perhaps use regular expression matching here?
+				return f(obj) && obj.Name() == name
+			}
+		}
+
+		// print contents
+		print(os.Stdout, pkg, filter)
+	}
+}
+
+// protect protects an importer imp from panics and returns the protected importer.
+func protect(imp types.Importer) types.Importer {
+	return func(packages map[string]*types.Package, path string) (pkg *types.Package, err error) {
+		defer func() {
+			if recover() != nil {
+				pkg = nil
+				err = importFailed
+			}
+		}()
+		return imp(packages, path)
+	}
+}
+
+func tryImport(packages map[string]*types.Package, path string) (pkg *types.Package, err error) {
+	for source, imp := range importers {
+		if *verbose {
+			fmt.Fprintf(os.Stderr, "(trying as %s)\n", source)
+		}
+		pkg, err = imp(packages, path)
+		if err == nil {
+			break
+		}
+	}
+	return
+}
+
+func register(source string, imp types.Importer) {
+	if _, ok := importers[source]; ok {
+		panic(source + " importer already registered")
+	}
+	importers[source] = imp
+}
+
+func importersList() string {
+	var s string
+	for n := range importers {
+		if len(s) == 0 {
+			s = n
+		} else {
+			s = s + ", " + n
+		}
+	}
+	return s
+}
+
+func exportFilter(obj types.Object) bool {
+	return obj.Exported()
+}
diff --git a/cmd/godex/print.go b/cmd/godex/print.go
new file mode 100644
index 0000000..438b3f7
--- /dev/null
+++ b/cmd/godex/print.go
@@ -0,0 +1,155 @@
+// Copyright 2014 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 (
+	"bytes"
+	"fmt"
+	"io"
+
+	"code.google.com/p/go.tools/go/types"
+)
+
+// TODO(gri) handle indentation
+// TODO(gri) filter unexported fields of struct types?
+// TODO(gri) use tabwriter for alignment?
+
+func print(w io.Writer, pkg *types.Package, filter func(types.Object) bool) {
+	var p printer
+	p.pkg = pkg
+	p.printPackage(pkg, filter)
+	io.Copy(w, &p.buf)
+}
+
+type printer struct {
+	pkg    *types.Package
+	buf    bytes.Buffer
+	indent int
+}
+
+func (p *printer) print(s string) {
+	p.buf.WriteString(s)
+}
+
+func (p *printer) printf(format string, args ...interface{}) {
+	fmt.Fprintf(&p.buf, format, args...)
+}
+
+func (p *printer) printPackage(pkg *types.Package, filter func(types.Object) bool) {
+	// collect objects by kind
+	var (
+		consts []*types.Const
+		typez  []*types.TypeName // types without methods
+		typem  []*types.TypeName // types with methods
+		vars   []*types.Var
+		funcs  []*types.Func
+	)
+	scope := pkg.Scope()
+	for _, name := range scope.Names() {
+		obj := scope.Lookup(name)
+		if !filter(obj) {
+			continue
+		}
+		switch obj := obj.(type) {
+		case *types.Const:
+			consts = append(consts, obj)
+		case *types.TypeName:
+			if obj.Type().(*types.Named).NumMethods() > 0 {
+				typem = append(typem, obj)
+			} else {
+				typez = append(typez, obj)
+			}
+		case *types.Var:
+			vars = append(vars, obj)
+		case *types.Func:
+			funcs = append(funcs, obj)
+		}
+	}
+
+	p.printf("package %s\n\n", pkg.Name())
+
+	if len(consts) > 0 {
+		p.print("const (\n")
+		for _, obj := range consts {
+			p.printObj(obj)
+			p.print("\n")
+		}
+		p.print(")\n\n")
+	}
+
+	if len(vars) > 0 {
+		p.print("var (\n")
+		for _, obj := range vars {
+			p.printObj(obj)
+			p.print("\n")
+		}
+		p.print(")\n\n")
+	}
+
+	if len(typez) > 0 {
+		p.print("type (\n")
+		for _, obj := range typez {
+			p.printf("\t%s ", obj.Name())
+			types.WriteType(&p.buf, p.pkg, obj.Type().Underlying())
+			p.print("\n")
+		}
+		p.print(")\n\n")
+	}
+
+	for _, obj := range typem {
+		p.printf("type %s ", obj.Name())
+		typ := obj.Type().(*types.Named)
+		types.WriteType(&p.buf, p.pkg, typ.Underlying())
+		p.print("\n")
+		for i, n := 0, typ.NumMethods(); i < n; i++ {
+			p.printFunc(typ.Method(i))
+			p.print("\n")
+		}
+		p.print("\n")
+	}
+
+	for _, obj := range funcs {
+		p.printFunc(obj)
+		p.print("\n")
+	}
+
+	p.print("\n")
+}
+
+func (p *printer) printObj(obj types.Object) {
+	p.printf("\t %s", obj.Name())
+	// don't write untyped types (for constants)
+	if typ := obj.Type(); typed(typ) {
+		p.print(" ")
+		types.WriteType(&p.buf, p.pkg, typ)
+	}
+	// write constant value
+	if obj, ok := obj.(*types.Const); ok {
+		p.printf(" = %s", obj.Val())
+	}
+}
+
+func (p *printer) printFunc(obj *types.Func) {
+	p.print("func ")
+	sig := obj.Type().(*types.Signature)
+	if recv := sig.Recv(); recv != nil {
+		p.print("(")
+		if name := recv.Name(); name != "" {
+			p.print(name)
+			p.print(" ")
+		}
+		types.WriteType(&p.buf, p.pkg, recv.Type())
+		p.print(") ")
+	}
+	p.print(obj.Name())
+	types.WriteSignature(&p.buf, p.pkg, sig)
+}
+
+func typed(typ types.Type) bool {
+	if t, ok := typ.Underlying().(*types.Basic); ok {
+		return t.Info()&types.IsUntyped == 0
+	}
+	return true
+}
diff --git a/cmd/godex/source.go b/cmd/godex/source.go
new file mode 100644
index 0000000..0c263d5
--- /dev/null
+++ b/cmd/godex/source.go
@@ -0,0 +1,19 @@
+// Copyright 2014 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.
+
+// This file implements access to export data from source.
+
+package main
+
+import (
+	"code.google.com/p/go.tools/go/types"
+)
+
+func init() {
+	register("source", protect(sourceImporter))
+}
+
+func sourceImporter(packages map[string]*types.Package, path string) (*types.Package, error) {
+	panic("unimplemented")
+}