go.tools/go/loader: use the build.Context's I/O hooks, if any.
Also, add loader.Config.DisplayPath hook, which allows the
filename returned by build.Context.Import() to be transformed
prior to attaching to the AST. This allows a virtual file
system to be used without leaking into the user interface.
Eliminate parsePackageFiles hook; I don't think we need it any
more. The test that was using it has been rewritten to use
the build.Context hooks.
LGTM=gri
R=gri, crawshaw
CC=daniel.morsing, golang-codereviews, rsc
https://golang.org/cl/75520046
diff --git a/go/loader/backdoor_test.go b/go/loader/backdoor_test.go
deleted file mode 100644
index 918546a..0000000
--- a/go/loader/backdoor_test.go
+++ /dev/null
@@ -1,17 +0,0 @@
-// 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 loader
-
-import (
- "go/ast"
- "go/build"
- "go/token"
-)
-
-// PackageLocatorFunc exposes the address of parsePackageFiles to tests.
-// This is a temporary hack until we expose a proper PackageLocator interface.
-func PackageLocatorFunc() *func(ctxt *build.Context, fset *token.FileSet, path string, which rune) ([]*ast.File, error) {
- return &parsePackageFiles
-}
diff --git a/go/loader/importer_test.go b/go/loader/importer_test.go
index be35825..f984f93 100644
--- a/go/loader/importer_test.go
+++ b/go/loader/importer_test.go
@@ -5,12 +5,14 @@
package loader_test
import (
+ "bytes"
"fmt"
- "go/ast"
"go/build"
- "go/token"
+ "io"
+ "os"
"sort"
"testing"
+ "time"
"code.google.com/p/go.tools/go/loader"
)
@@ -107,52 +109,61 @@
}
}
+type nopCloser struct{ *bytes.Buffer }
+
+func (nopCloser) Close() error { return nil }
+
+type fakeFileInfo struct{}
+
+func (fakeFileInfo) Name() string { return "x.go" }
+func (fakeFileInfo) Sys() interface{} { return nil }
+func (fakeFileInfo) ModTime() time.Time { return time.Time{} }
+func (fakeFileInfo) IsDir() bool { return false }
+func (fakeFileInfo) Size() int64 { return 0 }
+func (fakeFileInfo) Mode() os.FileMode { return 0644 }
+
+var justXgo = [1]os.FileInfo{fakeFileInfo{}} // ["x.go"]
+
func TestTransitivelyErrorFreeFlag(t *testing.T) {
conf := loader.Config{
AllowTypeErrors: true,
SourceImports: true,
}
- conf.Import("a")
- // Fake the following packages:
+ // Create an minimal custom build.Context
+ // that fakes the following packages:
//
// a --> b --> c! c has a TypeError
// \ d and e are transitively error free.
// e --> d
-
- // Temporary hack until we expose a principled PackageLocator.
- pfn := loader.PackageLocatorFunc()
- saved := *pfn
- *pfn = func(_ *build.Context, fset *token.FileSet, path string, which rune) (files []*ast.File, err error) {
- if which != 'g' {
- return nil, nil // no test/xtest files
- }
- var contents string
- switch path {
- case "a":
- contents = `package a; import (_ "b"; _ "e")`
- case "b":
- contents = `package b; import _ "c"`
- case "c":
- contents = `package c; func f() { _ = int(false) }` // type error within function body
- case "d":
- contents = `package d;`
- case "e":
- contents = `package e; import _ "d"`
- default:
- return nil, fmt.Errorf("no such package %q", path)
- }
- f, err := conf.ParseFile(fmt.Sprintf("%s/x.go", path), contents, 0)
- return []*ast.File{f}, err
+ //
+ // Each package [a-e] consists of one file, x.go.
+ pkgs := map[string]string{
+ "a": `package a; import (_ "b"; _ "e")`,
+ "b": `package b; import _ "c"`,
+ "c": `package c; func f() { _ = int(false) }`, // type error within function body
+ "d": `package d;`,
+ "e": `package e; import _ "d"`,
}
- defer func() { *pfn = saved }()
+ ctxt := build.Default // copy
+ ctxt.GOROOT = "/go"
+ ctxt.GOPATH = ""
+ ctxt.IsDir = func(path string) bool { return true }
+ ctxt.ReadDir = func(dir string) ([]os.FileInfo, error) { return justXgo[:], nil }
+ ctxt.OpenFile = func(path string) (io.ReadCloser, error) {
+ path = path[len("/go/src/pkg/"):]
+ return nopCloser{bytes.NewBufferString(pkgs[path[0:1]])}, nil
+ }
+ conf.Build = &ctxt
+
+ conf.Import("a")
prog, err := conf.Load()
if err != nil {
t.Errorf("Load failed: %s", err)
}
if prog == nil {
- t.Fatalf("Load returnd nil *Program")
+ t.Fatalf("Load returned nil *Program")
}
for pkg, info := range prog.AllPackages {
diff --git a/go/loader/loader.go b/go/loader/loader.go
index c3db31f..970fd6d 100644
--- a/go/loader/loader.go
+++ b/go/loader/loader.go
@@ -184,6 +184,12 @@
// Otherwise &build.Default is used.
Build *build.Context
+ // If DisplayPath is non-nil, it is used to transform each
+ // file name obtained from Build.Import(). This can be used
+ // to prevent a virtualized build.Config's file names from
+ // leaking into the user interface.
+ DisplayPath func(path string) string
+
// If AllowTypeErrors is true, Load will return a Program even
// if some of the its packages contained type errors; such
// errors are accessible via PackageInfo.TypeError.
@@ -334,7 +340,7 @@
// conf.CreatePkgs.
//
func (conf *Config) CreateFromFilenames(path string, filenames ...string) error {
- files, err := parseFiles(conf.fset(), ".", filenames...)
+ files, err := parseFiles(conf.fset(), conf.build(), nil, ".", filenames...)
if err != nil {
return err
}
@@ -373,7 +379,7 @@
conf.Import(path)
// Load the external test package.
- xtestFiles, err := parsePackageFiles(conf.build(), conf.fset(), path, 'x')
+ xtestFiles, err := conf.parsePackageFiles(path, 'x')
if err != nil {
return err
}
@@ -496,7 +502,7 @@
ii := imp.imported[path]
// Find and create the actual package.
- files, err := parsePackageFiles(imp.conf.build(), imp.conf.fset(), path, 't')
+ files, err := imp.conf.parsePackageFiles(path, 't')
// Prefer the earlier error, if any.
if err != nil && ii.err == nil {
ii.err = err // e.g. parse error.
@@ -598,6 +604,43 @@
return &build.Default
}
+// parsePackageFiles enumerates the files belonging to package path,
+// then loads, parses and returns them.
+//
+// 'which' indicates which files to include:
+// 'g': include non-test *.go source files (GoFiles)
+// 't': include in-package *_test.go source files (TestGoFiles)
+// 'x': include external *_test.go source files. (XTestGoFiles)
+//
+func (conf *Config) parsePackageFiles(path string, which rune) ([]*ast.File, error) {
+ // Set the "!cgo" go/build tag, preferring (dummy) Go to
+ // native C implementations of net.cgoLookupHost et al.
+ ctxt := *conf.build() // copy
+ ctxt.CgoEnabled = false
+
+ // Import(srcDir="") disables local imports, e.g. import "./foo".
+ bp, err := ctxt.Import(path, "", 0)
+ if _, ok := err.(*build.NoGoError); ok {
+ return nil, nil // empty directory
+ }
+ if err != nil {
+ return nil, err // import failed
+ }
+
+ var filenames []string
+ switch which {
+ case 'g':
+ filenames = bp.GoFiles
+ case 't':
+ filenames = bp.TestGoFiles
+ case 'x':
+ filenames = bp.XTestGoFiles
+ default:
+ panic(which)
+ }
+ return parseFiles(conf.fset(), &ctxt, conf.DisplayPath, bp.Dir, filenames...)
+}
+
// doImport imports the package denoted by path.
// It implements the types.Importer signature.
//
@@ -677,7 +720,7 @@
// located by go/build.
//
func (imp *importer) importFromSource(path string) (*PackageInfo, error) {
- files, err := parsePackageFiles(imp.conf.build(), imp.conf.fset(), path, 'g')
+ files, err := imp.conf.parsePackageFiles(path, 'g')
if err != nil {
return nil, err
}
diff --git a/go/loader/util.go b/go/loader/util.go
index 14bde95..a772f02 100644
--- a/go/loader/util.go
+++ b/go/loader/util.go
@@ -9,68 +9,54 @@
"go/build"
"go/parser"
"go/token"
+ "io"
+ "os"
"path/filepath"
"sync"
)
-// parsePackageFiles enumerates the files belonging to package path,
-// then loads, parses and returns them.
-//
-// 'which' is a list of flags indicating which files to include:
-// 'g': include non-test *.go source files (GoFiles)
-// 't': include in-package *_test.go source files (TestGoFiles)
-// 'x': include external *_test.go source files. (XTestGoFiles)
-//
-// This function is stored in a var as a concession to testing.
-// TODO(adonovan): we plan to replace this hook with an exposed
-// "PackageLocator" interface for use proprietary build sytems that
-// are incompatible with "go test", and also for testing.
-//
-var parsePackageFiles = func(ctxt *build.Context, fset *token.FileSet, path string, which rune) ([]*ast.File, error) {
- // Set the "!cgo" go/build tag, preferring (dummy) Go to
- // native C implementations of net.cgoLookupHost et al.
- ctxt2 := *ctxt
- ctxt2.CgoEnabled = false
-
- // Import(srcDir="") disables local imports, e.g. import "./foo".
- bp, err := ctxt2.Import(path, "", 0)
- if _, ok := err.(*build.NoGoError); ok {
- return nil, nil // empty directory
- }
- if err != nil {
- return nil, err // import failed
- }
-
- var filenames []string
- switch which {
- case 'g':
- filenames = bp.GoFiles
- case 't':
- filenames = bp.TestGoFiles
- case 'x':
- filenames = bp.XTestGoFiles
- default:
- panic(which)
- }
- return parseFiles(fset, bp.Dir, filenames...)
-}
-
// parseFiles parses the Go source files files within directory dir
// and returns their ASTs, or the first parse error if any.
//
-func parseFiles(fset *token.FileSet, dir string, files ...string) ([]*ast.File, error) {
+// I/O is done via ctxt, which may specify a virtual file system.
+// displayPath is used to transform the filenames attached to the ASTs.
+//
+func parseFiles(fset *token.FileSet, ctxt *build.Context, displayPath func(string) string, dir string, files ...string) ([]*ast.File, error) {
+ if displayPath == nil {
+ displayPath = func(path string) string { return path }
+ }
+ isAbs := filepath.IsAbs
+ if ctxt.IsAbsPath != nil {
+ isAbs = ctxt.IsAbsPath
+ }
+ joinPath := filepath.Join
+ if ctxt.JoinPath != nil {
+ joinPath = ctxt.JoinPath
+ }
var wg sync.WaitGroup
n := len(files)
- parsed := make([]*ast.File, n, n)
- errors := make([]error, n, n)
+ parsed := make([]*ast.File, n)
+ errors := make([]error, n)
for i, file := range files {
- if !filepath.IsAbs(file) {
- file = filepath.Join(dir, file)
+ if !isAbs(file) {
+ file = joinPath(dir, file)
}
wg.Add(1)
go func(i int, file string) {
- parsed[i], errors[i] = parser.ParseFile(fset, file, nil, 0)
- wg.Done()
+ defer wg.Done()
+ var rd io.ReadCloser
+ var err error
+ if ctxt.OpenFile != nil {
+ rd, err = ctxt.OpenFile(file)
+ } else {
+ rd, err = os.Open(file)
+ }
+ defer rd.Close()
+ if err != nil {
+ errors[i] = err
+ return
+ }
+ parsed[i], errors[i] = parser.ParseFile(fset, displayPath(file), rd, 0)
}(i, file)
}
wg.Wait()