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 }