cue/load: allow file overlays
This will avoid the need to write intermediate
files if these are not needed in the final result,
such as in generation pipelines.
Change-Id: I43d417ebcfa5c9b24704a441bf05dc5a79d6e4dd
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2369
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/load/config.go b/cue/load/config.go
index b2b7542..46ff754 100644
--- a/cue/load/config.go
+++ b/cue/load/config.go
@@ -21,6 +21,7 @@
"cuelang.org/go/cue/build"
"cuelang.org/go/cue/errors"
+ "cuelang.org/go/cue/token"
)
const (
@@ -113,6 +114,16 @@
// This is mostly used for bootstrapping.
StdRoot string
+ // Overlay provides a mapping of absolute file paths to file contents.
+ // If the file with the given path already exists, the parser will use the
+ // alternative file contents provided by the map.
+ //
+ // Overlays provide incomplete support for when a given file doesn't
+ // already exist on disk. See the package doc above for more details.
+ //
+ // If the value must be of type string, []byte, io.Reader, or *ast.File.
+ Overlay map[string]Source
+
fileSystem
}
@@ -122,10 +133,19 @@
return i
}
-func (c Config) newErrInstance(m *match, path string, err errors.Error) *build.Instance {
+func (c Config) newErrInstance(m *match, path string, err error) *build.Instance {
i := c.Context.NewInstance(path, nil)
i.DisplayPath = path
- i.ReportError(err)
+ switch x := err.(type) {
+ case errors.Error:
+ i.ReportError(x)
+ case errors.List:
+ for _, e := range x {
+ i.ReportError(e)
+ }
+ default:
+ i.ReportError(errors.Wrapf(err, token.NoPos, "instance"))
+ }
return i
}
@@ -144,11 +164,17 @@
}
}
+ // TODO: we could populate this already with absolute file paths,
+ // but relative paths cannot be added. Consider what is reasonable.
+ if err := c.fileSystem.init(&c); err != nil {
+ return nil, err
+ }
+
// TODO: determine root on a package basis. Maybe we even need a
// pkgname.cue.mod
// Look to see if there is a cue.mod.
if c.modRoot == "" {
- abs, err := findRoot(c.Dir)
+ abs, err := c.findRoot(c.Dir)
if err != nil {
// Not using modules: only consider the current directory.
c.modRoot = c.Dir
@@ -165,20 +191,21 @@
if c.cache == "" {
c.cache = filepath.Join(home(), defaultDir)
- // os.MkdirAll(c.Cache, 0755) // TODO: tools task
}
return &c, nil
}
-func findRoot(dir string) (string, error) {
+func (c Config) findRoot(dir string) (string, error) {
+ fs := &c.fileSystem
+
absDir, err := filepath.Abs(dir)
if err != nil {
return "", err
}
abs := absDir
for {
- info, err := os.Stat(filepath.Join(abs, modFile))
+ info, err := fs.stat(filepath.Join(abs, modFile))
if err == nil && !info.IsDir() {
return abs, nil
}
@@ -190,7 +217,7 @@
}
abs = absDir
for {
- info, err := os.Stat(filepath.Join(abs, pkgDir))
+ info, err := fs.stat(filepath.Join(abs, pkgDir))
if err == nil && info.IsDir() {
return abs, nil
}
diff --git a/cue/load/fs.go b/cue/load/fs.go
index 0898e5f..abf830f 100644
--- a/cue/load/fs.go
+++ b/cue/load/fs.go
@@ -15,97 +15,126 @@
package load
import (
+ "bytes"
+ "go/ast"
"io"
"io/ioutil"
"os"
"path/filepath"
+ "sort"
"strings"
+ "time"
+
+ "cuelang.org/go/cue/errors"
+ "cuelang.org/go/cue/token"
)
-// TODO: remove this file if we know we don't need it.
+type overlayFile struct {
+ basename string
+ contents []byte
+ file *ast.File
+ modtime time.Time
+ isDir bool
+}
+
+func (f *overlayFile) Name() string { return f.basename }
+func (f *overlayFile) Size() int64 { return int64(len(f.contents)) }
+func (f *overlayFile) Mode() os.FileMode { return 0644 }
+func (f *overlayFile) ModTime() time.Time { return f.modtime }
+func (f *overlayFile) IsDir() bool { return f.isDir }
+func (f *overlayFile) Sys() interface{} { return nil }
// A fileSystem specifies the supporting context for a build.
type fileSystem struct {
- // By default, Import uses the operating system's file system calls
- // to read directories and files. To read from other sources,
- // callers can set the following functions. They all have default
- // behaviors that use the local file system, so clients need only set
- // the functions whose behaviors they wish to change.
-
- // JoinPath joins the sequence of path fragments into a single path.
- // If JoinPath is nil, Import uses filepath.Join.
- JoinPath func(elem ...string) string
-
- // SplitPathList splits the path list into a slice of individual paths.
- // If SplitPathList is nil, Import uses filepath.SplitList.
- SplitPathList func(list string) []string
-
- // IsAbsPath reports whether path is an absolute path.
- // If IsAbsPath is nil, Import uses filepath.IsAbs.
- IsAbsPath func(path string) bool
-
- // IsDir reports whether the path names a directory.
- // If IsDir is nil, Import calls os.Stat and uses the result's IsDir method.
- IsDir func(path string) bool
-
- // HasSubdir reports whether dir is a subdirectory of
- // (perhaps multiple levels below) root.
- // If so, HasSubdir sets rel to a slash-separated path that
- // can be joined to root to produce a path equivalent to dir.
- // If HasSubdir is nil, Import uses an implementation built on
- // filepath.EvalSymlinks.
- HasSubdir func(root, dir string) (rel string, ok bool)
-
- // ReadDir returns a slice of os.FileInfo, sorted by Name,
- // describing the content of the named directory.
- // If ReadDir is nil, Import uses ioutil.ReadDir.
- ReadDir func(dir string) ([]os.FileInfo, error)
-
- // OpenFile opens a file (not a directory) for reading.
- // If OpenFile is nil, Import uses os.Open.
- OpenFile func(path string) (io.ReadCloser, error)
+ overlayDirs map[string]map[string]*overlayFile
+ cwd string
}
-// JoinPath calls ctxt.JoinPath (if not nil) or else filepath.Join.
-func (ctxt *fileSystem) joinPath(elem ...string) string {
- if f := ctxt.JoinPath; f != nil {
- return f(elem...)
+func (fs *fileSystem) getDir(dir string, create bool) map[string]*overlayFile {
+ dir = filepath.Clean(dir)
+ m, ok := fs.overlayDirs[dir]
+ if !ok {
+ m = map[string]*overlayFile{}
+ fs.overlayDirs[dir] = m
}
+ return m
+}
+
+func (fs *fileSystem) init(c *Config) error {
+ fs.cwd = c.Dir
+
+ overlay := c.Overlay
+ fs.overlayDirs = map[string]map[string]*overlayFile{}
+
+ // Organize overlay
+ for filename, src := range overlay {
+ if !strings.HasSuffix(filename, ".cue") {
+ return errors.Newf(token.NoPos, "overlay file %s not a .cue file", filename)
+ }
+
+ // TODO: do we need to further clean the path or check that the
+ // specified files are within the root/ absolute files?
+ dir, base := filepath.Split(filename)
+ m := fs.getDir(dir, true)
+
+ b, err := src.contents()
+ if err != nil {
+ return err
+ }
+ m[base] = &overlayFile{
+ basename: base,
+ contents: b,
+ modtime: time.Now(),
+ }
+
+ for {
+ prevdir := dir
+ dir, base = filepath.Split(filepath.Dir(dir))
+ if dir == prevdir || dir == "" {
+ break
+ }
+ m := fs.getDir(dir, true)
+ if m[base] == nil {
+ m[base] = &overlayFile{
+ basename: base,
+ modtime: time.Now(),
+ isDir: true,
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func (fs *fileSystem) joinPath(elem ...string) string {
return filepath.Join(elem...)
}
-// splitPathList calls ctxt.SplitPathList (if not nil) or else filepath.SplitList.
-func (ctxt *fileSystem) splitPathList(s string) []string {
- if f := ctxt.SplitPathList; f != nil {
- return f(s)
- }
+func (fs *fileSystem) splitPathList(s string) []string {
return filepath.SplitList(s)
}
-// isAbsPath calls ctxt.IsAbsPath (if not nil) or else filepath.IsAbs.
-func (ctxt *fileSystem) isAbsPath(path string) bool {
- if f := ctxt.IsAbsPath; f != nil {
- return f(path)
- }
+func (fs *fileSystem) isAbsPath(path string) bool {
return filepath.IsAbs(path)
}
-// isDir calls ctxt.IsDir (if not nil) or else uses os.Stat.
-func (ctxt *fileSystem) isDir(path string) bool {
- if f := ctxt.IsDir; f != nil {
- return f(path)
+func (fs *fileSystem) makeAbs(path string) string {
+ if fs.isAbsPath(path) {
+ return path
+ }
+ return filepath.Clean(filepath.Join(fs.cwd, path))
+}
+
+func (fs *fileSystem) isDir(path string) bool {
+ path = fs.makeAbs(path)
+ if fs.getDir(path, false) != nil {
+ return true
}
fi, err := os.Stat(path)
return err == nil && fi.IsDir()
}
-// hasSubdir calls ctxt.HasSubdir (if not nil) or else uses
-// the local file system to answer the question.
-func (ctxt *fileSystem) hasSubdir(root, dir string) (rel string, ok bool) {
- if f := ctxt.HasSubdir; f != nil {
- return f(root, dir)
- }
-
+func (fs *fileSystem) hasSubdir(root, dir string) (rel string, ok bool) {
// Try using paths we received.
if rel, ok = hasSubdir(root, dir); ok {
return
@@ -139,35 +168,127 @@
return filepath.ToSlash(dir[len(root):]), true
}
-// ReadDir calls ctxt.ReadDir (if not nil) or else ioutil.ReadDir.
-func (ctxt *fileSystem) readDir(path string) ([]os.FileInfo, error) {
- if f := ctxt.ReadDir; f != nil {
- return f(path)
+func (fs *fileSystem) readDir(path string) ([]os.FileInfo, errors.Error) {
+ path = fs.makeAbs(path)
+ m := fs.getDir(path, false)
+ items, err := ioutil.ReadDir(path)
+ if err != nil {
+ if !os.IsNotExist(err) || m == nil {
+ return nil, errors.Wrapf(err, token.NoPos, "readDir")
+ }
}
- return ioutil.ReadDir(path)
+ if m != nil {
+ done := map[string]bool{}
+ for i, fi := range items {
+ done[fi.Name()] = true
+ if o := m[fi.Name()]; o != nil {
+ items[i] = o
+ }
+ }
+ for _, o := range m {
+ if !done[o.Name()] {
+ items = append(items, o)
+ }
+ }
+ sort.Slice(items, func(i, j int) bool {
+ return items[i].Name() < items[j].Name()
+ })
+ }
+ return items, nil
}
-// openFile calls ctxt.OpenFile (if not nil) or else os.Open.
-func (ctxt *fileSystem) openFile(path string) (io.ReadCloser, error) {
- if fn := ctxt.OpenFile; fn != nil {
- return fn(path)
+func (fs *fileSystem) getOverlay(path string) *overlayFile {
+ dir, base := filepath.Split(path)
+ if m := fs.getDir(dir, false); m != nil {
+ return m[base]
+ }
+ return nil
+}
+
+func (fs *fileSystem) stat(path string) (os.FileInfo, errors.Error) {
+ path = fs.makeAbs(path)
+ if fi := fs.getOverlay(path); fi != nil {
+ return fi, nil
+ }
+ fi, err := os.Stat(path)
+ if err != nil {
+ return nil, errors.Wrapf(err, token.NoPos, "stat")
+ }
+ return fi, nil
+}
+
+func (fs *fileSystem) lstat(path string) (os.FileInfo, errors.Error) {
+ path = fs.makeAbs(path)
+ if fi := fs.getOverlay(path); fi != nil {
+ return fi, nil
+ }
+ fi, err := os.Lstat(path)
+ if err != nil {
+ return nil, errors.Wrapf(err, token.NoPos, "stat")
+ }
+ return fi, nil
+}
+
+func (fs *fileSystem) openFile(path string) (io.ReadCloser, errors.Error) {
+ path = fs.makeAbs(path)
+ if fi := fs.getOverlay(path); fi != nil {
+ return ioutil.NopCloser(bytes.NewReader(fi.contents)), nil
}
f, err := os.Open(path)
if err != nil {
- return nil, err // nil interface
+ return nil, errors.Wrapf(err, token.NoPos, "load")
}
return f, nil
}
-// isFile determines whether path is a file by trying to open it.
-// It reuses openFile instead of adding another function to the
-// list in Context.
-func (ctxt *fileSystem) isFile(path string) bool {
- f, err := ctxt.openFile(path)
+var skipDir = errors.Newf(token.NoPos, "skip directory")
+
+type walkFunc func(path string, info os.FileInfo, err errors.Error) errors.Error
+
+func (fs *fileSystem) walk(root string, f walkFunc) error {
+ fi, err := fs.lstat(root)
if err != nil {
- return false
+ err = f(root, fi, err)
+ } else if !fi.IsDir() {
+ return errors.Newf(token.NoPos, "path %q is not a directory", root)
+ } else {
+ err = fs.walkRec(root, fi, f)
}
- f.Close()
- return true
+ if err == skipDir {
+ return nil
+ }
+ return err
+
+}
+
+func (fs *fileSystem) walkRec(path string, info os.FileInfo, f walkFunc) errors.Error {
+ if !info.IsDir() {
+ return f(path, info, nil)
+ }
+
+ dir, err := fs.readDir(path)
+ err1 := f(path, info, err)
+
+ // If err != nil, walk can't walk into this directory.
+ // err1 != nil means walkFn want walk to skip this directory or stop walking.
+ // Therefore, if one of err and err1 isn't nil, walk will return.
+ if err != nil || err1 != nil {
+ // The caller's behavior is controlled by the return value, which is decided
+ // by walkFn. walkFn may ignore err and return nil.
+ // If walkFn returns SkipDir, it will be handled by the caller.
+ // So walk should return whatever walkFn returns.
+ return err1
+ }
+
+ for _, info := range dir {
+ filename := fs.joinPath(path, info.Name())
+ err = fs.walkRec(filename, info, f)
+ if err != nil {
+ if !info.IsDir() || err != skipDir {
+ return err
+ }
+ }
+ }
+ return nil
}
diff --git a/cue/load/import.go b/cue/load/import.go
index b5961b4..11f6a6e 100644
--- a/cue/load/import.go
+++ b/cue/load/import.go
@@ -17,7 +17,6 @@
import (
"bytes"
"log"
- "os"
pathpkg "path"
"path/filepath"
"sort"
@@ -150,10 +149,14 @@
}
for _, f := range p.CUEFiles {
- if !filepath.IsAbs(f) {
- f = filepath.Join(root, f)
+ if !ctxt.isAbsPath(f) {
+ f = ctxt.joinPath(root, f)
}
- _ = p.AddFile(f, nil)
+ r, err := ctxt.openFile(f)
+ if err != nil {
+ p.ReportError(err)
+ }
+ _ = p.AddFile(f, r)
}
p.Complete()
return p
@@ -214,7 +217,7 @@
return nil
}
dir := ctxt.joinPath(srcDir, path)
- info, err := os.Stat(filepath.Join(srcDir, path))
+ info, err := ctxt.stat(filepath.Join(srcDir, path))
if err == nil && info.IsDir() {
p.Dir = dir
return nil
@@ -330,7 +333,7 @@
match, data, filename, err := matchFile(fp.c, dir, name, true, fp.allFiles, fp.allTags)
if err != nil {
- return badFile(errors.Wrapf(err, pos, "no match"))
+ return badFile(err)
}
if !match {
if ext == cueSuffix {
@@ -341,10 +344,10 @@
return false // don't mark as added
}
- pf, err := parser.ParseFile(filename, data, parser.ImportsOnly, parser.ParseComments)
- if err != nil {
+ pf, perr := parser.ParseFile(filename, data, parser.ImportsOnly, parser.ParseComments)
+ if perr != nil {
// should always be an errors.List, but just in case.
- switch x := err.(type) {
+ switch x := perr.(type) {
case errors.List:
for _, e := range x {
badFile(e)
@@ -352,7 +355,7 @@
case errors.Error:
badFile(x)
default:
- badFile(errors.Wrapf(err, token.NoPos, "error adding file"))
+ badFile(errors.Wrapf(err, token.NoPos, "add failed"))
}
return true
}
diff --git a/cue/load/loader.go b/cue/load/loader.go
index 9060a1c..4753763 100644
--- a/cue/load/loader.go
+++ b/cue/load/loader.go
@@ -20,7 +20,6 @@
// - go/build
import (
- "os"
pathpkg "path"
"path/filepath"
"strings"
@@ -115,7 +114,7 @@
if !filepath.IsAbs(file) {
path = filepath.Join(cfg.Dir, file)
}
- fi, err := os.Stat(path)
+ fi, err := cfg.fileSystem.stat(path)
if err != nil {
return cfg.newErrInstance(nil, path,
errors.Wrapf(err, pos, "could not find dir %s", path))
@@ -146,10 +145,15 @@
// }
for _, f := range pkg.CUEFiles {
- if !filepath.IsAbs(f) {
- f = filepath.Join(cfg.Dir, f)
+ fs := &l.cfg.fileSystem
+ if !fs.isAbsPath(f) {
+ f = fs.joinPath(cfg.Dir, f)
}
- _ = pkg.AddFile(f, nil)
+ r, err := fs.openFile(f)
+ if err != nil {
+ pkg.ReportError(err)
+ }
+ _ = pkg.AddFile(f, r)
}
pkg.Local = true
diff --git a/cue/load/loader_test.go b/cue/load/loader_test.go
index ee0884f..18f709e 100644
--- a/cue/load/loader_test.go
+++ b/cue/load/loader_test.go
@@ -22,8 +22,11 @@
"strconv"
"strings"
"testing"
+ "unicode"
+ "cuelang.org/go/cue"
build "cuelang.org/go/cue/build"
+ "cuelang.org/go/cue/format"
"cuelang.org/go/internal/str"
)
@@ -122,3 +125,52 @@
}
return b.String()
}
+
+func TestOverlays(t *testing.T) {
+ cwd, _ := os.Getwd()
+ abs := func(path string) string {
+ return filepath.Join(cwd, path)
+ }
+ c := &Config{
+ Overlay: map[string]Source{
+ abs("dir/top.cue"): FromBytes([]byte(`
+ package top
+ msg: "Hello"
+ `)),
+ abs("dir/b/foo.cue"): FromString(`
+ package foo
+
+ a: <= 5
+ `),
+ abs("dir/b/bar.cue"): FromString(`
+ package foo
+
+ a: >= 5
+ `),
+ },
+ }
+ want := []string{
+ `{msg:"Hello"}`,
+ `{a:5}`,
+ }
+ rmSpace := func(r rune) rune {
+ if unicode.IsSpace(r) {
+ return -1
+ }
+ return r
+ }
+ for i, inst := range cue.Build(Instances([]string{"./dir/..."}, c)) {
+ if inst.Err != nil {
+ t.Error(inst.Err)
+ continue
+ }
+ b, err := format.Node(inst.Value().Syntax())
+ if err != nil {
+ t.Error(err)
+ continue
+ }
+ if got := string(bytes.Map(rmSpace, b)); got != want[i] {
+ t.Errorf("%s: got %s; want %s", inst.Dir, got, want)
+ }
+ }
+}
diff --git a/cue/load/match.go b/cue/load/match.go
index fb00fa6..5f8af37 100644
--- a/cue/load/match.go
+++ b/cue/load/match.go
@@ -16,9 +16,11 @@
import (
"bytes"
- "fmt"
"strings"
"unicode"
+
+ "cuelang.org/go/cue/errors"
+ "cuelang.org/go/cue/token"
)
// matchFileTest reports whether the file with the given name in the given directory
@@ -40,7 +42,7 @@
// considers text until the first non-comment.
// If allTags is non-nil, matchFile records any encountered build tag
// by setting allTags[tag] = true.
-func matchFile(cfg *Config, dir, name string, returnImports, allFiles bool, allTags map[string]bool) (match bool, data []byte, filename string, err error) {
+func matchFile(cfg *Config, dir, name string, returnImports, allFiles bool, allTags map[string]bool) (match bool, data []byte, filename string, err errors.Error) {
if strings.HasPrefix(name, "_") ||
strings.HasPrefix(name, ".") {
return
@@ -73,7 +75,7 @@
}
f.Close()
if err != nil {
- err = fmt.Errorf("read %s: %v", filename, err)
+ err = errors.Newf(token.NoPos, "read %s: %v", filename, err)
return
}
diff --git a/cue/load/match_test.go b/cue/load/match_test.go
index 3dbab93..f2876d5 100644
--- a/cue/load/match_test.go
+++ b/cue/load/match_test.go
@@ -16,8 +16,9 @@
import (
"io"
+ "os"
+ "path/filepath"
"reflect"
- "strings"
"testing"
)
@@ -116,27 +117,25 @@
match bool
}{
{defCfg, "foo.cue", "", true},
- {defCfg, "foo.cue", "// +build enable\n\npackage foo\n", false},
+ {defCfg, "a/b/c/foo.cue", "// +build enable\n\npackage foo\n", false},
{defCfg, "foo.cue", "// +build !enable\n\npackage foo\n", true},
{defCfg, "foo1.cue", "// +build linux\n\npackage foo\n", false},
{defCfg, "foo.badsuffix", "", false},
- {cfg, "foo.cue", "// +build enable\n\npackage foo\n", true},
+ {cfg, "a/b/c/d/foo.cue", "// +build enable\n\npackage foo\n", true},
{cfg, "foo.cue", "// +build !enable\n\npackage foo\n", false},
}
func TestMatchFile(t *testing.T) {
+ cwd, _ := os.Getwd()
+ abs := func(path string) string {
+ return filepath.Join(cwd, path)
+ }
for _, tt := range matchFileTests {
- ctxt := &tt.cfg.fileSystem
- ctxt.OpenFile = func(path string) (r io.ReadCloser, err error) {
- if path != "x+"+tt.name {
- t.Fatalf("OpenFile asked for %q, expected %q", path, "x+"+tt.name)
- }
- return &readNopCloser{strings.NewReader(tt.data)}, nil
- }
- ctxt.JoinPath = func(elem ...string) string {
- return strings.Join(elem, "+")
- }
- match, err := matchFileTest(tt.cfg, "x", tt.name)
+ cfg := tt.cfg
+ cfg.Overlay = map[string]Source{abs(tt.name): FromString(tt.data)}
+ cfg, _ = cfg.complete()
+
+ match, err := matchFileTest(cfg, "", tt.name)
if match != tt.match || err != nil {
t.Fatalf("MatchFile(%q) = %v, %v, want %v, nil", tt.name, match, err, tt.match)
}
diff --git a/cue/load/read.go b/cue/load/read.go
index 79f68b4..beb04d8 100644
--- a/cue/load/read.go
+++ b/cue/load/read.go
@@ -16,16 +16,18 @@
import (
"bufio"
- "errors"
"io"
"unicode/utf8"
+
+ "cuelang.org/go/cue/errors"
+ "cuelang.org/go/cue/token"
)
type importReader struct {
b *bufio.Reader
buf []byte
peek byte
- err error
+ err errors.Error
eof bool
nerr int
}
@@ -35,8 +37,8 @@
}
var (
- errSyntax = errors.New("syntax error")
- errNUL = errors.New("unexpected NUL in input")
+ errSyntax = errors.Newf(token.NoPos, "syntax error") // TODO: remove
+ errNUL = errors.Newf(token.NoPos, "unexpected NUL in input")
)
// syntaxError records a syntax error, but only if an I/O error has not already been recorded.
@@ -60,7 +62,7 @@
if err == io.EOF {
r.eof = true
} else if r.err == nil {
- r.err = err
+ r.err = errors.Wrapf(err, token.NoPos, "readByte")
}
c = 0
}
@@ -208,7 +210,7 @@
// readComments is like ioutil.ReadAll, except that it only reads the leading
// block of comments in the file.
-func readComments(f io.Reader) ([]byte, error) {
+func readComments(f io.Reader) ([]byte, errors.Error) {
r := &importReader{b: bufio.NewReader(f)}
r.peekByte(true)
if r.err == nil && !r.eof {
@@ -220,7 +222,7 @@
// readImports is like ioutil.ReadAll, except that it expects a Go file as input
// and stops reading the input once the imports have completed.
-func readImports(f io.Reader, reportSyntaxError bool, imports *[]string) ([]byte, error) {
+func readImports(f io.Reader, reportSyntaxError bool, imports *[]string) ([]byte, errors.Error) {
r := &importReader{b: bufio.NewReader(f)}
r.readKeyword("package")
diff --git a/cue/load/read_test.go b/cue/load/read_test.go
index aeb2b1d..372ebe8 100644
--- a/cue/load/read_test.go
+++ b/cue/load/read_test.go
@@ -18,6 +18,8 @@
"io"
"strings"
"testing"
+
+ "cuelang.org/go/cue/errors"
)
const quote = "`"
@@ -104,7 +106,7 @@
},
}
-func testRead(t *testing.T, tests []readTest, read func(io.Reader) ([]byte, error)) {
+func testRead(t *testing.T, tests []readTest, read func(io.Reader) ([]byte, errors.Error)) {
for i, tt := range tests {
var in, testOut string
j := strings.Index(tt.in, "ℙ")
@@ -141,7 +143,9 @@
}
func TestReadImports(t *testing.T) {
- testRead(t, readImportsTests, func(r io.Reader) ([]byte, error) { return readImports(r, true, nil) })
+ testRead(t, readImportsTests, func(r io.Reader) ([]byte, errors.Error) {
+ return readImports(r, true, nil)
+ })
}
func TestReadComments(t *testing.T) {
@@ -217,7 +221,9 @@
func TestReadFailures(t *testing.T) {
// Errors should be reported (true arg to readImports).
- testRead(t, readFailuresTests, func(r io.Reader) ([]byte, error) { return readImports(r, true, nil) })
+ testRead(t, readFailuresTests, func(r io.Reader) ([]byte, errors.Error) {
+ return readImports(r, true, nil)
+ })
}
func TestReadFailuresIgnored(t *testing.T) {
@@ -232,5 +238,7 @@
tt.err = ""
}
}
- testRead(t, tests, func(r io.Reader) ([]byte, error) { return readImports(r, false, nil) })
+ testRead(t, tests, func(r io.Reader) ([]byte, errors.Error) {
+ return readImports(r, false, nil)
+ })
}
diff --git a/cue/load/search.go b/cue/load/search.go
index af48a0b..da43ad5 100644
--- a/cue/load/search.go
+++ b/cue/load/search.go
@@ -15,7 +15,8 @@
package load
import (
- "fmt" // TODO: remove this usage
+ // TODO: remove this usage
+
"os"
"path"
"path/filepath"
@@ -23,6 +24,7 @@
"strings"
build "cuelang.org/go/cue/build"
+ "cuelang.org/go/cue/errors"
"cuelang.org/go/cue/token"
)
@@ -94,7 +96,7 @@
// return nil
// }
// if !want {
- // return filepath.SkipDir
+ // return skipDir
// }
// if have[name] {
@@ -148,7 +150,7 @@
if c.modRoot != "" {
if !hasFilepathPrefix(root, c.modRoot) {
- m.Err = fmt.Errorf(
+ m.Err = errors.Newf(token.NoPos,
"cue: pattern %s refers to dir %s, outside module root %s",
pattern, root, c.modRoot)
return m
@@ -157,12 +159,12 @@
pkgDir := filepath.Join(root, "pkg")
- _ = filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
+ _ = c.fileSystem.walk(root, func(path string, fi os.FileInfo, err errors.Error) errors.Error {
if err != nil || !fi.IsDir() {
return nil
}
if path == pkgDir {
- return filepath.SkipDir
+ return skipDir
}
top := path == root
@@ -171,13 +173,13 @@
_, elem := filepath.Split(path)
dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".."
if dot || strings.HasPrefix(elem, "_") || (elem == "testdata" && !top) {
- return filepath.SkipDir
+ return skipDir
}
if !top {
// Ignore other modules found in subdirectories.
- if _, err := os.Stat(filepath.Join(path, modFile)); err == nil {
- return filepath.SkipDir
+ if _, err := c.fileSystem.stat(filepath.Join(path, modFile)); err == nil {
+ return skipDir
}
}
@@ -300,7 +302,7 @@
for _, m := range matches {
if len(m.Pkgs) == 0 {
m.Err =
- fmt.Errorf("cue: %q matched no packages\n", m.Pattern)
+ errors.Newf(token.NoPos, "cue: %q matched no packages\n", m.Pattern)
}
}
}
diff --git a/cue/load/source.go b/cue/load/source.go
new file mode 100644
index 0000000..9514f2a
--- /dev/null
+++ b/cue/load/source.go
@@ -0,0 +1,60 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package load
+
+import (
+ "cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/format"
+)
+
+// A Source represents file contents.
+type Source interface {
+ contents() ([]byte, error)
+}
+
+// FromString creates a Source from the given string.
+func FromString(s string) Source {
+ return stringSource(s)
+}
+
+// FromBytes creates a Source from the given bytes. The contents are not
+// copied and should not be modified.
+func FromBytes(b []byte) Source {
+ return bytesSource(b)
+}
+
+// FromFile creates a Source from the given *ast.File. The file should not be
+// modified. It is assumed the file is error-free.
+func FromFile(f *ast.File) Source {
+ return (*fileSource)(f)
+}
+
+type stringSource string
+
+func (s stringSource) contents() ([]byte, error) {
+ return []byte(s), nil
+}
+
+type bytesSource []byte
+
+func (s bytesSource) contents() ([]byte, error) {
+ return []byte(s), nil
+}
+
+type fileSource ast.File
+
+func (s *fileSource) contents() ([]byte, error) {
+ return format.Node((*ast.File)(s))
+}