| // 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/ast/astutil" |
| "cuelang.org/go/cue/errors" |
| "cuelang.org/go/cue/parser" |
| "cuelang.org/go/cue/token" |
| "cuelang.org/go/internal" |
| ) |
| |
| // 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 cue/load package. |
| type Instance struct { |
| ctxt *Context |
| |
| BuildFiles []*File // files to be inclduded in the build |
| IgnoredFiles []*File // files excluded for this build |
| OrphanedFiles []*File // recognized file formats not part of any build |
| InvalidFiles []*File // could not parse these files |
| UnknownFiles []*File // unknown file types |
| |
| User bool // True if package was created from individual files. |
| |
| // Files contains the AST for all files part of this instance. |
| // TODO: the intent is to deprecate this in favor of BuildFiles. |
| Files []*ast.File |
| |
| loadFunc LoadFunc |
| done bool |
| |
| // 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 errors.Error |
| |
| 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. |
| |
| // DisplayPath is a user-friendly version of the package or import path. |
| DisplayPath string |
| |
| // Module defines the module name of a package. It must be defined if |
| // the packages within the directory structure of the module are to be |
| // imported by other packages, including those within the module. |
| Module string |
| |
| // Root is the root of the directory hierarchy, it may be "" if this an |
| // instance has no imports. |
| // If Module != "", this corresponds to the module root. |
| // Root/pkg is the directory that holds third-party packages. |
| Root string // root directory of hierarchy ("" if unknown) |
| |
| // Dir is the package directory. A package may also include files from |
| // ancestor directories, up to the module file. |
| Dir string |
| |
| // NOTICE: the below tags may change in the future. |
| |
| // ImportComment is the path in the import comment on the package statement. |
| ImportComment string `api:"alpha"` |
| |
| // AllTags are the build tags that can influence file selection in this |
| // directory. |
| AllTags []string `api:"alpha"` |
| |
| // Incomplete reports whether any dependencies had an error. |
| Incomplete bool `api:"alpha"` |
| |
| // Dependencies |
| // ImportPaths gives the transitive dependencies of all imports. |
| ImportPaths []string `api:"alpha"` |
| ImportPos map[string][]token.Pos `api:"alpha"` // line information for Imports |
| |
| Deps []string `api:"alpha"` |
| DepsErrors []error `api:"alpha"` |
| Match []string `api:"alpha"` |
| } |
| |
| // RelPath reports the path of f relative to the root of the instance's module |
| // directory. The full path is returned if a relative path could not be found. |
| func (inst *Instance) RelPath(f *File) string { |
| p, err := filepath.Rel(inst.Root, f.Filename) |
| if err != nil { |
| return f.Filename |
| } |
| return p |
| } |
| |
| // ID returns the package ID unique for this module. |
| func (inst *Instance) ID() string { |
| if s := inst.ImportPath; s != "" { |
| return s |
| } |
| if inst.PkgName == "" { |
| return "_" |
| } |
| s := fmt.Sprintf("%s:%s", inst.Module, inst.PkgName) |
| return s |
| } |
| |
| // Dependencies reports all Instances on which this instance depends. |
| func (inst *Instance) Dependencies() []*Instance { |
| // TODO: as cyclic dependencies are not allowed, we could just not check. |
| // Do for safety now and remove later if needed. |
| return appendDependencies(nil, inst, map[*Instance]bool{}) |
| } |
| |
| func appendDependencies(a []*Instance, inst *Instance, done map[*Instance]bool) []*Instance { |
| for _, d := range inst.Imports { |
| if done[d] { |
| continue |
| } |
| a = append(a, d) |
| done[d] = true |
| a = appendDependencies(a, d, done) |
| } |
| return a |
| } |
| |
| // 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) 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 errors.Error) { |
| inst.Err = errors.Append(inst.Err, err) |
| } |
| |
| // 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 |
| } |
| |
| func (inst *Instance) parse(name string, src interface{}) (*ast.File, error) { |
| if inst.ctxt != nil && inst.ctxt.parseFunc != nil { |
| return inst.ctxt.parseFunc(name, src) |
| } |
| return parser.ParseFile(name, src, parser.ParseComments) |
| } |
| |
| // 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. |
| // |
| // Deprecated: use AddSyntax or wait for this to be renamed using a new |
| // signature. |
| func (inst *Instance) AddFile(filename string, src interface{}) error { |
| file, err := inst.parse(filename, src) |
| if err != nil { |
| // should always be an errors.List, but just in case. |
| err := errors.Promote(err, "error adding file") |
| inst.ReportError(err) |
| return err |
| } |
| |
| return inst.AddSyntax(file) |
| } |
| |
| // 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) errors.Error { |
| astutil.Resolve(file, func(pos token.Pos, msg string, args ...interface{}) { |
| inst.Err = errors.Append(inst.Err, errors.Newf(pos, msg, args...)) |
| }) |
| _, pkg, pos := internal.PackageInfo(file) |
| if pkg != "" && pkg != "_" && !inst.setPkg(pkg) && pkg != inst.PkgName { |
| err := errors.Newf(pos, |
| "package name %q conflicts with previous package name %q", |
| pkg, inst.PkgName) |
| inst.ReportError(err) |
| return err |
| } |
| 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, "../") |
| } |