cue/build: add package
Change-Id: I572b8279b51b8816425923dc7f2a7f3d31853ad3
diff --git a/cue/build/context.go b/cue/build/context.go
new file mode 100644
index 0000000..b918833
--- /dev/null
+++ b/cue/build/context.go
@@ -0,0 +1,136 @@
+// Copyright 2018 The 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 build defines data types and utilities for defining CUE configuration
+// instances.
+//
+// This package enforces the rules regarding packages and instances as defined
+// in the spec, but it leaves any other details, as well as handling of modules,
+// up to the implementation.
+//
+// A full implementation of instance loading can be found in the loader package.
+//
+// WARNING: this packages may change. It is fine to use load and cue, who both
+// use this package.
+package build
+
+import (
+ "context"
+
+ "cuelang.org/go/cue/parser"
+ "cuelang.org/go/cue/token"
+)
+
+// A Context keeps track of state of building instances and caches work.
+type Context struct {
+ ctxt context.Context
+ fset *token.FileSet
+
+ loader LoadFunc
+ parseOptions []parser.Option
+
+ initialized bool
+
+ imports map[string]*Instance
+}
+
+// NewInstance creates an instance for this Context.
+func (c *Context) NewInstance(dir string, f LoadFunc) *Instance {
+ if f == nil {
+ f = c.loader
+ }
+ return &Instance{
+ ctxt: c,
+ loadFunc: f,
+ Dir: dir,
+ }
+}
+
+// Complete finishes the initialization of an instance. All files must have
+// been added with AddFile before this call.
+func (inst *Instance) Complete() error {
+ if inst.done {
+ return inst.Err
+ }
+ inst.done = true
+
+ err := inst.complete()
+ if err != nil {
+ inst.Err = err
+ inst.Incomplete = true
+ }
+ return err
+}
+
+func (c *Context) init() {
+ if !c.initialized {
+ c.initialized = true
+ c.ctxt = context.Background()
+ c.initialized = true
+ c.imports = map[string]*Instance{}
+ c.fset = token.NewFileSet()
+ }
+}
+
+// Options:
+// - certain parse modes
+// - parallellism
+// - error handler (allows cancelling the context)
+// - file set.
+
+// NewContext creates a new build context.
+//
+// All instances must be created with a context.
+func NewContext(opts ...Option) *Context {
+ c := &Context{}
+ for _, o := range opts {
+ o(c)
+ }
+ c.init()
+ return c
+}
+
+// Pos returns position information for a token.Pos.
+func (c *Context) Pos(pos token.Pos) token.Position {
+ if c.fset == nil {
+ return token.Position{}
+ }
+ return c.fset.Position(pos)
+}
+
+// FileSet reports the file set used for parsing files.
+func (c *Context) FileSet() *token.FileSet {
+ c.init()
+ return c.fset
+}
+
+// PurgeCache purges the instance cache.
+func (c *Context) PurgeCache() {
+ for name := range c.imports {
+ delete(c.imports, name)
+ }
+}
+
+// Option define build options.
+type Option func(c *Context)
+
+// ParseOptions sets parsing options.
+func ParseOptions(mode ...parser.Option) Option {
+ return func(c *Context) { c.parseOptions = mode }
+}
+
+// Loader sets parsing options.
+func Loader(f LoadFunc) Option {
+ return func(c *Context) { c.loader = f }
+}
diff --git a/cue/build/doc.go b/cue/build/doc.go
new file mode 100644
index 0000000..52421c6
--- /dev/null
+++ b/cue/build/doc.go
@@ -0,0 +1,16 @@
+// Copyright 2018 The 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 build defines collections of CUE files to build an instance.
+package build // import "cuelang.org/go/cue/build"
diff --git a/cue/build/import.go b/cue/build/import.go
new file mode 100644
index 0000000..bb29061
--- /dev/null
+++ b/cue/build/import.go
@@ -0,0 +1,154 @@
+// Copyright 2018 The 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 build
+
+import (
+ "log"
+ "sort"
+ "strconv"
+
+ "cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/errors"
+ "cuelang.org/go/cue/token"
+)
+
+type LoadFunc func(path string) *Instance
+
+func (inst *Instance) complete() error {
+ // TODO: handle case-insensitive collisions.
+ // dir := inst.Dir
+ // names := []string{}
+ // for _, src := range sources {
+ // names = append(names, src.path)
+ // }
+ // f1, f2 := str.FoldDup(names)
+ // if f1 != "" {
+ // return nil, fmt.Errorf("case-insensitive file name collision: %q and %q", f1, f2)
+ // }
+
+ var (
+ c = inst.ctxt
+ fset = c.FileSet()
+ imported = map[string][]token.Position{}
+ )
+
+ for _, f := range inst.Files {
+ for _, decl := range f.Decls {
+ d, ok := decl.(*ast.ImportDecl)
+ if !ok {
+ continue
+ }
+ for _, spec := range d.Specs {
+ quoted := spec.Path.Value
+ path, err := strconv.Unquote(quoted)
+ if err != nil {
+ // TODO: remove panic
+ log.Panicf("%s: parser returned invalid quoted string: <%s>", f.Filename, quoted)
+ }
+ imported[path] = append(imported[path], fset.Position(spec.Pos()))
+ }
+ }
+ }
+
+ paths := make([]string, 0, len(imported))
+ for path := range imported {
+ paths = append(paths, path)
+ if path == "" {
+ return errors.E(imported[path], "empty import path")
+ }
+ }
+
+ sort.Strings(paths)
+
+ if inst.loadFunc != nil {
+ for i, path := range paths {
+ isLocal := IsLocalImport(path)
+ if isLocal {
+ // path = dirToImportPath(filepath.Join(dir, path))
+ }
+
+ imp := c.imports[path]
+ if imp == nil {
+ imp = inst.loadFunc(path)
+ if imp == nil {
+ continue
+ }
+ if imp.Err != nil {
+ if len(imported[path]) > 0 {
+ imp.Err = errors.Augment(imp.Err, imported[path][0])
+ }
+ return imp.Err
+ }
+ imp.ImportPath = path
+ // imp.parent = inst
+ c.imports[path] = imp
+ // imp.parent = nil
+ } else if imp.parent != nil {
+ // TODO: report a standard cycle message.
+ // cycle is now handled explicitly in loader
+ }
+ paths[i] = imp.ImportPath
+
+ inst.addImport(imp)
+ if imp.Incomplete {
+ inst.Incomplete = true
+ }
+ }
+ }
+
+ inst.ImportPaths = paths
+ inst.ImportPos = imported
+
+ // Build full dependencies
+ deps := make(map[string]*Instance)
+ var q []*Instance
+ q = append(q, inst.Imports...)
+ for i := 0; i < len(q); i++ {
+ p1 := q[i]
+ path := p1.ImportPath
+ // The same import path could produce an error or not,
+ // depending on what tries to import it.
+ // Prefer to record entries with errors, so we can report them.
+ // p0 := deps[path]
+ // if err0, err1 := lastError(p0), lastError(p1); p0 == nil || err1 != nil && (err0 == nil || len(err0.ImportStack) > len(err1.ImportStack)) {
+ // deps[path] = p1
+ // for _, p2 := range p1.Imports {
+ // if deps[p2.ImportPath] != p2 {
+ // q = append(q, p2)
+ // }
+ // }
+ // }
+ if _, ok := deps[path]; !ok {
+ deps[path] = p1
+ }
+ }
+ inst.Deps = make([]string, 0, len(deps))
+ for dep := range deps {
+ inst.Deps = append(inst.Deps, dep)
+ }
+ sort.Strings(inst.Deps)
+
+ for _, dep := range inst.Deps {
+ p1 := deps[dep]
+ if p1 == nil {
+ panic("impossible: missing entry in package cache for " + dep + " imported by " + inst.ImportPath)
+ }
+ if p1.Err != nil {
+ inst.DepsErrors = append(inst.DepsErrors, p1.Err)
+ }
+ }
+
+ return nil
+}
diff --git a/cue/build/instance.go b/cue/build/instance.go
new file mode 100644
index 0000000..966d9ea
--- /dev/null
+++ b/cue/build/instance.go
@@ -0,0 +1,240 @@
+// Copyright 2018 The 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 build
+
+import (
+ "fmt"
+ pathpkg "path"
+ "path/filepath"
+ "strings"
+ "unicode"
+
+ "cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/errors"
+ "cuelang.org/go/cue/parser"
+ "cuelang.org/go/cue/token"
+)
+
+// An Instance describes the collection of files, and its imports, necessary
+// to build a CUE instance.
+//
+// A typical way to create an Instance is to use the loader package.
+type Instance struct {
+ ctxt *Context
+
+ // Files contains the AST for all files part of this instance.
+ Files []*ast.File
+
+ loadFunc LoadFunc
+ done bool
+
+ // Scope is another instance that may be used to resolve any unresolved
+ // reference of this instance. For instance, tool and test instances
+ // may refer to top-level fields in their package scope.
+ Scope *Instance
+
+ // PkgName is the name specified in the package clause.
+ PkgName string
+ hasName bool
+
+ // ImportPath returns the unique path to identify an imported instance.
+ //
+ // Instances created with NewInstance do not have an import path.
+ ImportPath string
+
+ // Imports lists the instances of all direct imports of this instance.
+ Imports []*Instance
+
+ // The Err for loading this package or nil on success. This does not
+ // include any errors of dependencies. Incomplete will be set if there
+ // were any errors in dependencies.
+ Err error
+
+ // Incomplete reports whether any dependencies had an error.
+ Incomplete bool
+
+ parent *Instance // TODO: for cycle detection
+
+ // The following fields are for informative purposes and are not used by
+ // the cue package to create an instance.
+
+ // ImportComment is the path in the import comment on the package statement.
+ ImportComment string
+
+ // DisplayPath is a user-friendly version of the package or import path.
+ DisplayPath string
+
+ // Dir is the package directory. Note that a package may also include files
+ // from ancestor directories, up to the module file.
+ Dir string
+
+ Root string // module root directory ("" if unknown)
+
+ // AllTags are the build tags that can influence file selection in this
+ // directory.
+ AllTags []string
+
+ Standard bool // Is a builtin package
+ Local bool
+ localPrefix string
+
+ // Relative to Dir
+ CUEFiles []string // .cue source files
+ DataFiles []string // recognized data files (.json, .yaml, etc.)
+ TestCUEFiles []string // .cue test files (_test.cue)
+ ToolCUEFiles []string // .cue tool files (_tool.cue)
+ IgnoredCUEFiles []string // .cue source files ignored for this build
+ InvalidCUEFiles []string // .cue source files with detected problems (parse error, wrong package name, and so on)
+
+ // Dependencies
+ ImportPaths []string
+ ImportPos map[string][]token.Position // line information for Imports
+
+ Deps []string
+ DepsErrors []error
+ Match []string
+}
+
+// Abs converts relative path used in the one of the file fields to an
+// absolute one.
+func (inst *Instance) Abs(path string) string {
+ if filepath.IsAbs(path) {
+ return path
+ }
+ return filepath.Join(inst.Root, path)
+}
+
+func (inst *Instance) chkErr(err error) error {
+ if err != nil {
+ inst.ReportError(err)
+ }
+ return err
+}
+
+func (inst *Instance) setPkg(pkg string) bool {
+ if !inst.hasName {
+ inst.hasName = true
+ inst.PkgName = pkg
+ return true
+ }
+ return false
+}
+
+// ReportError reports an error processing this instance.
+func (inst *Instance) ReportError(err error) {
+ if inst.Err == nil {
+ inst.Err = err
+ }
+}
+
+func (inst *Instance) errorf(pos token.Pos, format string, args ...interface{}) error {
+ return inst.chkErr(errors.E(inst.ctxt.Pos(pos), fmt.Sprintf(format, args...)))
+}
+
+// Context defines the build context for this instance. All files defined
+// in Syntax as well as all imported instances must be created using the
+// same build context.
+func (inst *Instance) Context() *Context {
+ return inst.ctxt
+}
+
+// LookupImport defines a mapping from an ImportSpec's ImportPath to Instance.
+func (inst *Instance) LookupImport(path string) *Instance {
+ path = inst.expandPath(path)
+ for _, inst := range inst.Imports {
+ if inst.ImportPath == path {
+ return inst
+ }
+ }
+ return nil
+}
+
+func (inst *Instance) addImport(imp *Instance) {
+ for _, inst := range inst.Imports {
+ if inst.ImportPath == imp.ImportPath {
+ if inst != imp {
+ panic("import added multiple times with different instances")
+ }
+ return
+ }
+ }
+ inst.Imports = append(inst.Imports, imp)
+}
+
+// AddFile adds the file with the given name to the list of files for this
+// instance. The file may be loaded from the cache of the instance's context.
+// It does not process the file's imports. The package name of the file must
+// match the package name of the instance.
+func (inst *Instance) AddFile(filename string, src interface{}) error {
+ c := inst.ctxt
+ file, err := parser.ParseFile(c.FileSet(), filename, src, c.parseOptions...)
+ if err == nil {
+ err = inst.addSyntax(file)
+ }
+ return inst.chkErr(err)
+}
+
+// addSyntax adds the given file to list of files for this instance. The package
+// name of the file must match the package name of the instance.
+func (inst *Instance) addSyntax(file *ast.File) error {
+ pkg := ""
+ pos := file.Pos()
+ if file.Name != nil {
+ pkg = file.Name.Name
+ pos = file.Name.Pos()
+ }
+ if !inst.setPkg(pkg) && pkg != inst.PkgName {
+ return inst.errorf(pos,
+ "package name %q conflicts with previous package name %q",
+ pkg, inst.PkgName)
+ }
+ inst.Files = append(inst.Files, file)
+ return nil
+}
+
+func (inst *Instance) expandPath(path string) string {
+ isLocal := IsLocalImport(path)
+ if isLocal {
+ path = dirToImportPath(filepath.Join(inst.Dir, path))
+ }
+ return path
+}
+
+// dirToImportPath returns the pseudo-import path we use for a package
+// outside the CUE path. It begins with _/ and then contains the full path
+// to the directory. If the package lives in c:\home\gopher\my\pkg then
+// the pseudo-import path is _/c_/home/gopher/my/pkg.
+// Using a pseudo-import path like this makes the ./ imports no longer
+// a special case, so that all the code to deal with ordinary imports works
+// automatically.
+func dirToImportPath(dir string) string {
+ return pathpkg.Join("_", strings.Map(makeImportValid, filepath.ToSlash(dir)))
+}
+
+func makeImportValid(r rune) rune {
+ // Should match Go spec, compilers, and ../../go/parser/parser.go:/isValidImport.
+ const illegalChars = `!"#$%&'()*,:;<=>?[\]^{|}` + "`\uFFFD"
+ if !unicode.IsGraphic(r) || unicode.IsSpace(r) || strings.ContainsRune(illegalChars, r) {
+ return '_'
+ }
+ return r
+}
+
+// IsLocalImport reports whether the import path is
+// a local import path, like ".", "..", "./foo", or "../foo".
+func IsLocalImport(path string) bool {
+ return path == "." || path == ".." ||
+ strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../")
+}