github.com/joomcode/cue@v0.4.4-0.20221111115225-539fe3512047/cue/load/import.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 "bytes" 19 "os" 20 "path/filepath" 21 "sort" 22 "strconv" 23 "strings" 24 "unicode" 25 "unicode/utf8" 26 27 "github.com/joomcode/cue/cue/ast" 28 "github.com/joomcode/cue/cue/build" 29 "github.com/joomcode/cue/cue/errors" 30 "github.com/joomcode/cue/cue/parser" 31 "github.com/joomcode/cue/cue/token" 32 "github.com/joomcode/cue/internal" 33 "github.com/joomcode/cue/internal/filetypes" 34 ) 35 36 // An importMode controls the behavior of the Import method. 37 type importMode uint 38 39 const ( 40 // If findOnly is set, Import stops after locating the directory 41 // that should contain the sources for a package. It does not 42 // read any files in the directory. 43 findOnly importMode = 1 << iota 44 45 // If importComment is set, parse import comments on package statements. 46 // Import returns an error if it finds a comment it cannot understand 47 // or finds conflicting comments in multiple source files. 48 // See golang.org/s/go14customimport for more information. 49 importComment 50 51 allowAnonymous 52 ) 53 54 // importPkg returns details about the CUE package named by the import path, 55 // interpreting local import paths relative to the srcDir directory. 56 // If the path is a local import path naming a package that can be imported 57 // using a standard import path, the returned package will set p.ImportPath 58 // to that path. 59 // 60 // In the directory and ancestor directories up to including one with a 61 // cue.mod file, all .cue files are considered part of the package except for: 62 // 63 // - files starting with _ or . (likely editor temporary files) 64 // - files with build constraints not satisfied by the context 65 // 66 // If an error occurs, importPkg sets the error in the returned instance, 67 // which then may contain partial information. 68 // 69 // pkgName indicates which packages to load. It supports the following 70 // values: 71 // "" the default package for the directory, if only one 72 // is present. 73 // _ anonymous files (which may be marked with _) 74 // * all packages 75 // 76 func (l *loader) importPkg(pos token.Pos, p *build.Instance) []*build.Instance { 77 l.stk.Push(p.ImportPath) 78 defer l.stk.Pop() 79 80 cfg := l.cfg 81 ctxt := &cfg.fileSystem 82 83 if p.Err != nil { 84 return []*build.Instance{p} 85 } 86 87 retErr := func(errs errors.Error) []*build.Instance { 88 // XXX: move this loop to ReportError 89 for _, err := range errors.Errors(errs) { 90 p.ReportError(err) 91 } 92 return []*build.Instance{p} 93 } 94 95 if !strings.HasPrefix(p.Dir, cfg.ModuleRoot) { 96 err := errors.Newf(token.NoPos, "module root not defined", p.DisplayPath) 97 return retErr(err) 98 } 99 100 fp := newFileProcessor(cfg, p) 101 102 if p.PkgName == "" { 103 if l.cfg.Package == "*" { 104 fp.ignoreOther = true 105 fp.allPackages = true 106 p.PkgName = "_" 107 } else { 108 p.PkgName = l.cfg.Package 109 } 110 } 111 if p.PkgName != "" { 112 // If we have an explicit package name, we can ignore other packages. 113 fp.ignoreOther = true 114 } 115 116 if !strings.HasPrefix(p.Dir, cfg.ModuleRoot) { 117 panic("") 118 } 119 120 var dirs [][2]string 121 genDir := GenPath(cfg.ModuleRoot) 122 if strings.HasPrefix(p.Dir, genDir) { 123 dirs = append(dirs, [2]string{genDir, p.Dir}) 124 // TODO(legacy): don't support "pkg" 125 // && p.PkgName != "_" 126 if filepath.Base(genDir) != "pkg" { 127 for _, sub := range []string{"pkg", "usr"} { 128 rel, err := filepath.Rel(genDir, p.Dir) 129 if err != nil { 130 // should not happen 131 return retErr( 132 errors.Wrapf(err, token.NoPos, "invalid path")) 133 } 134 base := filepath.Join(cfg.ModuleRoot, modDir, sub) 135 dir := filepath.Join(base, rel) 136 dirs = append(dirs, [2]string{base, dir}) 137 } 138 } 139 } else { 140 dirs = append(dirs, [2]string{cfg.ModuleRoot, p.Dir}) 141 } 142 143 found := false 144 for _, d := range dirs { 145 info, err := ctxt.stat(d[1]) 146 if err == nil && info.IsDir() { 147 found = true 148 break 149 } 150 } 151 152 if !found { 153 return retErr( 154 &PackageError{ 155 Message: errors.NewMessage("cannot find package %q", 156 []interface{}{p.DisplayPath}), 157 }) 158 } 159 160 // This algorithm assumes that multiple directories within cue.mod/*/ 161 // have the same module scope and that there are no invalid modules. 162 inModule := false // if pkg == "_" 163 for _, d := range dirs { 164 if l.cfg.findRoot(d[1]) != "" { 165 inModule = true 166 break 167 } 168 } 169 170 for _, d := range dirs { 171 for dir := filepath.Clean(d[1]); ctxt.isDir(dir); { 172 files, err := ctxt.readDir(dir) 173 if err != nil && !os.IsNotExist(err) { 174 return retErr(errors.Wrapf(err, pos, "import failed reading dir %v", dirs[0][1])) 175 } 176 for _, f := range files { 177 if f.IsDir() { 178 continue 179 } 180 if f.Name() == "-" { 181 if _, err := cfg.fileSystem.stat("-"); !os.IsNotExist(err) { 182 continue 183 } 184 } 185 file, err := filetypes.ParseFile(f.Name(), filetypes.Input) 186 if err != nil { 187 p.UnknownFiles = append(p.UnknownFiles, &build.File{ 188 Filename: f.Name(), 189 ExcludeReason: errors.Newf(token.NoPos, "unknown filetype"), 190 }) 191 continue // skip unrecognized file types 192 } 193 fp.add(pos, dir, file, importComment) 194 } 195 196 if p.PkgName == "" || !inModule || l.cfg.isRoot(dir) || dir == d[0] { 197 break 198 } 199 200 // From now on we just ignore files that do not belong to the same 201 // package. 202 fp.ignoreOther = true 203 204 parent, _ := filepath.Split(dir) 205 parent = filepath.Clean(parent) 206 207 if parent == dir || len(parent) < len(d[0]) { 208 break 209 } 210 dir = parent 211 } 212 } 213 214 all := []*build.Instance{} 215 216 for _, p := range fp.pkgs { 217 impPath, err := addImportQualifier(importPath(p.ImportPath), p.PkgName) 218 p.ImportPath = string(impPath) 219 if err != nil { 220 p.ReportError(err) 221 } 222 223 all = append(all, p) 224 rewriteFiles(p, cfg.ModuleRoot, false) 225 if errs := fp.finalize(p); errs != nil { 226 p.ReportError(errs) 227 return all 228 } 229 230 l.addFiles(cfg.ModuleRoot, p) 231 _ = p.Complete() 232 } 233 sort.Slice(all, func(i, j int) bool { 234 return all[i].Dir < all[j].Dir 235 }) 236 return all 237 } 238 239 // loadFunc creates a LoadFunc that can be used to create new build.Instances. 240 func (l *loader) loadFunc() build.LoadFunc { 241 242 return func(pos token.Pos, path string) *build.Instance { 243 cfg := l.cfg 244 245 impPath := importPath(path) 246 if isLocalImport(path) { 247 return cfg.newErrInstance(pos, impPath, 248 errors.Newf(pos, "relative import paths not allowed (%q)", path)) 249 } 250 251 // is it a builtin? 252 if strings.IndexByte(strings.Split(path, "/")[0], '.') == -1 { 253 if l.cfg.StdRoot != "" { 254 p := cfg.newInstance(pos, impPath) 255 _ = l.importPkg(pos, p) 256 return p 257 } 258 return nil 259 } 260 261 p := cfg.newInstance(pos, impPath) 262 _ = l.importPkg(pos, p) 263 return p 264 } 265 } 266 267 func rewriteFiles(p *build.Instance, root string, isLocal bool) { 268 p.Root = root 269 270 normalizeFiles(p.BuildFiles) 271 normalizeFiles(p.IgnoredFiles) 272 normalizeFiles(p.OrphanedFiles) 273 normalizeFiles(p.InvalidFiles) 274 normalizeFiles(p.UnknownFiles) 275 } 276 277 func normalizeFiles(a []*build.File) { 278 sort.Slice(a, func(i, j int) bool { 279 return len(filepath.Dir(a[i].Filename)) < len(filepath.Dir(a[j].Filename)) 280 }) 281 } 282 283 type fileProcessor struct { 284 firstFile string 285 firstCommentFile string 286 imported map[string][]token.Pos 287 allTags map[string]bool 288 allFiles bool 289 ignoreOther bool // ignore files from other packages 290 allPackages bool 291 292 c *Config 293 pkgs map[string]*build.Instance 294 pkg *build.Instance 295 296 err errors.Error 297 } 298 299 func newFileProcessor(c *Config, p *build.Instance) *fileProcessor { 300 return &fileProcessor{ 301 imported: make(map[string][]token.Pos), 302 allTags: make(map[string]bool), 303 c: c, 304 pkgs: map[string]*build.Instance{"_": p}, 305 pkg: p, 306 } 307 } 308 309 func countCUEFiles(c *Config, p *build.Instance) int { 310 count := len(p.BuildFiles) 311 for _, f := range p.IgnoredFiles { 312 if c.Tools && strings.HasSuffix(f.Filename, "_tool.cue") { 313 count++ 314 } 315 if c.Tests && strings.HasSuffix(f.Filename, "_test.cue") { 316 count++ 317 } 318 } 319 return count 320 } 321 322 func (fp *fileProcessor) finalize(p *build.Instance) errors.Error { 323 if fp.err != nil { 324 return fp.err 325 } 326 if countCUEFiles(fp.c, p) == 0 && 327 !fp.c.DataFiles && 328 (p.PkgName != "_" || !fp.allPackages) { 329 fp.err = errors.Append(fp.err, &NoFilesError{Package: p, ignored: len(p.IgnoredFiles) > 0}) 330 return fp.err 331 } 332 333 for tag := range fp.allTags { 334 p.AllTags = append(p.AllTags, tag) 335 } 336 sort.Strings(p.AllTags) 337 338 p.ImportPaths, _ = cleanImports(fp.imported) 339 340 return nil 341 } 342 343 func (fp *fileProcessor) add(pos token.Pos, root string, file *build.File, mode importMode) (added bool) { 344 fullPath := file.Filename 345 if fullPath != "-" { 346 if !filepath.IsAbs(fullPath) { 347 fullPath = filepath.Join(root, fullPath) 348 } 349 } 350 file.Filename = fullPath 351 352 base := filepath.Base(fullPath) 353 354 // special * and _ 355 p := fp.pkg // default package 356 357 // badFile := func(p *build.Instance, err errors.Error) bool { 358 badFile := func(err errors.Error) bool { 359 fp.err = errors.Append(fp.err, err) 360 file.ExcludeReason = fp.err 361 p.InvalidFiles = append(p.InvalidFiles, file) 362 return true 363 } 364 365 match, data, err := matchFile(fp.c, file, true, fp.allFiles, fp.allTags) 366 switch { 367 case match: 368 369 case err == nil: 370 // Not a CUE file. 371 p.OrphanedFiles = append(p.OrphanedFiles, file) 372 return false 373 374 case !errors.Is(err, errExclude): 375 return badFile(err) 376 377 default: 378 file.ExcludeReason = err 379 if file.Interpretation == "" { 380 p.IgnoredFiles = append(p.IgnoredFiles, file) 381 } else { 382 p.OrphanedFiles = append(p.OrphanedFiles, file) 383 } 384 return false 385 } 386 387 pf, perr := parser.ParseFile(fullPath, data, parser.ImportsOnly, parser.ParseComments) 388 if perr != nil { 389 badFile(errors.Promote(perr, "add failed")) 390 return true 391 } 392 393 _, pkg, pos := internal.PackageInfo(pf) 394 if pkg == "" { 395 pkg = "_" 396 } 397 398 switch { 399 case pkg == p.PkgName, mode&allowAnonymous != 0: 400 case fp.allPackages && pkg != "_": 401 q := fp.pkgs[pkg] 402 if q == nil { 403 q = &build.Instance{ 404 PkgName: pkg, 405 406 Dir: p.Dir, 407 DisplayPath: p.DisplayPath, 408 ImportPath: p.ImportPath + ":" + pkg, 409 Root: p.Root, 410 Module: p.Module, 411 } 412 fp.pkgs[pkg] = q 413 } 414 p = q 415 416 case pkg != "_": 417 418 default: 419 file.ExcludeReason = excludeError{errors.Newf(pos, "no package name")} 420 p.IgnoredFiles = append(p.IgnoredFiles, file) 421 return false // don't mark as added 422 } 423 424 if !fp.c.AllCUEFiles { 425 if err := shouldBuildFile(pf, fp); err != nil { 426 if !errors.Is(err, errExclude) { 427 fp.err = errors.Append(fp.err, err) 428 } 429 file.ExcludeReason = err 430 p.IgnoredFiles = append(p.IgnoredFiles, file) 431 return false 432 } 433 } 434 435 if pkg != "" && pkg != "_" { 436 if p.PkgName == "" { 437 p.PkgName = pkg 438 fp.firstFile = base 439 } else if pkg != p.PkgName { 440 if fp.ignoreOther { 441 file.ExcludeReason = excludeError{errors.Newf(pos, 442 "package is %s, want %s", pkg, p.PkgName)} 443 p.IgnoredFiles = append(p.IgnoredFiles, file) 444 return false 445 } 446 return badFile(&MultiplePackageError{ 447 Dir: p.Dir, 448 Packages: []string{p.PkgName, pkg}, 449 Files: []string{fp.firstFile, base}, 450 }) 451 } 452 } 453 454 isTest := strings.HasSuffix(base, "_test"+cueSuffix) 455 isTool := strings.HasSuffix(base, "_tool"+cueSuffix) 456 457 if mode&importComment != 0 { 458 qcom, line := findimportComment(data) 459 if line != 0 { 460 com, err := strconv.Unquote(qcom) 461 if err != nil { 462 badFile(errors.Newf(pos, "%s:%d: cannot parse import comment", fullPath, line)) 463 } else if p.ImportComment == "" { 464 p.ImportComment = com 465 fp.firstCommentFile = base 466 } else if p.ImportComment != com { 467 badFile(errors.Newf(pos, "found import comments %q (%s) and %q (%s) in %s", p.ImportComment, fp.firstCommentFile, com, base, p.Dir)) 468 } 469 } 470 } 471 472 for _, decl := range pf.Decls { 473 d, ok := decl.(*ast.ImportDecl) 474 if !ok { 475 continue 476 } 477 for _, spec := range d.Specs { 478 quoted := spec.Path.Value 479 path, err := strconv.Unquote(quoted) 480 if err != nil { 481 badFile(errors.Newf( 482 spec.Path.Pos(), 483 "%s: parser returned invalid quoted string: <%s>", fullPath, quoted, 484 )) 485 } 486 if !isTest || fp.c.Tests { 487 fp.imported[path] = append(fp.imported[path], spec.Pos()) 488 } 489 } 490 } 491 switch { 492 case isTest: 493 if fp.c.loader.cfg.Tests { 494 p.BuildFiles = append(p.BuildFiles, file) 495 } else { 496 file.ExcludeReason = excludeError{errors.Newf(pos, 497 "_test.cue files excluded in non-test mode")} 498 p.IgnoredFiles = append(p.IgnoredFiles, file) 499 } 500 case isTool: 501 if fp.c.loader.cfg.Tools { 502 p.BuildFiles = append(p.BuildFiles, file) 503 } else { 504 file.ExcludeReason = excludeError{errors.Newf(pos, 505 "_tool.cue files excluded in non-cmd mode")} 506 p.IgnoredFiles = append(p.IgnoredFiles, file) 507 } 508 default: 509 p.BuildFiles = append(p.BuildFiles, file) 510 } 511 return true 512 } 513 514 func findimportComment(data []byte) (s string, line int) { 515 // expect keyword package 516 word, data := parseWord(data) 517 if string(word) != "package" { 518 return "", 0 519 } 520 521 // expect package name 522 _, data = parseWord(data) 523 524 // now ready for import comment, a // comment 525 // beginning and ending on the current line. 526 for len(data) > 0 && (data[0] == ' ' || data[0] == '\t' || data[0] == '\r') { 527 data = data[1:] 528 } 529 530 var comment []byte 531 switch { 532 case bytes.HasPrefix(data, slashSlash): 533 i := bytes.Index(data, newline) 534 if i < 0 { 535 i = len(data) 536 } 537 comment = data[2:i] 538 } 539 comment = bytes.TrimSpace(comment) 540 541 // split comment into `import`, `"pkg"` 542 word, arg := parseWord(comment) 543 if string(word) != "import" { 544 return "", 0 545 } 546 547 line = 1 + bytes.Count(data[:cap(data)-cap(arg)], newline) 548 return strings.TrimSpace(string(arg)), line 549 } 550 551 var ( 552 slashSlash = []byte("//") 553 newline = []byte("\n") 554 ) 555 556 // skipSpaceOrComment returns data with any leading spaces or comments removed. 557 func skipSpaceOrComment(data []byte) []byte { 558 for len(data) > 0 { 559 switch data[0] { 560 case ' ', '\t', '\r', '\n': 561 data = data[1:] 562 continue 563 case '/': 564 if bytes.HasPrefix(data, slashSlash) { 565 i := bytes.Index(data, newline) 566 if i < 0 { 567 return nil 568 } 569 data = data[i+1:] 570 continue 571 } 572 } 573 break 574 } 575 return data 576 } 577 578 // parseWord skips any leading spaces or comments in data 579 // and then parses the beginning of data as an identifier or keyword, 580 // returning that word and what remains after the word. 581 func parseWord(data []byte) (word, rest []byte) { 582 data = skipSpaceOrComment(data) 583 584 // Parse past leading word characters. 585 rest = data 586 for { 587 r, size := utf8.DecodeRune(rest) 588 if unicode.IsLetter(r) || '0' <= r && r <= '9' || r == '_' { 589 rest = rest[size:] 590 continue 591 } 592 break 593 } 594 595 word = data[:len(data)-len(rest)] 596 if len(word) == 0 { 597 return nil, nil 598 } 599 600 return word, rest 601 } 602 603 func cleanImports(m map[string][]token.Pos) ([]string, map[string][]token.Pos) { 604 all := make([]string, 0, len(m)) 605 for path := range m { 606 all = append(all, path) 607 } 608 sort.Strings(all) 609 return all, m 610 } 611 612 // isLocalImport reports whether the import path is 613 // a local import path, like ".", "..", "./foo", or "../foo". 614 func isLocalImport(path string) bool { 615 return path == "." || path == ".." || 616 strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../") 617 }