383 lines
10 KiB
Go
383 lines
10 KiB
Go
// Copyright 2013 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 importer
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/tools/go/gcimporter"
|
|
"golang.org/x/tools/go/types"
|
|
)
|
|
|
|
var fset = token.NewFileSet()
|
|
|
|
var tests = []string{
|
|
`package p`,
|
|
|
|
// consts
|
|
`package p; const X = true`,
|
|
`package p; const X, y, Z = true, false, 0 != 0`,
|
|
`package p; const ( A float32 = 1<<iota; B; C; D)`,
|
|
`package p; const X = "foo"`,
|
|
`package p; const X string = "foo"`,
|
|
`package p; const X = 0`,
|
|
`package p; const X = -42`,
|
|
`package p; const X = 3.14159265`,
|
|
`package p; const X = -1e-10`,
|
|
`package p; const X = 1.2 + 2.3i`,
|
|
`package p; const X = -1i`,
|
|
`package p; import "math"; const Pi = math.Pi`,
|
|
`package p; import m "math"; const Pi = m.Pi`,
|
|
|
|
// types
|
|
`package p; type T int`,
|
|
`package p; type T [10]int`,
|
|
`package p; type T []int`,
|
|
`package p; type T struct{}`,
|
|
`package p; type T struct{x int}`,
|
|
`package p; type T *int`,
|
|
`package p; type T func()`,
|
|
`package p; type T *T`,
|
|
`package p; type T interface{}`,
|
|
`package p; type T interface{ foo() }`,
|
|
`package p; type T interface{ m() T }`,
|
|
// TODO(gri) disabled for now - import/export works but
|
|
// types.Type.String() used in the test cannot handle cases
|
|
// like this yet
|
|
// `package p; type T interface{ m() interface{T} }`,
|
|
`package p; type T map[string]bool`,
|
|
`package p; type T chan int`,
|
|
`package p; type T <-chan complex64`,
|
|
`package p; type T chan<- map[int]string`,
|
|
// test case for issue 8177
|
|
`package p; type T1 interface { F(T2) }; type T2 interface { T1 }`,
|
|
|
|
// vars
|
|
`package p; var X int`,
|
|
`package p; var X, Y, Z struct{f int "tag"}`,
|
|
|
|
// funcs
|
|
`package p; func F()`,
|
|
`package p; func F(x int, y struct{}) bool`,
|
|
`package p; type T int; func (*T) F(x int, y struct{}) T`,
|
|
|
|
// selected special cases
|
|
`package p; type T int`,
|
|
`package p; type T uint8`,
|
|
`package p; type T byte`,
|
|
`package p; type T error`,
|
|
`package p; import "net/http"; type T http.Client`,
|
|
`package p; import "net/http"; type ( T1 http.Client; T2 struct { http.Client } )`,
|
|
`package p; import "unsafe"; type ( T1 unsafe.Pointer; T2 unsafe.Pointer )`,
|
|
`package p; import "unsafe"; type T struct { p unsafe.Pointer }`,
|
|
}
|
|
|
|
func TestImportSrc(t *testing.T) {
|
|
for _, src := range tests {
|
|
pkg, err := pkgForSource(src)
|
|
if err != nil {
|
|
t.Errorf("typecheck failed: %s", err)
|
|
continue
|
|
}
|
|
testExportImport(t, pkg, "")
|
|
}
|
|
}
|
|
|
|
func TestImportStdLib(t *testing.T) {
|
|
start := time.Now()
|
|
|
|
libs, err := stdLibs()
|
|
if err != nil {
|
|
t.Fatalf("could not compute list of std libraries: %s", err)
|
|
}
|
|
if len(libs) < 100 {
|
|
t.Fatalf("only %d std libraries found - something's not right", len(libs))
|
|
}
|
|
|
|
// make sure printed go/types types and gc-imported types
|
|
// can be compared reasonably well
|
|
types.GcCompatibilityMode = true
|
|
|
|
var totSize, totGcSize int
|
|
for _, lib := range libs {
|
|
// limit run time for short tests
|
|
if testing.Short() && time.Since(start) >= 750*time.Millisecond {
|
|
return
|
|
}
|
|
|
|
pkg, err := pkgForPath(lib)
|
|
switch err := err.(type) {
|
|
case nil:
|
|
// ok
|
|
case *build.NoGoError:
|
|
// no Go files - ignore
|
|
continue
|
|
default:
|
|
t.Errorf("typecheck failed: %s", err)
|
|
continue
|
|
}
|
|
|
|
size, gcsize := testExportImport(t, pkg, lib)
|
|
if gcsize == 0 {
|
|
// if gc import didn't happen, assume same size
|
|
// (and avoid division by zero below)
|
|
gcsize = size
|
|
}
|
|
|
|
if testing.Verbose() {
|
|
fmt.Printf("%s\t%d\t%d\t%d%%\n", lib, size, gcsize, int(float64(size)*100/float64(gcsize)))
|
|
}
|
|
totSize += size
|
|
totGcSize += gcsize
|
|
}
|
|
|
|
if testing.Verbose() {
|
|
fmt.Printf("\n%d\t%d\t%d%%\n", totSize, totGcSize, int(float64(totSize)*100/float64(totGcSize)))
|
|
}
|
|
|
|
types.GcCompatibilityMode = false
|
|
}
|
|
|
|
func testExportImport(t *testing.T, pkg0 *types.Package, path string) (size, gcsize int) {
|
|
data := ExportData(pkg0)
|
|
size = len(data)
|
|
|
|
imports := make(map[string]*types.Package)
|
|
n, pkg1, err := ImportData(imports, data)
|
|
if err != nil {
|
|
t.Errorf("package %s: import failed: %s", pkg0.Name(), err)
|
|
return
|
|
}
|
|
if n != size {
|
|
t.Errorf("package %s: not all input data consumed", pkg0.Name())
|
|
return
|
|
}
|
|
|
|
s0 := pkgString(pkg0)
|
|
s1 := pkgString(pkg1)
|
|
if s1 != s0 {
|
|
t.Errorf("package %s: \nimport got:\n%s\nwant:\n%s\n", pkg0.Name(), s1, s0)
|
|
}
|
|
|
|
// If we have a standard library, compare also against the gcimported package.
|
|
if path == "" {
|
|
return // not std library
|
|
}
|
|
|
|
gcdata, err := gcExportData(path)
|
|
if err != nil {
|
|
if pkg0.Name() == "main" {
|
|
return // no export data present for main package
|
|
}
|
|
t.Errorf("package %s: couldn't get export data: %s", pkg0.Name(), err)
|
|
}
|
|
gcsize = len(gcdata)
|
|
|
|
imports = make(map[string]*types.Package)
|
|
pkg2, err := gcImportData(imports, gcdata, path)
|
|
if err != nil {
|
|
t.Errorf("package %s: gcimport failed: %s", pkg0.Name(), err)
|
|
return
|
|
}
|
|
|
|
s2 := pkgString(pkg2)
|
|
if s2 != s0 {
|
|
t.Errorf("package %s: \ngcimport got:\n%s\nwant:\n%s\n", pkg0.Name(), s2, s0)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func pkgForSource(src string) (*types.Package, error) {
|
|
f, err := parser.ParseFile(fset, "", src, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return typecheck("import-test", f)
|
|
}
|
|
|
|
func pkgForPath(path string) (*types.Package, error) {
|
|
// collect filenames
|
|
ctxt := build.Default
|
|
pkginfo, err := ctxt.Import(path, "", 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
filenames := append(pkginfo.GoFiles, pkginfo.CgoFiles...)
|
|
|
|
// parse files
|
|
files := make([]*ast.File, len(filenames))
|
|
for i, filename := range filenames {
|
|
var err error
|
|
files[i], err = parser.ParseFile(fset, filepath.Join(pkginfo.Dir, filename), nil, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return typecheck(path, files...)
|
|
}
|
|
|
|
var defaultConf = types.Config{
|
|
// we only care about exports and thus can ignore function bodies
|
|
IgnoreFuncBodies: true,
|
|
// work around C imports if possible
|
|
FakeImportC: true,
|
|
// strconv exports IntSize as a constant. The type-checker must
|
|
// use the same word size otherwise the result of the type-checker
|
|
// and gc imports is different. We don't care about alignment
|
|
// since none of the tests have exported constants depending
|
|
// on alignment (see also issue 8366).
|
|
Sizes: &types.StdSizes{WordSize: strconv.IntSize / 8, MaxAlign: 8},
|
|
}
|
|
|
|
func typecheck(path string, files ...*ast.File) (*types.Package, error) {
|
|
return defaultConf.Check(path, fset, files, nil)
|
|
}
|
|
|
|
// pkgString returns a string representation of a package's exported interface.
|
|
func pkgString(pkg *types.Package) string {
|
|
var buf bytes.Buffer
|
|
|
|
fmt.Fprintf(&buf, "package %s\n", pkg.Name())
|
|
|
|
scope := pkg.Scope()
|
|
for _, name := range scope.Names() {
|
|
if exported(name) {
|
|
obj := scope.Lookup(name)
|
|
buf.WriteString(obj.String())
|
|
|
|
switch obj := obj.(type) {
|
|
case *types.Const:
|
|
// For now only print constant values if they are not float
|
|
// or complex. This permits comparing go/types results with
|
|
// gc-generated gcimported package interfaces.
|
|
info := obj.Type().Underlying().(*types.Basic).Info()
|
|
if info&types.IsFloat == 0 && info&types.IsComplex == 0 {
|
|
fmt.Fprintf(&buf, " = %s", obj.Val())
|
|
}
|
|
|
|
case *types.TypeName:
|
|
// Print associated methods.
|
|
// Basic types (e.g., unsafe.Pointer) have *types.Basic
|
|
// type rather than *types.Named; so we need to check.
|
|
if typ, _ := obj.Type().(*types.Named); typ != nil {
|
|
if n := typ.NumMethods(); n > 0 {
|
|
// Sort methods by name so that we get the
|
|
// same order independent of whether the
|
|
// methods got imported or coming directly
|
|
// for the source.
|
|
// TODO(gri) This should probably be done
|
|
// in go/types.
|
|
list := make([]*types.Func, n)
|
|
for i := 0; i < n; i++ {
|
|
list[i] = typ.Method(i)
|
|
}
|
|
sort.Sort(byName(list))
|
|
|
|
buf.WriteString("\nmethods (\n")
|
|
for _, m := range list {
|
|
fmt.Fprintf(&buf, "\t%s\n", m)
|
|
}
|
|
buf.WriteString(")")
|
|
}
|
|
}
|
|
}
|
|
buf.WriteByte('\n')
|
|
}
|
|
}
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
var stdLibRoot = filepath.Join(runtime.GOROOT(), "src") + string(filepath.Separator)
|
|
|
|
// The following std libraries are excluded from the stdLibs list.
|
|
var excluded = map[string]bool{
|
|
"builtin": true, // contains type declarations with cycles
|
|
"unsafe": true, // contains fake declarations
|
|
}
|
|
|
|
// stdLibs returns the list of standard library package paths.
|
|
func stdLibs() (list []string, err error) {
|
|
err = filepath.Walk(stdLibRoot, func(path string, info os.FileInfo, err error) error {
|
|
if err == nil && info.IsDir() {
|
|
// testdata directories don't contain importable libraries
|
|
if info.Name() == "testdata" {
|
|
return filepath.SkipDir
|
|
}
|
|
pkgPath := path[len(stdLibRoot):] // remove stdLibRoot
|
|
if len(pkgPath) > 0 && !excluded[pkgPath] {
|
|
list = append(list, pkgPath)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return
|
|
}
|
|
|
|
type byName []*types.Func
|
|
|
|
func (a byName) Len() int { return len(a) }
|
|
func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
func (a byName) Less(i, j int) bool { return a[i].Name() < a[j].Name() }
|
|
|
|
// gcExportData returns the gc-generated export data for the given path.
|
|
// It is based on a trimmed-down version of gcimporter.Import which does
|
|
// not do the actual import, does not handle package unsafe, and assumes
|
|
// that path is a correct standard library package path (no canonicalization,
|
|
// or handling of local import paths).
|
|
func gcExportData(path string) ([]byte, error) {
|
|
filename, id := gcimporter.FindPkg(path, "")
|
|
if filename == "" {
|
|
return nil, fmt.Errorf("can't find import: %s", path)
|
|
}
|
|
if id != path {
|
|
panic("path should be canonicalized")
|
|
}
|
|
|
|
f, err := os.Open(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
buf := bufio.NewReader(f)
|
|
if err = gcimporter.FindExportData(buf); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var data []byte
|
|
for {
|
|
line, err := buf.ReadBytes('\n')
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data = append(data, line...)
|
|
// export data ends in "$$\n"
|
|
if len(line) == 3 && line[0] == '$' && line[1] == '$' {
|
|
return data, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func gcImportData(imports map[string]*types.Package, data []byte, path string) (*types.Package, error) {
|
|
filename := fmt.Sprintf("<filename for %s>", path) // so we have a decent error message if necessary
|
|
return gcimporter.ImportData(imports, filename, path, bufio.NewReader(bytes.NewBuffer(data)))
|
|
}
|