cuelang.org/go@v0.13.0/cue/load/loader_common.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 load 16 17 import ( 18 "cmp" 19 "maps" 20 pathpkg "path" 21 "path/filepath" 22 "slices" 23 "strconv" 24 "strings" 25 26 "cuelang.org/go/cue/build" 27 "cuelang.org/go/cue/errors" 28 "cuelang.org/go/cue/token" 29 ) 30 31 // An importMode controls the behavior of the Import method. 32 type importMode uint 33 34 const ( 35 allowAnonymous = 1 << iota 36 allowExcludedFiles 37 ) 38 39 var errExclude = errors.New("file rejected") 40 41 type cueError = errors.Error 42 type excludeError struct { 43 cueError 44 } 45 46 func (e excludeError) Is(err error) bool { return err == errExclude } 47 48 func rewriteFiles(p *build.Instance, root string, isLocal bool) { 49 p.Root = root 50 51 normalizeFiles(p.BuildFiles) 52 normalizeFiles(p.IgnoredFiles) 53 normalizeFiles(p.OrphanedFiles) 54 normalizeFiles(p.InvalidFiles) 55 normalizeFiles(p.UnknownFiles) 56 } 57 58 // normalizeFiles sorts the files so that files contained by a parent directory 59 // always come before files contained in sub-directories, and that filenames in 60 // the same directory are sorted lexically byte-wise, like Go's `<` operator. 61 func normalizeFiles(files []*build.File) { 62 slices.SortFunc(files, func(a, b *build.File) int { 63 fa := a.Filename 64 fb := b.Filename 65 ca := strings.Count(fa, string(filepath.Separator)) 66 cb := strings.Count(fb, string(filepath.Separator)) 67 if c := cmp.Compare(ca, cb); c != 0 { 68 return c 69 } 70 return cmp.Compare(fa, fb) 71 }) 72 } 73 74 func cleanImport(path string) string { 75 orig := path 76 path = pathpkg.Clean(path) 77 if strings.HasPrefix(orig, "./") && path != ".." && !strings.HasPrefix(path, "../") { 78 path = "./" + path 79 } 80 return path 81 } 82 83 // An importStack is a stack of import paths, possibly with the suffix " (test)" appended. 84 // The import path of a test package is the import path of the corresponding 85 // non-test package with the suffix "_test" added. 86 type importStack []string 87 88 func (s *importStack) Push(p string) { 89 *s = append(*s, p) 90 } 91 92 func (s *importStack) Pop() { 93 *s = (*s)[0 : len(*s)-1] 94 } 95 96 func (s *importStack) Copy() []string { 97 return slices.Clone(*s) 98 } 99 100 type fileProcessor struct { 101 firstFile string 102 imported map[string][]token.Pos 103 ignoreOther bool // ignore files from other packages 104 allPackages bool 105 106 c *fileProcessorConfig 107 tagger *tagger 108 pkgs map[string]*build.Instance 109 pkg *build.Instance 110 111 err errors.Error 112 } 113 114 type fileProcessorConfig = Config 115 116 func newFileProcessor(c *fileProcessorConfig, p *build.Instance, tg *tagger) *fileProcessor { 117 return &fileProcessor{ 118 imported: make(map[string][]token.Pos), 119 c: c, 120 pkgs: map[string]*build.Instance{"_": p}, 121 pkg: p, 122 tagger: tg, 123 } 124 } 125 126 func countCUEFiles(c *fileProcessorConfig, p *build.Instance) int { 127 count := len(p.BuildFiles) 128 for _, f := range p.IgnoredFiles { 129 if c.Tools && strings.HasSuffix(f.Filename, "_tool.cue") { 130 count++ 131 } 132 if c.Tests && strings.HasSuffix(f.Filename, "_test.cue") { 133 count++ 134 } 135 } 136 return count 137 } 138 139 func (fp *fileProcessor) finalize(p *build.Instance) errors.Error { 140 if fp.err != nil { 141 return fp.err 142 } 143 if countCUEFiles(fp.c, p) == 0 && 144 !fp.c.DataFiles && 145 (p.PkgName != "_" || !fp.allPackages) { 146 fp.err = errors.Append(fp.err, &NoFilesError{Package: p, ignored: len(p.IgnoredFiles) > 0}) 147 return fp.err 148 } 149 150 p.ImportPaths = slices.Sorted(maps.Keys(fp.imported)) 151 152 return nil 153 } 154 155 // add adds the given file to the appropriate package in fp. 156 func (fp *fileProcessor) add(root string, file *build.File, mode importMode) { 157 fullPath := file.Filename 158 if fullPath != "-" { 159 if !filepath.IsAbs(fullPath) { 160 fullPath = filepath.Join(root, fullPath) 161 } 162 file.Filename = fullPath 163 } 164 165 base := filepath.Base(fullPath) 166 167 // special * and _ 168 p := fp.pkg // default package 169 170 // sameDir holds whether the file should be considered to be 171 // part of the same directory as the default package. This is 172 // true when the file is part of the original package directory 173 // or when allowExcludedFiles is specified, signifying that the 174 // file is part of an explicit set of files provided on the 175 // command line. 176 sameDir := filepath.Dir(fullPath) == p.Dir || (mode&allowExcludedFiles) != 0 177 178 // badFile := func(p *build.Instance, err errors.Error) bool { 179 badFile := func(err errors.Error) { 180 fp.err = errors.Append(fp.err, err) 181 file.ExcludeReason = fp.err 182 p.InvalidFiles = append(p.InvalidFiles, file) 183 } 184 if err := setFileSource(fp.c, file); err != nil { 185 badFile(errors.Promote(err, "")) 186 return 187 } 188 189 if file.Encoding != build.CUE { 190 // Not a CUE file. 191 if sameDir { 192 p.OrphanedFiles = append(p.OrphanedFiles, file) 193 } 194 return 195 } 196 if (mode & allowExcludedFiles) == 0 { 197 var badPrefix string 198 for _, prefix := range []string{".", "_"} { 199 if strings.HasPrefix(base, prefix) { 200 badPrefix = prefix 201 } 202 } 203 if badPrefix != "" { 204 if !sameDir { 205 return 206 } 207 file.ExcludeReason = errors.Newf(token.NoPos, "filename starts with a '%s'", badPrefix) 208 if file.Interpretation == "" { 209 p.IgnoredFiles = append(p.IgnoredFiles, file) 210 } else { 211 p.OrphanedFiles = append(p.OrphanedFiles, file) 212 } 213 return 214 } 215 } 216 // Note: when path is "-" (stdin), it will already have 217 // been read and file.Source set to the resulting data 218 // by setFileSource. 219 pf, perr := fp.c.fileSystem.getCUESyntax(file) 220 if perr != nil { 221 badFile(errors.Promote(perr, "add failed")) 222 return 223 } 224 225 pkg := pf.PackageName() 226 if pkg == "" { 227 pkg = "_" 228 } 229 pos := pf.Pos() 230 231 switch { 232 case pkg == p.PkgName && (sameDir || pkg != "_"): 233 // We've got the exact package that's being looked for. 234 // It will already be present in fp.pkgs. 235 case mode&allowAnonymous != 0 && sameDir: 236 // It's an anonymous file that's not in a parent directory. 237 case fp.allPackages && pkg != "_": 238 q := fp.pkgs[pkg] 239 if q == nil && !sameDir { 240 // It's a file in a parent directory that doesn't correspond 241 // to a package in the original directory. 242 return 243 } 244 if q == nil { 245 q = fp.c.Context.NewInstance(p.Dir, nil) 246 q.PkgName = pkg 247 q.DisplayPath = p.DisplayPath 248 q.ImportPath = p.ImportPath + ":" + pkg 249 q.Root = p.Root 250 q.Module = p.Module 251 fp.pkgs[pkg] = q 252 } 253 p = q 254 255 case pkg != "_": 256 // We're loading a single package and we either haven't matched 257 // the earlier selected package or we haven't selected a package 258 // yet. In either case, the default package is the one we want to use. 259 default: 260 if sameDir { 261 file.ExcludeReason = excludeError{errors.Newf(pos, "no package name")} 262 p.IgnoredFiles = append(p.IgnoredFiles, file) 263 } 264 return 265 } 266 267 if !fp.c.AllCUEFiles { 268 tagIsSet := fp.tagger.tagIsSet 269 if p.Module != "" && p.Module != fp.c.Module { 270 // The file is outside the main module so treat all build tag keys as unset. 271 // Note that if there's no module, we don't consider it to be outside 272 // the main module, because otherwise @if tags in non-package files 273 // explicitly specified on the command line will not work. 274 tagIsSet = func(string) bool { 275 return false 276 } 277 } 278 if err := shouldBuildFile(pf, tagIsSet); err != nil { 279 if !errors.Is(err, errExclude) { 280 fp.err = errors.Append(fp.err, err) 281 } 282 file.ExcludeReason = err 283 p.IgnoredFiles = append(p.IgnoredFiles, file) 284 return 285 } 286 } 287 288 if pkg != "" && pkg != "_" { 289 if p.PkgName == "" { 290 p.PkgName = pkg 291 fp.firstFile = base 292 } else if pkg != p.PkgName { 293 if fp.ignoreOther { 294 file.ExcludeReason = excludeError{errors.Newf(pos, 295 "package is %s, want %s", pkg, p.PkgName)} 296 p.IgnoredFiles = append(p.IgnoredFiles, file) 297 return 298 } 299 if !fp.allPackages { 300 badFile(&MultiplePackageError{ 301 Dir: p.Dir, 302 Packages: []string{p.PkgName, pkg}, 303 Files: []string{fp.firstFile, base}, 304 }) 305 return 306 } 307 } 308 } 309 310 isTest := strings.HasSuffix(base, "_test"+cueSuffix) 311 isTool := strings.HasSuffix(base, "_tool"+cueSuffix) 312 313 for _, spec := range pf.Imports { 314 quoted := spec.Path.Value 315 path, err := strconv.Unquote(quoted) 316 if err != nil { 317 badFile(errors.Newf( 318 spec.Path.Pos(), 319 "%s: parser returned invalid quoted string: <%s>", fullPath, quoted, 320 )) 321 } 322 if !isTest || fp.c.Tests { 323 fp.imported[path] = append(fp.imported[path], spec.Pos()) 324 } 325 } 326 switch { 327 case isTest: 328 if fp.c.Tests { 329 p.BuildFiles = append(p.BuildFiles, file) 330 } else { 331 file.ExcludeReason = excludeError{errors.Newf(pos, 332 "_test.cue files excluded in non-test mode")} 333 p.IgnoredFiles = append(p.IgnoredFiles, file) 334 } 335 case isTool: 336 if fp.c.Tools { 337 p.BuildFiles = append(p.BuildFiles, file) 338 } else { 339 file.ExcludeReason = excludeError{errors.Newf(pos, 340 "_tool.cue files excluded in non-cmd mode")} 341 p.IgnoredFiles = append(p.IgnoredFiles, file) 342 } 343 default: 344 p.BuildFiles = append(p.BuildFiles, file) 345 } 346 } 347 348 // isLocalImport reports whether the import path is 349 // a local import path, like ".", "..", "./foo", or "../foo". 350 func isLocalImport(path string) bool { 351 return path == "." || path == ".." || 352 strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../") 353 } 354 355 // warnUnmatched warns about patterns that didn't match any packages. 356 func warnUnmatched(matches []*match) { 357 for _, m := range matches { 358 if len(m.Pkgs) == 0 { 359 m.Err = errors.Newf(token.NoPos, "cue: %q matched no packages", m.Pattern) 360 } 361 } 362 } 363 364 // cleanPatterns returns the patterns to use for the given 365 // command line. It canonicalizes the patterns but does not 366 // evaluate any matches. 367 func cleanPatterns(patterns []string) []string { 368 if len(patterns) == 0 { 369 return []string{"."} 370 } 371 var out []string 372 for _, a := range patterns { 373 // Arguments are supposed to be import paths, but 374 // as a courtesy to Windows developers, rewrite \ to / 375 // in command-line arguments. Handles .\... and so on. 376 if filepath.Separator == '\\' { 377 a = strings.Replace(a, `\`, `/`, -1) 378 } 379 380 // Put argument in canonical form, but preserve leading "./". 381 if strings.HasPrefix(a, "./") { 382 a = "./" + pathpkg.Clean(a) 383 if a == "./." { 384 a = "." 385 } 386 } else if a != "" { 387 a = pathpkg.Clean(a) 388 } 389 out = append(out, a) 390 } 391 return out 392 } 393 394 // isMetaPackage checks if name is a reserved package name that expands to multiple packages. 395 // TODO: none of these package names are actually recognized anywhere else 396 // and at least one (cmd) doesn't seem like it belongs in the CUE world. 397 func isMetaPackage(name string) bool { 398 return name == "std" || name == "cmd" || name == "all" 399 } 400 401 // hasFilepathPrefix reports whether the path s begins with the 402 // elements in prefix. 403 func hasFilepathPrefix(s, prefix string) bool { 404 switch { 405 default: 406 return false 407 case len(s) == len(prefix): 408 return s == prefix 409 case len(s) > len(prefix): 410 if prefix != "" && prefix[len(prefix)-1] == filepath.Separator { 411 return strings.HasPrefix(s, prefix) 412 } 413 return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == prefix 414 } 415 }