cuelang.org/go@v0.10.1/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 pathpkg "path" 20 "path/filepath" 21 "slices" 22 "sort" 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, _ = cleanImports(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 return 184 } 185 if err := setFileSource(fp.c, file); err != nil { 186 badFile(errors.Promote(err, "")) 187 return 188 } 189 190 if file.Encoding != build.CUE { 191 // Not a CUE file. 192 if sameDir { 193 p.OrphanedFiles = append(p.OrphanedFiles, file) 194 } 195 return 196 } 197 if (mode & allowExcludedFiles) == 0 { 198 var badPrefix string 199 for _, prefix := range []string{".", "_"} { 200 if strings.HasPrefix(base, prefix) { 201 badPrefix = prefix 202 } 203 } 204 if badPrefix != "" { 205 if !sameDir { 206 return 207 } 208 file.ExcludeReason = errors.Newf(token.NoPos, "filename starts with a '%s'", badPrefix) 209 if file.Interpretation == "" { 210 p.IgnoredFiles = append(p.IgnoredFiles, file) 211 } else { 212 p.OrphanedFiles = append(p.OrphanedFiles, file) 213 } 214 return 215 } 216 } 217 // Note: when path is "-" (stdin), it will already have 218 // been read and file.Source set to the resulting data 219 // by setFileSource. 220 pf, perr := fp.c.fileSystem.getCUESyntax(file) 221 if perr != nil { 222 badFile(errors.Promote(perr, "add failed")) 223 return 224 } 225 226 pkg := pf.PackageName() 227 if pkg == "" { 228 pkg = "_" 229 } 230 pos := pf.Pos() 231 232 switch { 233 case pkg == p.PkgName && (sameDir || pkg != "_"): 234 // We've got the exact package that's being looked for. 235 // It will already be present in fp.pkgs. 236 case mode&allowAnonymous != 0 && sameDir: 237 // It's an anonymous file that's not in a parent directory. 238 case fp.allPackages && pkg != "_": 239 q := fp.pkgs[pkg] 240 if q == nil && !sameDir { 241 // It's a file in a parent directory that doesn't correspond 242 // to a package in the original directory. 243 return 244 } 245 if q == nil { 246 q = &build.Instance{ 247 PkgName: pkg, 248 249 Dir: p.Dir, 250 DisplayPath: p.DisplayPath, 251 ImportPath: p.ImportPath + ":" + pkg, 252 Root: p.Root, 253 Module: p.Module, 254 } 255 fp.pkgs[pkg] = q 256 } 257 p = q 258 259 case pkg != "_": 260 // We're loading a single package and we either haven't matched 261 // the earlier selected package or we haven't selected a package 262 // yet. In either case, the default package is the one we want to use. 263 default: 264 if sameDir { 265 file.ExcludeReason = excludeError{errors.Newf(pos, "no package name")} 266 p.IgnoredFiles = append(p.IgnoredFiles, file) 267 } 268 return 269 } 270 271 if !fp.c.AllCUEFiles { 272 tagIsSet := fp.tagger.tagIsSet 273 if p.Module != "" && p.Module != fp.c.Module { 274 // The file is outside the main module so treat all build tag keys as unset. 275 // Note that if there's no module, we don't consider it to be outside 276 // the main module, because otherwise @if tags in non-package files 277 // explicitly specified on the command line will not work. 278 tagIsSet = func(string) bool { 279 return false 280 } 281 } 282 if err := shouldBuildFile(pf, tagIsSet); err != nil { 283 if !errors.Is(err, errExclude) { 284 fp.err = errors.Append(fp.err, err) 285 } 286 file.ExcludeReason = err 287 p.IgnoredFiles = append(p.IgnoredFiles, file) 288 return 289 } 290 } 291 292 if pkg != "" && pkg != "_" { 293 if p.PkgName == "" { 294 p.PkgName = pkg 295 fp.firstFile = base 296 } else if pkg != p.PkgName { 297 if fp.ignoreOther { 298 file.ExcludeReason = excludeError{errors.Newf(pos, 299 "package is %s, want %s", pkg, p.PkgName)} 300 p.IgnoredFiles = append(p.IgnoredFiles, file) 301 return 302 } 303 if !fp.allPackages { 304 badFile(&MultiplePackageError{ 305 Dir: p.Dir, 306 Packages: []string{p.PkgName, pkg}, 307 Files: []string{fp.firstFile, base}, 308 }) 309 return 310 } 311 } 312 } 313 314 isTest := strings.HasSuffix(base, "_test"+cueSuffix) 315 isTool := strings.HasSuffix(base, "_tool"+cueSuffix) 316 317 for _, spec := range pf.Imports { 318 quoted := spec.Path.Value 319 path, err := strconv.Unquote(quoted) 320 if err != nil { 321 badFile(errors.Newf( 322 spec.Path.Pos(), 323 "%s: parser returned invalid quoted string: <%s>", fullPath, quoted, 324 )) 325 } 326 if !isTest || fp.c.Tests { 327 fp.imported[path] = append(fp.imported[path], spec.Pos()) 328 } 329 } 330 switch { 331 case isTest: 332 if fp.c.Tests { 333 p.BuildFiles = append(p.BuildFiles, file) 334 } else { 335 file.ExcludeReason = excludeError{errors.Newf(pos, 336 "_test.cue files excluded in non-test mode")} 337 p.IgnoredFiles = append(p.IgnoredFiles, file) 338 } 339 case isTool: 340 if fp.c.Tools { 341 p.BuildFiles = append(p.BuildFiles, file) 342 } else { 343 file.ExcludeReason = excludeError{errors.Newf(pos, 344 "_tool.cue files excluded in non-cmd mode")} 345 p.IgnoredFiles = append(p.IgnoredFiles, file) 346 } 347 default: 348 p.BuildFiles = append(p.BuildFiles, file) 349 } 350 } 351 352 func cleanImports(m map[string][]token.Pos) ([]string, map[string][]token.Pos) { 353 all := make([]string, 0, len(m)) 354 for path := range m { 355 all = append(all, path) 356 } 357 sort.Strings(all) 358 return all, m 359 } 360 361 // isLocalImport reports whether the import path is 362 // a local import path, like ".", "..", "./foo", or "../foo". 363 func isLocalImport(path string) bool { 364 return path == "." || path == ".." || 365 strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../") 366 } 367 368 // warnUnmatched warns about patterns that didn't match any packages. 369 func warnUnmatched(matches []*match) { 370 for _, m := range matches { 371 if len(m.Pkgs) == 0 { 372 m.Err = errors.Newf(token.NoPos, "cue: %q matched no packages", m.Pattern) 373 } 374 } 375 } 376 377 // cleanPatterns returns the patterns to use for the given 378 // command line. It canonicalizes the patterns but does not 379 // evaluate any matches. 380 func cleanPatterns(patterns []string) []string { 381 if len(patterns) == 0 { 382 return []string{"."} 383 } 384 var out []string 385 for _, a := range patterns { 386 // Arguments are supposed to be import paths, but 387 // as a courtesy to Windows developers, rewrite \ to / 388 // in command-line arguments. Handles .\... and so on. 389 if filepath.Separator == '\\' { 390 a = strings.Replace(a, `\`, `/`, -1) 391 } 392 393 // Put argument in canonical form, but preserve leading "./". 394 if strings.HasPrefix(a, "./") { 395 a = "./" + pathpkg.Clean(a) 396 if a == "./." { 397 a = "." 398 } 399 } else if a != "" { 400 a = pathpkg.Clean(a) 401 } 402 out = append(out, a) 403 } 404 return out 405 } 406 407 // isMetaPackage checks if name is a reserved package name that expands to multiple packages. 408 // TODO: none of these package names are actually recognized anywhere else 409 // and at least one (cmd) doesn't seem like it belongs in the CUE world. 410 func isMetaPackage(name string) bool { 411 return name == "std" || name == "cmd" || name == "all" 412 } 413 414 // hasFilepathPrefix reports whether the path s begins with the 415 // elements in prefix. 416 func hasFilepathPrefix(s, prefix string) bool { 417 switch { 418 default: 419 return false 420 case len(s) == len(prefix): 421 return s == prefix 422 case len(s) > len(prefix): 423 if prefix != "" && prefix[len(prefix)-1] == filepath.Separator { 424 return strings.HasPrefix(s, prefix) 425 } 426 return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == prefix 427 } 428 }