| // 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" |
| |
| "llvm.org/llgo/third_party/gotools/go/gcimporter" |
| "llvm.org/llgo/third_party/gotools/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))) |
| } |