cue/load: use build.File to support "-"

Added test to test high-level usage for the eval command.

Change-Id: I00541817ef1a6a30580d27a0c2ccd3685602e846
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5223
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/common.go b/cmd/cue/cmd/common.go
index b1abf47..7e0a220 100644
--- a/cmd/cue/cmd/common.go
+++ b/cmd/cue/cmd/common.go
@@ -301,6 +301,8 @@
 	if cfg == nil {
 		cfg = defaultConfig
 	}
+	cfg.Stdin = stdin
+
 	builds := loadFromArgs(cmd, args, cfg)
 	if builds == nil {
 		return nil, errors.Newf(token.NoPos, "invalid args")
diff --git a/cmd/cue/cmd/testdata/script/eval_stdin.txt b/cmd/cue/cmd/testdata/script/eval_stdin.txt
new file mode 100644
index 0000000..18d07eb
--- /dev/null
+++ b/cmd/cue/cmd/testdata/script/eval_stdin.txt
@@ -0,0 +1,12 @@
+stdin stdin.cue
+cue eval t.cue -
+cmp stdout expect-stdout
+
+-- stdin.cue --
+foo:3
+-- t.cue --
+foo: int
+bar: 3
+-- expect-stdout --
+foo: 3
+bar: 3
diff --git a/cmd/cue/cmd/testdata/script/issue174.txt b/cmd/cue/cmd/testdata/script/issue174.txt
new file mode 100644
index 0000000..a611481
--- /dev/null
+++ b/cmd/cue/cmd/testdata/script/issue174.txt
@@ -0,0 +1,8 @@
+! cue export ./issue174
+cmp stderr expect-stderr
+-- expect-stderr --
+build constraints exclude all CUE files in ./issue174 (ignored: issue174/issue174.cue)
+-- issue174/issue174.cue --
+import 'foo'
+
+a: 1
\ No newline at end of file
diff --git a/cue/load/config.go b/cue/load/config.go
index 51b1faf..95d883e 100644
--- a/cue/load/config.go
+++ b/cue/load/config.go
@@ -15,6 +15,7 @@
 package load
 
 import (
+	"io"
 	"os"
 	pathpkg "path"
 	"path/filepath"
@@ -178,17 +179,25 @@
 	// 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
 
+	// Stdin defines an alternative for os.Stdin for the file "-". When used,
+	// the corresponding build.File will be associated with the full buffer.
+	Stdin io.Reader
+
 	fileSystem
 
 	loadFunc build.LoadFunc
 }
 
+func (c *Config) stdin() io.Reader {
+	if c.Stdin == nil {
+		return os.Stdin
+	}
+	return c.Stdin
+}
+
 func (c *Config) newInstance(pos token.Pos, p importPath) *build.Instance {
 	dir, name, err := c.absDirFromImportPath(pos, p)
 	i := c.Context.NewInstance(dir, c.loadFunc)
diff --git a/cue/load/fs.go b/cue/load/fs.go
index 082efbb..b269f00 100644
--- a/cue/load/fs.go
+++ b/cue/load/fs.go
@@ -16,7 +16,6 @@
 
 import (
 	"bytes"
-	"go/ast"
 	"io"
 	"io/ioutil"
 	"os"
@@ -25,6 +24,7 @@
 	"strings"
 	"time"
 
+	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/token"
 )
@@ -73,13 +73,14 @@
 		dir, base := filepath.Split(filename)
 		m := fs.getDir(dir, true)
 
-		b, err := src.contents()
+		b, file, err := src.contents()
 		if err != nil {
 			return err
 		}
 		m[base] = &overlayFile{
 			basename: base,
 			contents: b,
+			file:     file,
 			modtime:  time.Now(),
 		}
 
diff --git a/cue/load/import.go b/cue/load/import.go
index 3839ad7..ab045ce 100644
--- a/cue/load/import.go
+++ b/cue/load/import.go
@@ -238,27 +238,27 @@
 
 func rewriteFiles(p *build.Instance, root string, isLocal bool) {
 	p.Root = root
-	for i, path := range p.CUEFiles {
-		p.CUEFiles[i] = normPrefix(root, path, isLocal)
-		sortParentsFirst(p.CUEFiles)
-	}
-	for i, path := range p.TestCUEFiles {
-		p.TestCUEFiles[i] = normPrefix(root, path, isLocal)
-		sortParentsFirst(p.TestCUEFiles)
-	}
-	for i, path := range p.ToolCUEFiles {
-		p.ToolCUEFiles[i] = normPrefix(root, path, isLocal)
-		sortParentsFirst(p.ToolCUEFiles)
-	}
-	for i, path := range p.IgnoredCUEFiles {
+
+	normalizeFilenames(root, p.CUEFiles, isLocal)
+	normalizeFilenames(root, p.TestCUEFiles, isLocal)
+	normalizeFilenames(root, p.ToolCUEFiles, isLocal)
+	normalizeFilenames(root, p.IgnoredCUEFiles, isLocal)
+	normalizeFilenames(root, p.InvalidCUEFiles, isLocal)
+
+	normalizeFiles(p.BuildFiles)
+	normalizeFiles(p.IgnoredFiles)
+	normalizeFiles(p.OrphanedFiles)
+	normalizeFiles(p.InvalidFiles)
+	normalizeFiles(p.UnknownFiles)
+}
+
+func normalizeFilenames(root string, a []string, isLocal bool) {
+	for i, path := range a {
 		if strings.HasPrefix(path, root) {
-			p.IgnoredCUEFiles[i] = normPrefix(root, path, isLocal)
+			a[i] = normPrefix(root, path, isLocal)
 		}
 	}
-	for i, path := range p.InvalidCUEFiles {
-		p.InvalidCUEFiles[i] = normPrefix(root, path, isLocal)
-		sortParentsFirst(p.InvalidCUEFiles)
-	}
+	sortParentsFirst(a)
 }
 
 func sortParentsFirst(s []string) {
@@ -267,6 +267,12 @@
 	})
 }
 
+func normalizeFiles(a []*build.File) {
+	sort.Slice(a, func(i, j int) bool {
+		return len(filepath.Dir(a[i].Filename)) < len(filepath.Dir(a[j].Filename))
+	})
+}
+
 type fileProcessor struct {
 	firstFile        string
 	firstCommentFile string
@@ -322,15 +328,15 @@
 }
 
 func (fp *fileProcessor) add(pos token.Pos, root string, file *build.File, mode importMode) (added bool) {
-	path := file.Filename
-	fullPath := path
-	if !filepath.IsAbs(path) {
-		fullPath = filepath.Join(root, path)
+	fullPath := file.Filename
+	if fullPath != "-" {
+		if !filepath.IsAbs(fullPath) {
+			fullPath = filepath.Join(root, fullPath)
+		}
 	}
 	file.Filename = fullPath
 
-	name := filepath.Base(fullPath)
-	dir := filepath.Dir(fullPath)
+	base := filepath.Base(fullPath)
 
 	p := fp.pkg
 
@@ -341,7 +347,7 @@
 		return true
 	}
 
-	match, data, filename, err := matchFile(fp.c, dir, name, true, fp.allFiles, fp.allTags)
+	match, data, err := matchFile(fp.c, file, true, fp.allFiles, fp.allTags)
 	if err != nil {
 		return badFile(err)
 	}
@@ -356,7 +362,7 @@
 		return false // don't mark as added
 	}
 
-	pf, perr := parser.ParseFile(filename, data, parser.ImportsOnly, parser.ParseComments)
+	pf, perr := parser.ParseFile(fullPath, data, parser.ImportsOnly, parser.ParseComments)
 	if perr != nil {
 		badFile(errors.Promote(perr, "add failed"))
 		return true
@@ -371,7 +377,7 @@
 
 	if p.PkgName == "" {
 		p.PkgName = pkg
-		fp.firstFile = name
+		fp.firstFile = base
 	} else if pkg != p.PkgName {
 		if fp.ignoreOther {
 			p.IgnoredCUEFiles = append(p.IgnoredCUEFiles, fullPath)
@@ -381,24 +387,24 @@
 		return badFile(&MultiplePackageError{
 			Dir:      p.Dir,
 			Packages: []string{p.PkgName, pkg},
-			Files:    []string{fp.firstFile, name},
+			Files:    []string{fp.firstFile, base},
 		})
 	}
 
-	isTest := strings.HasSuffix(name, "_test"+cueSuffix)
-	isTool := strings.HasSuffix(name, "_tool"+cueSuffix)
+	isTest := strings.HasSuffix(base, "_test"+cueSuffix)
+	isTool := strings.HasSuffix(base, "_tool"+cueSuffix)
 
 	if mode&importComment != 0 {
 		qcom, line := findimportComment(data)
 		if line != 0 {
 			com, err := strconv.Unquote(qcom)
 			if err != nil {
-				badFile(errors.Newf(pos, "%s:%d: cannot parse import comment", filename, line))
+				badFile(errors.Newf(pos, "%s:%d: cannot parse import comment", fullPath, line))
 			} else if p.ImportComment == "" {
 				p.ImportComment = com
-				fp.firstCommentFile = name
+				fp.firstCommentFile = base
 			} else if p.ImportComment != com {
-				badFile(errors.Newf(pos, "found import comments %q (%s) and %q (%s) in %s", p.ImportComment, fp.firstCommentFile, com, name, p.Dir))
+				badFile(errors.Newf(pos, "found import comments %q (%s) and %q (%s) in %s", p.ImportComment, fp.firstCommentFile, com, base, p.Dir))
 			}
 		}
 	}
@@ -414,7 +420,7 @@
 			if err != nil {
 				badFile(errors.Newf(
 					spec.Path.Pos(),
-					"%s: parser returned invalid quoted string: <%s>", filename, quoted,
+					"%s: parser returned invalid quoted string: <%s>", fullPath, quoted,
 				))
 			}
 			if !isTest || fp.c.Tests {
diff --git a/cue/load/loader.go b/cue/load/loader.go
index 7ac0d01..74a9daa 100644
--- a/cue/load/loader.go
+++ b/cue/load/loader.go
@@ -29,6 +29,7 @@
 	"cuelang.org/go/cue/build"
 	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/token"
+	"cuelang.org/go/internal/encoding"
 	"cuelang.org/go/internal/filetypes"
 	"golang.org/x/xerrors"
 )
@@ -149,39 +150,37 @@
 	// ModInit() // TODO: support modules
 	pkg := l.cfg.Context.NewInstance(cfg.Dir, l.loadFunc())
 
+	_, err := filepath.Abs(cfg.Dir)
+	if err != nil {
+		return cfg.newErrInstance(pos, toImportPath(cfg.Dir),
+			errors.Wrapf(err, pos, "could not convert '%s' to absolute path", cfg.Dir))
+	}
+
 	for _, bf := range files {
 		f := bf.Filename
-		if cfg.isDir(f) {
+		if f == "-" {
+			continue
+		}
+		if !filepath.IsAbs(f) {
+			f = filepath.Join(cfg.Dir, f)
+		}
+		fi, err := cfg.fileSystem.stat(f)
+		if err != nil {
+			return cfg.newErrInstance(pos, toImportPath(f),
+				errors.Wrapf(err, pos, "could not find file"))
+		}
+		if fi.IsDir() {
 			return cfg.newErrInstance(token.NoPos, toImportPath(f),
 				errors.Newf(pos, "file is a directory %v", f))
 		}
 	}
 
-	// TODO: add fields directly?
 	fp := newFileProcessor(cfg, pkg)
 	for _, file := range files {
-		path := file.Filename
-		if !filepath.IsAbs(path) {
-			path = filepath.Join(cfg.Dir, path)
-		}
-		fi, err := cfg.fileSystem.stat(path)
-		if err != nil {
-			return cfg.newErrInstance(pos, toImportPath(path),
-				errors.Wrapf(err, pos, "could not find dir %s", path))
-		}
-		if fi.IsDir() {
-			return cfg.newErrInstance(pos, toImportPath(path),
-				errors.Newf(pos, "%s is a directory, should be a CUE file", file.Filename))
-		}
 		fp.add(pos, cfg.Dir, file, allowAnonymous)
 	}
 
 	// TODO: ModImportFromFiles(files)
-	_, err := filepath.Abs(cfg.Dir)
-	if err != nil {
-		return cfg.newErrInstance(pos, toImportPath(cfg.Dir),
-			errors.Wrapf(err, pos, "could convert '%s' to absolute path", cfg.Dir))
-	}
 	pkg.Dir = cfg.Dir
 	rewriteFiles(pkg, pkg.Dir, true)
 	for _, err := range errors.Errors(fp.finalize()) { // ImportDir(&ctxt, dir, 0)
@@ -211,19 +210,15 @@
 }
 
 func (l *loader) addFiles(dir string, p *build.Instance) {
-	files := p.CUEFiles
-	fs := &l.cfg.fileSystem
-
-	for _, f := range files {
-		if !fs.isAbsPath(f) {
-			f = fs.joinPath(dir, f)
+	for _, f := range p.BuildFiles {
+		d := encoding.NewDecoder(f, &encoding.Config{Stdin: l.cfg.stdin()})
+		for ; !d.Done(); d.Next() {
+			_ = p.AddSyntax(d.File())
 		}
-		r, err := fs.openFile(f)
-		if err != nil {
-			p.ReportError(err)
+		if err := d.Err(); err != nil {
+			p.ReportError(errors.Promote(err, "load"))
 		}
-
-		_ = p.AddFile(f, r)
+		d.Close()
 	}
 }
 
diff --git a/cue/load/loader_test.go b/cue/load/loader_test.go
index b0090d8..524333a 100644
--- a/cue/load/loader_test.go
+++ b/cue/load/loader_test.go
@@ -154,7 +154,7 @@
 		cfg:  dirCfg,
 		args: args("example.org/test/hello:nonexist"),
 		want: `
-err:    build constraints exclude all CUE files in example.org/test/hello:nonexist (ignored: hello/test.cue, anon.cue, test.cue)
+err:    build constraints exclude all CUE files in example.org/test/hello:nonexist (ignored: anon.cue, test.cue, hello/test.cue)
 path:   example.org/test/hello:nonexist
 module: example.org/test
 root:   $CWD/testdata
@@ -183,7 +183,18 @@
 dir:    $CWD/testdata
 display:command-line-arguments
 files:
-	$CWD/testdata/anon.cue`,
+    $CWD/testdata/anon.cue`,
+	}, {
+		cfg:  dirCfg,
+		args: args("-"),
+		want: `
+path:   ""
+module: ""
+root:   $CWD/testdata
+dir:    $CWD/testdata
+display:command-line-arguments
+files:
+    -`,
 	}, {
 		// NOTE: dir should probably be set to $CWD/testdata, but either way.
 		cfg:  dirCfg,
diff --git a/cue/load/match.go b/cue/load/match.go
index cb61e07..e357996 100644
--- a/cue/load/match.go
+++ b/cue/load/match.go
@@ -16,12 +16,15 @@
 
 import (
 	"bytes"
-	"path"
+	"io/ioutil"
+	"path/filepath"
 	"strings"
 	"unicode"
 
+	"cuelang.org/go/cue/build"
 	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/token"
+	"cuelang.org/go/internal/filetypes"
 )
 
 // matchFileTest reports whether the file with the given name in the given directory
@@ -31,7 +34,11 @@
 // matchFileTest considers the name of the file and may use cfg.Build.OpenFile to
 // read some or all of the file's content.
 func matchFileTest(cfg *Config, dir, name string) (match bool, err error) {
-	match, _, _, err = matchFile(cfg, dir, name, false, false, nil)
+	file, err := filetypes.ParseFile(filepath.Join(dir, name), filetypes.Input)
+	if err != nil {
+		return false, nil
+	}
+	match, _, err = matchFile(cfg, file, false, false, nil)
 	return
 }
 
@@ -43,38 +50,49 @@
 // 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 errors.Error) {
-	if strings.HasPrefix(name, "_") {
+func matchFile(cfg *Config, file *build.File, returnImports, allFiles bool, allTags map[string]bool) (match bool, data []byte, err errors.Error) {
+	if fi := cfg.fileSystem.getOverlay(file.Filename); fi != nil {
+		if fi.file != nil {
+			file.Source = fi.file
+		} else {
+			file.Source = fi.contents
+		}
+	}
+
+	if file.Encoding != build.CUE {
 		return
 	}
+
+	if file.Filename == "-" {
+		b, err2 := ioutil.ReadAll(cfg.stdin())
+		if err2 != nil {
+			err = errors.Newf(token.NoPos, "read stdin: %v", err)
+			return
+		}
+		file.Source = b
+		data = b
+		match = true // don't check shouldBuild for stdin
+		return
+	}
+
+	name := filepath.Base(file.Filename)
 	if !cfg.filesMode && strings.HasPrefix(name, ".") {
 		return
 	}
 
-	ext := path.Ext(name)
-
-	switch ext {
-	case cueSuffix:
-		// tentatively okay - read to make sure
-	default:
-		// skip
+	if strings.HasPrefix(name, "_") {
 		return
 	}
 
-	filename = cfg.fileSystem.joinPath(dir, name)
-	f, err := cfg.fileSystem.openFile(filename)
+	f, err := cfg.fileSystem.openFile(file.Filename)
 	if err != nil {
 		return
 	}
 
-	if strings.HasSuffix(filename, cueSuffix) {
-		data, err = readImports(f, false, nil)
-	} else {
-		data, err = readComments(f)
-	}
+	data, err = readImports(f, false, nil)
 	f.Close()
 	if err != nil {
-		err = errors.Newf(token.NoPos, "read %s: %v", filename, err)
+		err = errors.Newf(token.NoPos, "read %s: %v", file.Filename, err)
 		return
 	}
 
diff --git a/cue/load/source.go b/cue/load/source.go
index 9514f2a..0485585 100644
--- a/cue/load/source.go
+++ b/cue/load/source.go
@@ -21,7 +21,7 @@
 
 // A Source represents file contents.
 type Source interface {
-	contents() ([]byte, error)
+	contents() ([]byte, *ast.File, error)
 }
 
 // FromString creates a Source from the given string.
@@ -43,18 +43,21 @@
 
 type stringSource string
 
-func (s stringSource) contents() ([]byte, error) {
-	return []byte(s), nil
+func (s stringSource) contents() ([]byte, *ast.File, error) {
+	return []byte(s), nil, nil
 }
 
 type bytesSource []byte
 
-func (s bytesSource) contents() ([]byte, error) {
-	return []byte(s), nil
+func (s bytesSource) contents() ([]byte, *ast.File, error) {
+	return []byte(s), nil, nil
 }
 
 type fileSource ast.File
 
-func (s *fileSource) contents() ([]byte, error) {
-	return format.Node((*ast.File)(s))
+func (s *fileSource) contents() ([]byte, *ast.File, error) {
+	f := (*ast.File)(s)
+	// TODO: wasteful formatting, but needed for now.
+	b, err := format.Node(f)
+	return b, f, err
 }
diff --git a/internal/encoding/encoding.go b/internal/encoding/encoding.go
index 22871ea..178d506 100644
--- a/internal/encoding/encoding.go
+++ b/internal/encoding/encoding.go
@@ -115,18 +115,24 @@
 // type of f must be a data type, but does not have to be an encoding that
 // can stream. stdin is used in case the file is "-".
 func NewDecoder(f *build.File, cfg *Config) *Decoder {
-	r, err := reader(f, cfg.Stdin)
-	i := &Decoder{
-		closer:   r,
-		err:      err,
-		filename: f.Filename,
-		next: func() (ast.Expr, error) {
-			if err == nil {
-				err = io.EOF
-			}
-			return nil, io.EOF
-		},
+	i := &Decoder{filename: f.Filename}
+	i.next = func() (ast.Expr, error) {
+		if i.err != nil {
+			return nil, i.err
+		}
+		return nil, io.EOF
 	}
+
+	if f, ok := f.Source.(*ast.File); ok {
+		i.file = f
+		i.closer = ioutil.NopCloser(strings.NewReader(""))
+		// TODO: verify input format for CUE.
+		return i
+	}
+
+	r, err := reader(f, cfg.Stdin)
+	i.closer = r
+	i.err = err
 	if err != nil {
 		return i
 	}