cuelang.org/go@v0.13.0/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 [Context.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 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 struct field tags may change in the future. 94 95 // Incomplete reports whether any dependencies had an error. 96 Incomplete bool `api:"alpha"` 97 98 // Dependencies 99 100 // ImportPaths gives the transitive dependencies of all imports. 101 ImportPaths []string `api:"alpha"` 102 ImportPos map[string][]token.Pos `api:"alpha"` // line information for Imports 103 104 Deps []string `api:"alpha"` 105 DepsErrors []error `api:"alpha"` 106 Match []string `api:"alpha"` 107 } 108 109 // RelPath reports the path of f relative to the root of the instance's module 110 // directory. The full path is returned if a relative path could not be found. 111 func (inst *Instance) RelPath(f *File) string { 112 p, err := filepath.Rel(inst.Root, f.Filename) 113 if err != nil { 114 return f.Filename 115 } 116 return p 117 } 118 119 // ID returns the package ID unique for this module. 120 func (inst *Instance) ID() string { 121 if s := inst.ImportPath; s != "" { 122 return s 123 } 124 if inst.PkgName == "" { 125 return "_" 126 } 127 s := fmt.Sprintf("%s:%s", inst.Module, inst.PkgName) 128 return s 129 } 130 131 // Dependencies reports all Instances on which this instance depends. 132 func (inst *Instance) Dependencies() []*Instance { 133 // TODO: as cyclic dependencies are not allowed, we could just not check. 134 // Do for safety now and remove later if needed. 135 return appendDependencies(nil, inst, map[*Instance]bool{}) 136 } 137 138 func appendDependencies(a []*Instance, inst *Instance, done map[*Instance]bool) []*Instance { 139 for _, d := range inst.Imports { 140 if done[d] { 141 continue 142 } 143 a = append(a, d) 144 done[d] = true 145 a = appendDependencies(a, d, done) 146 } 147 return a 148 } 149 150 // Abs converts relative path used in the one of the file fields to an 151 // absolute one. 152 func (inst *Instance) Abs(path string) string { 153 if filepath.IsAbs(path) { 154 return path 155 } 156 return filepath.Join(inst.Root, path) 157 } 158 159 func (inst *Instance) setPkg(pkg string) bool { 160 if !inst.hasName { 161 inst.hasName = true 162 inst.PkgName = pkg 163 return true 164 } 165 return false 166 } 167 168 // ReportError reports an error processing this instance. 169 func (inst *Instance) ReportError(err errors.Error) { 170 inst.Err = errors.Append(inst.Err, err) 171 } 172 173 // Context defines the build context for this instance. All files defined 174 // in Syntax as well as all imported instances must be created using the 175 // same build context. 176 func (inst *Instance) Context() *Context { 177 return inst.ctxt 178 } 179 180 func (inst *Instance) parse(name string, src interface{}) (*ast.File, error) { 181 if inst.ctxt != nil && inst.ctxt.parseFunc != nil { 182 return inst.ctxt.parseFunc(name, src) 183 } 184 return parser.ParseFile(name, src, parser.ParseComments) 185 } 186 187 // LookupImport defines a mapping from an ImportSpec's ImportPath to Instance. 188 func (inst *Instance) LookupImport(path string) *Instance { 189 path = inst.expandPath(path) 190 for _, inst := range inst.Imports { 191 if inst.ImportPath == path { 192 return inst 193 } 194 } 195 return nil 196 } 197 198 func (inst *Instance) addImport(imp *Instance) { 199 for _, inst := range inst.Imports { 200 if inst.ImportPath == imp.ImportPath { 201 if inst != imp { 202 panic("import added multiple times with different instances") 203 } 204 return 205 } 206 } 207 inst.Imports = append(inst.Imports, imp) 208 } 209 210 // AddFile adds the file with the given name to the list of files for this 211 // instance. The file may be loaded from the cache of the instance's context. 212 // It does not process the file's imports. The package name of the file must 213 // match the package name of the instance. 214 // 215 // Deprecated: use [Instance.AddSyntax] or wait for this to be renamed using a new 216 // signature. 217 func (inst *Instance) AddFile(filename string, src interface{}) error { 218 file, err := inst.parse(filename, src) 219 if err != nil { 220 // should always be an errors.List, but just in case. 221 err := errors.Promote(err, "error adding file") 222 inst.ReportError(err) 223 return err 224 } 225 226 return inst.AddSyntax(file) 227 } 228 229 // AddSyntax adds the given file to list of files for this instance. The package 230 // name of the file must match the package name of the instance. 231 func (inst *Instance) AddSyntax(file *ast.File) errors.Error { 232 astutil.Resolve(file, func(pos token.Pos, msg string, args ...interface{}) { 233 inst.Err = errors.Append(inst.Err, errors.Newf(pos, msg, args...)) 234 }) 235 pkg := file.PackageName() 236 if pkg != "" && pkg != "_" && !inst.User && !inst.setPkg(pkg) && pkg != inst.PkgName { 237 err := errors.Newf(file.Pos(), 238 "package name %q conflicts with previous package name %q", 239 pkg, inst.PkgName) 240 inst.ReportError(err) 241 return err 242 } 243 inst.Files = append(inst.Files, file) 244 return nil 245 } 246 247 func (inst *Instance) expandPath(path string) string { 248 isLocal := IsLocalImport(path) 249 if isLocal { 250 path = dirToImportPath(filepath.Join(inst.Dir, path)) 251 } 252 return path 253 } 254 255 // dirToImportPath returns the pseudo-import path we use for a package 256 // outside the CUE path. It begins with _/ and then contains the full path 257 // to the directory. If the package lives in c:\home\gopher\my\pkg then 258 // the pseudo-import path is _/c_/home/gopher/my/pkg. 259 // Using a pseudo-import path like this makes the ./ imports no longer 260 // a special case, so that all the code to deal with ordinary imports works 261 // automatically. 262 func dirToImportPath(dir string) string { 263 return pathpkg.Join("_", strings.Map(makeImportValid, filepath.ToSlash(dir))) 264 } 265 266 func makeImportValid(r rune) rune { 267 // Should match Go spec, compilers, and ../../go/parser/parser.go:/isValidImport. 268 const illegalChars = `!"#$%&'()*,:;<=>?[\]^{|}` + "`\uFFFD" 269 if !unicode.IsGraphic(r) || unicode.IsSpace(r) || strings.ContainsRune(illegalChars, r) { 270 return '_' 271 } 272 return r 273 } 274 275 // IsLocalImport reports whether the import path is 276 // a local import path, like ".", "..", "./foo", or "../foo". 277 func IsLocalImport(path string) bool { 278 return path == "." || path == ".." || 279 strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../") 280 }