cuelang.org/go@v0.10.1/cue/build/instance.go (about)

     1  // Copyright 2018 The CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package build
    16  
    17  import (
    18  	"fmt"
    19  	pathpkg "path"
    20  	"path/filepath"
    21  	"strings"
    22  	"unicode"
    23  
    24  	"cuelang.org/go/cue/ast"
    25  	"cuelang.org/go/cue/ast/astutil"
    26  	"cuelang.org/go/cue/errors"
    27  	"cuelang.org/go/cue/parser"
    28  	"cuelang.org/go/cue/token"
    29  )
    30  
    31  // An Instance describes the collection of files, and its imports, necessary
    32  // to build a CUE instance.
    33  //
    34  // A typical way to create an Instance is to use the cue/load package.
    35  type Instance struct {
    36  	ctxt *Context
    37  
    38  	BuildFiles    []*File // files to be included in the build
    39  	IgnoredFiles  []*File // files excluded for this build
    40  	OrphanedFiles []*File // recognized file formats not part of any build
    41  	InvalidFiles  []*File // could not parse these files
    42  	UnknownFiles  []*File // unknown file types
    43  
    44  	User bool // True if package was created from individual files.
    45  
    46  	// Files contains the AST for all files part of this instance.
    47  	// TODO: the intent is to deprecate this in favor of BuildFiles.
    48  	Files []*ast.File
    49  
    50  	loadFunc LoadFunc
    51  	done     bool
    52  
    53  	// PkgName is the name specified in the package clause.
    54  	PkgName string
    55  	hasName bool
    56  
    57  	// ImportPath returns the unique path to identify an imported instance.
    58  	//
    59  	// Instances created with NewInstance do not have an import path.
    60  	ImportPath string
    61  
    62  	// Imports lists the instances of all direct imports of this instance.
    63  	Imports []*Instance
    64  
    65  	// The Err for loading this package or nil on success. This does not
    66  	// include any errors of dependencies. Incomplete will be set if there
    67  	// were any errors in dependencies.
    68  	Err errors.Error
    69  
    70  	parent *Instance // TODO: for cycle detection
    71  
    72  	// The following fields are for informative purposes and are not used by
    73  	// the cue package to create an instance.
    74  
    75  	// DisplayPath is a user-friendly version of the package or import path.
    76  	DisplayPath string
    77  
    78  	// Module defines the module name of a package. It must be defined if
    79  	// the packages within the directory structure of the module are to be
    80  	// imported by other packages, including those within the module.
    81  	Module string
    82  
    83  	// Root is the root of the directory hierarchy, it may be "" if this an
    84  	// instance has no imports.
    85  	// If Module != "", this corresponds to the module root.
    86  	// Root/pkg is the directory that holds third-party packages.
    87  	Root string // root directory of hierarchy ("" if unknown)
    88  
    89  	// Dir is the package directory. A package may also include files from
    90  	// ancestor directories, up to the module file.
    91  	Dir string
    92  
    93  	// NOTICE: the below tags may change in the future.
    94  
    95  	// ImportComment is the path in the import comment on the package statement.
    96  	//
    97  	// Deprecated: CUE has never needed or supported import comments.
    98  	ImportComment string `api:"alpha"`
    99  
   100  	// AllTags are the build tags that can influence file selection in this
   101  	// directory.
   102  	//
   103  	// Deprecated: this field is not used.
   104  	AllTags []string `api:"alpha"`
   105  
   106  	// Incomplete reports whether any dependencies had an error.
   107  	Incomplete bool `api:"alpha"`
   108  
   109  	// Dependencies
   110  	// ImportPaths gives the transitive dependencies of all imports.
   111  	ImportPaths []string               `api:"alpha"`
   112  	ImportPos   map[string][]token.Pos `api:"alpha"` // line information for Imports
   113  
   114  	Deps       []string `api:"alpha"`
   115  	DepsErrors []error  `api:"alpha"`
   116  	Match      []string `api:"alpha"`
   117  }
   118  
   119  // RelPath reports the path of f relative to the root of the instance's module
   120  // directory. The full path is returned if a relative path could not be found.
   121  func (inst *Instance) RelPath(f *File) string {
   122  	p, err := filepath.Rel(inst.Root, f.Filename)
   123  	if err != nil {
   124  		return f.Filename
   125  	}
   126  	return p
   127  }
   128  
   129  // ID returns the package ID unique for this module.
   130  func (inst *Instance) ID() string {
   131  	if s := inst.ImportPath; s != "" {
   132  		return s
   133  	}
   134  	if inst.PkgName == "" {
   135  		return "_"
   136  	}
   137  	s := fmt.Sprintf("%s:%s", inst.Module, inst.PkgName)
   138  	return s
   139  }
   140  
   141  // Dependencies reports all Instances on which this instance depends.
   142  func (inst *Instance) Dependencies() []*Instance {
   143  	// TODO: as cyclic dependencies are not allowed, we could just not check.
   144  	// Do for safety now and remove later if needed.
   145  	return appendDependencies(nil, inst, map[*Instance]bool{})
   146  }
   147  
   148  func appendDependencies(a []*Instance, inst *Instance, done map[*Instance]bool) []*Instance {
   149  	for _, d := range inst.Imports {
   150  		if done[d] {
   151  			continue
   152  		}
   153  		a = append(a, d)
   154  		done[d] = true
   155  		a = appendDependencies(a, d, done)
   156  	}
   157  	return a
   158  }
   159  
   160  // Abs converts relative path used in the one of the file fields to an
   161  // absolute one.
   162  func (inst *Instance) Abs(path string) string {
   163  	if filepath.IsAbs(path) {
   164  		return path
   165  	}
   166  	return filepath.Join(inst.Root, path)
   167  }
   168  
   169  func (inst *Instance) setPkg(pkg string) bool {
   170  	if !inst.hasName {
   171  		inst.hasName = true
   172  		inst.PkgName = pkg
   173  		return true
   174  	}
   175  	return false
   176  }
   177  
   178  // ReportError reports an error processing this instance.
   179  func (inst *Instance) ReportError(err errors.Error) {
   180  	inst.Err = errors.Append(inst.Err, err)
   181  }
   182  
   183  // Context defines the build context for this instance. All files defined
   184  // in Syntax as well as all imported instances must be created using the
   185  // same build context.
   186  func (inst *Instance) Context() *Context {
   187  	return inst.ctxt
   188  }
   189  
   190  func (inst *Instance) parse(name string, src interface{}) (*ast.File, error) {
   191  	if inst.ctxt != nil && inst.ctxt.parseFunc != nil {
   192  		return inst.ctxt.parseFunc(name, src)
   193  	}
   194  	return parser.ParseFile(name, src, parser.ParseComments)
   195  }
   196  
   197  // LookupImport defines a mapping from an ImportSpec's ImportPath to Instance.
   198  func (inst *Instance) LookupImport(path string) *Instance {
   199  	path = inst.expandPath(path)
   200  	for _, inst := range inst.Imports {
   201  		if inst.ImportPath == path {
   202  			return inst
   203  		}
   204  	}
   205  	return nil
   206  }
   207  
   208  func (inst *Instance) addImport(imp *Instance) {
   209  	for _, inst := range inst.Imports {
   210  		if inst.ImportPath == imp.ImportPath {
   211  			if inst != imp {
   212  				panic("import added multiple times with different instances")
   213  			}
   214  			return
   215  		}
   216  	}
   217  	inst.Imports = append(inst.Imports, imp)
   218  }
   219  
   220  // AddFile adds the file with the given name to the list of files for this
   221  // instance. The file may be loaded from the cache of the instance's context.
   222  // It does not process the file's imports. The package name of the file must
   223  // match the package name of the instance.
   224  //
   225  // Deprecated: use AddSyntax or wait for this to be renamed using a new
   226  // signature.
   227  func (inst *Instance) AddFile(filename string, src interface{}) error {
   228  	file, err := inst.parse(filename, src)
   229  	if err != nil {
   230  		// should always be an errors.List, but just in case.
   231  		err := errors.Promote(err, "error adding file")
   232  		inst.ReportError(err)
   233  		return err
   234  	}
   235  
   236  	return inst.AddSyntax(file)
   237  }
   238  
   239  // AddSyntax adds the given file to list of files for this instance. The package
   240  // name of the file must match the package name of the instance.
   241  func (inst *Instance) AddSyntax(file *ast.File) errors.Error {
   242  	astutil.Resolve(file, func(pos token.Pos, msg string, args ...interface{}) {
   243  		inst.Err = errors.Append(inst.Err, errors.Newf(pos, msg, args...))
   244  	})
   245  	pkg := file.PackageName()
   246  	if pkg != "" && pkg != "_" && !inst.User && !inst.setPkg(pkg) && pkg != inst.PkgName {
   247  		err := errors.Newf(file.Pos(),
   248  			"package name %q conflicts with previous package name %q",
   249  			pkg, inst.PkgName)
   250  		inst.ReportError(err)
   251  		return err
   252  	}
   253  	inst.Files = append(inst.Files, file)
   254  	return nil
   255  }
   256  
   257  func (inst *Instance) expandPath(path string) string {
   258  	isLocal := IsLocalImport(path)
   259  	if isLocal {
   260  		path = dirToImportPath(filepath.Join(inst.Dir, path))
   261  	}
   262  	return path
   263  }
   264  
   265  // dirToImportPath returns the pseudo-import path we use for a package
   266  // outside the CUE path. It begins with _/ and then contains the full path
   267  // to the directory. If the package lives in c:\home\gopher\my\pkg then
   268  // the pseudo-import path is _/c_/home/gopher/my/pkg.
   269  // Using a pseudo-import path like this makes the ./ imports no longer
   270  // a special case, so that all the code to deal with ordinary imports works
   271  // automatically.
   272  func dirToImportPath(dir string) string {
   273  	return pathpkg.Join("_", strings.Map(makeImportValid, filepath.ToSlash(dir)))
   274  }
   275  
   276  func makeImportValid(r rune) rune {
   277  	// Should match Go spec, compilers, and ../../go/parser/parser.go:/isValidImport.
   278  	const illegalChars = `!"#$%&'()*,:;<=>?[\]^{|}` + "`\uFFFD"
   279  	if !unicode.IsGraphic(r) || unicode.IsSpace(r) || strings.ContainsRune(illegalChars, r) {
   280  		return '_'
   281  	}
   282  	return r
   283  }
   284  
   285  // IsLocalImport reports whether the import path is
   286  // a local import path, like ".", "..", "./foo", or "../foo".
   287  func IsLocalImport(path string) bool {
   288  	return path == "." || path == ".." ||
   289  		strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../")
   290  }