github.com/joomcode/cue@v0.4.4-0.20221111115225-539fe3512047/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 "github.com/joomcode/cue/cue/ast" 25 "github.com/joomcode/cue/cue/ast/astutil" 26 "github.com/joomcode/cue/cue/errors" 27 "github.com/joomcode/cue/cue/parser" 28 "github.com/joomcode/cue/cue/token" 29 "github.com/joomcode/cue/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 }