...

Source file src/cuelang.org/go/cue/build/instance.go

Documentation: cuelang.org/go/cue/build

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

View as plain text