cuelang.org/go@v0.10.1/cue/load/search.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 "fmt" 19 "io/fs" 20 "path" 21 "path/filepath" 22 "strings" 23 24 "cuelang.org/go/cue/build" 25 "cuelang.org/go/cue/errors" 26 "cuelang.org/go/cue/token" 27 "cuelang.org/go/internal/mod/modimports" 28 "cuelang.org/go/mod/module" 29 ) 30 31 // A match represents the result of matching a single package pattern. 32 type match struct { 33 Pattern string // the pattern itself 34 Literal bool // whether it is a literal (no wildcards) 35 Pkgs []*build.Instance 36 Err errors.Error 37 } 38 39 // TODO: should be matched from module file only. 40 // The pattern is either "all" (all packages), "std" (standard packages), 41 // "cmd" (standard commands), or a path including "...". 42 func (l *loader) matchPackages(pattern, pkgName string) *match { 43 // cfg := l.cfg 44 m := &match{ 45 Pattern: pattern, 46 Literal: false, 47 } 48 // match := func(string) bool { return true } 49 // treeCanMatch := func(string) bool { return true } 50 // if !isMetaPackage(pattern) { 51 // match = matchPattern(pattern) 52 // treeCanMatch = treeCanMatchPattern(pattern) 53 // } 54 55 // have := map[string]bool{ 56 // "builtin": true, // ignore pseudo-package that exists only for documentation 57 // } 58 59 // for _, src := range cfg.srcDirs() { 60 // if pattern == "std" || pattern == "cmd" { 61 // continue 62 // } 63 // src = filepath.Clean(src) + string(filepath.Separator) 64 // root := src 65 // if pattern == "cmd" { 66 // root += "cmd" + string(filepath.Separator) 67 // } 68 // filepath.Walk(root, func(path string, fi os.FileInfo, err error) error { 69 // if err != nil || path == src { 70 // return nil 71 // } 72 73 // want := true 74 // // Avoid .foo, _foo, and testdata directory trees. 75 // _, elem := filepath.Split(path) 76 // if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" { 77 // want = false 78 // } 79 80 // name := filepath.ToSlash(path[len(src):]) 81 // if pattern == "std" && (!isStandardImportPath(name) || name == "cmd") { 82 // // The name "std" is only the standard library. 83 // // If the name is cmd, it's the root of the command tree. 84 // want = false 85 // } 86 // if !treeCanMatch(name) { 87 // want = false 88 // } 89 90 // if !fi.IsDir() { 91 // if fi.Mode()&os.ModeSymlink != 0 && want { 92 // if target, err := os.Stat(path); err == nil && target.IsDir() { 93 // fmt.Fprintf(os.Stderr, "warning: ignoring symlink %s\n", path) 94 // } 95 // } 96 // return nil 97 // } 98 // if !want { 99 // return skipDir 100 // } 101 102 // if have[name] { 103 // return nil 104 // } 105 // have[name] = true 106 // if !match(name) { 107 // return nil 108 // } 109 // pkg := l.importPkg(".", path) 110 // if err := pkg.Error; err != nil { 111 // if _, noGo := err.(*noCUEError); noGo { 112 // return nil 113 // } 114 // } 115 116 // // If we are expanding "cmd", skip main 117 // // packages under cmd/vendor. At least as of 118 // // March, 2017, there is one there for the 119 // // vendored pprof tool. 120 // if pattern == "cmd" && strings.HasPrefix(pkg.DisplayPath, "cmd/vendor") && pkg.PkgName == "main" { 121 // return nil 122 // } 123 124 // m.Pkgs = append(m.Pkgs, pkg) 125 // return nil 126 // }) 127 // } 128 return m 129 } 130 131 // matchPackagesInFS is like allPackages but is passed a pattern 132 // beginning ./ or ../, meaning it should scan the tree rooted 133 // at the given directory. There are ... in the pattern too. 134 // (See cue help inputs for pattern syntax.) 135 func (l *loader) matchPackagesInFS(pattern, pkgName string) *match { 136 c := l.cfg 137 m := &match{ 138 Pattern: pattern, 139 Literal: false, 140 } 141 142 // Find directory to begin the scan. 143 // Could be smarter but this one optimization 144 // is enough for now, since ... is usually at the 145 // end of a path. 146 i := strings.Index(pattern, "...") 147 dir, _ := path.Split(pattern[:i]) 148 149 root := l.abs(dir) 150 151 // Find new module root from here or check there are no additional 152 // cue.mod files between here and the next module. 153 154 if !hasFilepathPrefix(root, c.ModuleRoot) { 155 m.Err = errors.Newf(token.NoPos, 156 "cue: pattern %s refers to dir %s, outside module root %s", 157 pattern, root, c.ModuleRoot) 158 return m 159 } 160 161 pkgDir := filepath.Join(root, modDir) 162 163 _ = c.fileSystem.walk(root, func(path string, entry fs.DirEntry, err errors.Error) errors.Error { 164 if err != nil || !entry.IsDir() { 165 return nil 166 } 167 if path == pkgDir { 168 return skipDir 169 } 170 171 top := path == root 172 173 // Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..". 174 _, elem := filepath.Split(path) 175 dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".." 176 if dot || strings.HasPrefix(elem, "_") || (elem == "testdata" && !top) { 177 return skipDir 178 } 179 180 if !top { 181 // Ignore other modules found in subdirectories. 182 if _, err := c.fileSystem.stat(filepath.Join(path, modDir)); err == nil { 183 return skipDir 184 } 185 } 186 187 // name := prefix + filepath.ToSlash(path) 188 // if !match(name) { 189 // return nil 190 // } 191 192 // We keep the directory if we can import it, or if we can't import it 193 // due to invalid CUE source files. This means that directories 194 // containing parse errors will be built (and fail) instead of being 195 // silently skipped as not matching the pattern. 196 // Do not take root, as we want to stay relative 197 // to one dir only. 198 relPath, err2 := filepath.Rel(c.Dir, path) 199 if err2 != nil { 200 panic(err2) // Should never happen because c.Dir is absolute. 201 } 202 relPath = "./" + filepath.ToSlash(relPath) 203 // TODO: consider not doing these checks here. 204 inst := l.newRelInstance(token.NoPos, relPath, pkgName) 205 pkgs := l.importPkg(token.NoPos, inst) 206 for _, p := range pkgs { 207 if err := p.Err; err != nil && (p == nil || len(p.InvalidFiles) == 0) { 208 switch err.(type) { 209 case *NoFilesError: 210 if c.DataFiles && len(p.OrphanedFiles) > 0 { 211 break 212 } 213 return nil 214 default: 215 m.Err = errors.Append(m.Err, err) 216 } 217 } 218 } 219 220 m.Pkgs = append(m.Pkgs, pkgs...) 221 return nil 222 }) 223 return m 224 } 225 226 // importPaths returns the matching paths to use for the given command line. 227 // It calls ImportPathsQuiet and then WarnUnmatched. 228 func (l *loader) importPaths(patterns []string) []*match { 229 matches := l.importPathsQuiet(patterns) 230 warnUnmatched(matches) 231 return matches 232 } 233 234 // importPathsQuiet is like importPaths but does not warn about patterns with no matches. 235 func (l *loader) importPathsQuiet(patterns []string) []*match { 236 var out []*match 237 for _, a := range cleanPatterns(patterns) { 238 if isMetaPackage(a) { 239 out = append(out, l.matchPackages(a, l.cfg.Package)) 240 continue 241 } 242 243 orig := a 244 pkgName := l.cfg.Package 245 switch p := strings.IndexByte(a, ':'); { 246 case p < 0: 247 case p == 0: 248 pkgName = a[1:] 249 a = "." 250 default: 251 pkgName = a[p+1:] 252 a = a[:p] 253 } 254 if pkgName == "*" { 255 pkgName = "" 256 } 257 258 if strings.Contains(a, "...") { 259 if isLocalImport(a) { 260 out = append(out, l.matchPackagesInFS(a, pkgName)) 261 } else { 262 out = append(out, l.matchPackages(a, pkgName)) 263 } 264 continue 265 } 266 267 var p *build.Instance 268 if isLocalImport(a) { 269 p = l.newRelInstance(token.NoPos, a, pkgName) 270 } else { 271 p = l.newInstance(token.NoPos, importPath(orig)) 272 } 273 274 pkgs := l.importPkg(token.NoPos, p) 275 out = append(out, &match{Pattern: a, Literal: true, Pkgs: pkgs}) 276 } 277 return out 278 } 279 280 type resolvedPackageArg struct { 281 // The original field may be needed once we want to replace the original 282 // package pattern matching code, as it is necessary to populate Instance.DisplayPath. 283 original string 284 resolvedCanonical string 285 } 286 287 func expandPackageArgs(c *Config, pkgArgs []string, pkgQual string, tg *tagger) ([]resolvedPackageArg, error) { 288 expanded := make([]resolvedPackageArg, 0, len(pkgArgs)) 289 for _, p := range pkgArgs { 290 var err error 291 expanded, err = appendExpandedPackageArg(c, expanded, p, pkgQual, tg) 292 if err != nil { 293 return nil, err 294 } 295 } 296 return expanded, nil 297 } 298 299 // appendExpandedPackageArg appends all the package paths matched by p to pkgPaths 300 // and returns the result. It also cleans the paths and makes them absolute. 301 // 302 // pkgQual is used to determine which packages to match when wildcards are expanded. 303 // Its semantics follow those of [Config.Package]. 304 func appendExpandedPackageArg(c *Config, pkgPaths []resolvedPackageArg, p string, pkgQual string, tg *tagger) ([]resolvedPackageArg, error) { 305 origp := p 306 if filepath.IsAbs(p) { 307 return nil, fmt.Errorf("cannot use absolute directory %q as package path", p) 308 } 309 // Arguments are supposed to be import paths, but 310 // as a courtesy to Windows developers, rewrite \ to / 311 // in command-line arguments. Handles .\... and so on. 312 p = filepath.ToSlash(p) 313 314 ip := module.ParseImportPath(p) 315 if ip.Qualifier == "_" { 316 return nil, fmt.Errorf("invalid import path qualifier _ in %q", origp) 317 } 318 319 isRel := strings.HasPrefix(ip.Path, "./") 320 // Put argument in canonical form. 321 ip.Path = path.Clean(ip.Path) 322 if isRel && ip.Path != "." { 323 // Preserve leading "./". 324 ip.Path = "./" + ip.Path 325 } 326 isLocal := isLocalImport(ip.Path) 327 // Note that when c.Module is empty, c.ModuleRoot is sometimes, 328 // but not always, the same as c.Dir. Specifically it might point 329 // to the directory containing a cue.mod directory even if that 330 // directory doesn't actually contain a module.cue file. 331 moduleRoot := c.ModuleRoot 332 if isLocal { 333 if c.Module != "" { 334 // Make local import paths into absolute paths inside 335 // the module root. 336 absPath := path.Join(c.Dir, ip.Path) 337 pkgPath, err := importPathFromAbsDir(c, absPath, origp) 338 if err != nil { 339 return nil, err 340 } 341 ip1 := module.ParseImportPath(string(pkgPath)) 342 // Leave ip.Qualifier and ip.ExplicitQualifier intact. 343 ip.Path = ip1.Path 344 ip.Version = ip1.Version 345 } else { 346 // There's no module, so we can't make 347 // the import path absolute. 348 moduleRoot = c.Dir 349 } 350 } 351 if !strings.Contains(ip.Path, "...") { 352 if isLocal && !ip.ExplicitQualifier { 353 // A package qualifier has not been explicitly specified for a local 354 // import path so we need to walk the package directory to find the 355 // packages in it. We have a special rule for local imports because it's 356 // inconvenient always to have to specify a package qualifier when 357 // there's only one package in the current directory but the last 358 // component of its package path does not match its name. 359 return appendExpandedUnqualifiedPackagePath(pkgPaths, origp, ip, pkgQual, module.SourceLoc{ 360 FS: c.fileSystem.ioFS(moduleRoot), 361 Dir: ".", 362 }, c.Module, tg) 363 } 364 return append(pkgPaths, resolvedPackageArg{origp, ip.Canonical().String()}), nil 365 } 366 // Strip the module prefix, leaving only the directory relative 367 // to the module root. 368 ip, ok := cutModulePrefix(ip, c.Module) 369 if !ok { 370 return nil, fmt.Errorf("pattern not allowed in external package path %q", origp) 371 } 372 return appendExpandedWildcardPackagePath(pkgPaths, ip, pkgQual, module.SourceLoc{ 373 FS: c.fileSystem.ioFS(moduleRoot), 374 Dir: ".", 375 }, c.Module, tg) 376 } 377 378 // appendExpandedUnqualifiedPackagePath expands the given import path, 379 // which is relative to the root of the module, into its resolved and 380 // qualified package paths according to the following rules (the first rule 381 // that applies is used) 382 // 383 // 1. if pkgQual is "*", it chooses all the packages present in the 384 // package directory. 385 // 2. if pkgQual is "_", it looks for a package file with no package name. 386 // 3. if there's a package named after ip.Qualifier it chooses that 387 // 4. if there's exactly one package in the directory it will choose that. 388 // 5. if there's more than one package in the directory, it returns a MultiplePackageError. 389 // 6. if there are no package files in the directory, it just appends the import path as is, leaving it 390 // to later logic to produce an error in this case. 391 func appendExpandedUnqualifiedPackagePath(pkgPaths []resolvedPackageArg, origp string, ip module.ImportPath, pkgQual string, mainModRoot module.SourceLoc, mainModPath string, tg *tagger) (_ []resolvedPackageArg, _err error) { 392 ipRel, ok := cutModulePrefix(ip, mainModPath) 393 if !ok { 394 // Should never happen. 395 return nil, fmt.Errorf("internal error: local import path %q in module %q has resulted in non-internal package %q", origp, mainModPath, ip) 396 } 397 dir := path.Join(mainModRoot.Dir, ipRel.Path) 398 info, err := fs.Stat(mainModRoot.FS, dir) 399 if err != nil { 400 // The package directory doesn't exist. 401 // Treat it like an empty directory and let later logic deal with it. 402 return append(pkgPaths, resolvedPackageArg{origp, ip.String()}), nil 403 } 404 if !info.IsDir() { 405 return nil, fmt.Errorf("%s is a file and not a package directory", origp) 406 } 407 iter := modimports.PackageFiles(mainModRoot.FS, dir, "*") 408 409 // 1. if pkgQual is "*", it appends all the packages present in the package directory. 410 if pkgQual == "*" { 411 wasAdded := make(map[string]bool) 412 iter(func(f modimports.ModuleFile, err error) bool { 413 if err != nil { 414 _err = err 415 return false 416 } 417 if err := shouldBuildFile(f.Syntax, tg.tagIsSet); err != nil { 418 // Later build logic should pick up and report the same error. 419 return true 420 } 421 pkgName := f.Syntax.PackageName() 422 if wasAdded[pkgName] { 423 return true 424 } 425 wasAdded[pkgName] = true 426 ip := ip 427 ip.Qualifier = pkgName 428 p := ip.String() 429 pkgPaths = append(pkgPaths, resolvedPackageArg{p, p}) 430 return true 431 }) 432 if _err != nil { 433 return nil, _err 434 } 435 return pkgPaths, nil 436 } 437 var files []modimports.ModuleFile 438 foundQualifier := false 439 // TODO(rog): for f, err := range iter { 440 iter(func(f modimports.ModuleFile, err error) bool { 441 if err != nil { 442 _err = err 443 return false 444 } 445 pkgName := f.Syntax.PackageName() 446 // 2. if pkgQual is "_", it looks for a package file with no package name. 447 // 3. if there's a package named after ip.Qualifier it chooses that 448 if (pkgName != "" && pkgName == ip.Qualifier) || (pkgQual == "_" && pkgName == "") { 449 foundQualifier = true 450 return false 451 } 452 if pkgName != "" { 453 files = append(files, f) 454 } 455 return true 456 }) 457 if _err != nil { 458 return nil, _err 459 } 460 if foundQualifier { 461 // We found the actual package that was implied by the import path (or pkgQual == "_"). 462 // This takes precedence over anything else. 463 return append(pkgPaths, resolvedPackageArg{origp, ip.String()}), nil 464 } 465 if len(files) == 0 { 466 // 6. if there are no package files in the directory, it just appends the import path as is, 467 // leaving it to later logic to produce an error in this case. 468 return append(pkgPaths, resolvedPackageArg{origp, ip.String()}), nil 469 } 470 pkgName := files[0].Syntax.PackageName() 471 for _, f := range files[1:] { 472 // 5. if there's more than one package in the directory, it returns a MultiplePackageError. 473 if pkgName1 := f.Syntax.PackageName(); pkgName1 != pkgName { 474 return nil, &MultiplePackageError{ 475 Dir: dir, 476 Packages: []string{pkgName, pkgName1}, 477 Files: []string{ 478 path.Base(files[0].FilePath), 479 path.Base(f.FilePath), 480 }, 481 } 482 } 483 } 484 // 4. if there's exactly one package in the directory it will choose that. 485 ip.Qualifier = pkgName 486 return append(pkgPaths, resolvedPackageArg{origp, ip.String()}), nil 487 } 488 489 // appendExpandedWildcardPackagePath expands the given pattern into any packages that it matches 490 // and appends the results to pkgPaths. It returns an error if the pattern matches nothing. 491 // 492 // Note: 493 // * We know that pattern contains "..." 494 // * We know that pattern is relative to the module root 495 func appendExpandedWildcardPackagePath(pkgPaths []resolvedPackageArg, pattern module.ImportPath, pkgQual string, mainModRoot module.SourceLoc, mainModPath string, tg *tagger) (_ []resolvedPackageArg, _err error) { 496 modIpath := module.ParseImportPath(mainModPath) 497 // Find directory to begin the scan. 498 // Could be smarter but this one optimization is enough for now, 499 // since ... is usually at the end of a path. 500 // TODO: strip package qualifier. 501 i := strings.Index(pattern.Path, "...") 502 dir, _ := path.Split(pattern.Path[:i]) 503 dir = path.Join(mainModRoot.Dir, dir) 504 var isSelected func(string) bool 505 switch pkgQual { 506 case "_": 507 isSelected = func(pkgName string) bool { 508 return pkgName == "" 509 } 510 case "*": 511 isSelected = func(pkgName string) bool { 512 return true 513 } 514 case "": 515 isSelected = func(pkgName string) bool { 516 // The package ambiguity logic will be triggered if there's more than one 517 // package in the same directory. 518 return pkgName != "" 519 } 520 default: 521 isSelected = func(pkgName string) bool { 522 return pkgName == pkgQual 523 } 524 } 525 526 var prevFile modimports.ModuleFile 527 var prevImportPath module.ImportPath 528 iter := modimports.AllModuleFiles(mainModRoot.FS, dir) 529 iter(func(f modimports.ModuleFile, err error) bool { 530 if err != nil { 531 return false 532 } 533 if err := shouldBuildFile(f.Syntax, tg.tagIsSet); err != nil { 534 // Later build logic should pick up and report the same error. 535 return true 536 } 537 pkgName := f.Syntax.PackageName() 538 if !isSelected(pkgName) { 539 return true 540 } 541 if pkgName == "" { 542 pkgName = "_" 543 } 544 ip := module.ImportPath{ 545 Path: path.Join(modIpath.Path, path.Dir(f.FilePath)), 546 Qualifier: pkgName, 547 Version: modIpath.Version, 548 } 549 if modIpath.Path == "" { 550 // There's no module, so make sure that the path still looks like a relative import path. 551 if !strings.HasPrefix(ip.Path, "../") { 552 ip.Path = "./" + ip.Path 553 } 554 } 555 if ip == prevImportPath { 556 // TODO(rog): this isn't sufficient for full deduplication: we can get an alternation of different 557 // package names within the same directory. We'll need to maintain a map. 558 return true 559 } 560 if pkgQual == "" { 561 // Note: we can look at the previous item only rather than maintaining a map 562 // because modimports.AllModuleFiles guarantees that files in the same 563 // package are always adjacent. 564 if prevFile.FilePath != "" && prevImportPath.Path == ip.Path && ip.Qualifier != prevImportPath.Qualifier { 565 // A wildcard isn't currently allowed to match multiple packages 566 // in a single directory. 567 _err = &MultiplePackageError{ 568 Dir: path.Dir(f.FilePath), 569 Packages: []string{prevImportPath.Qualifier, ip.Qualifier}, 570 Files: []string{ 571 path.Base(prevFile.FilePath), 572 path.Base(f.FilePath), 573 }, 574 } 575 return false 576 } 577 } 578 pkgPaths = append(pkgPaths, resolvedPackageArg{ip.String(), ip.String()}) 579 prevFile, prevImportPath = f, ip 580 return true 581 }) 582 return pkgPaths, _err 583 } 584 585 // cutModulePrefix strips the given module path from p and reports whether p is inside mod. 586 // It returns a relative package path within m. 587 // 588 // If p does not contain a major version suffix but otherwise matches mod, it counts as a match. 589 func cutModulePrefix(p module.ImportPath, mod string) (module.ImportPath, bool) { 590 if mod == "" { 591 return p, true 592 } 593 modPath, modVers, ok := module.SplitPathVersion(mod) 594 if !ok { 595 modPath = mod 596 } 597 if !strings.HasPrefix(p.Path, modPath) { 598 return module.ImportPath{}, false 599 } 600 if p.Path == modPath { 601 p.Path = "." 602 return p, true 603 } 604 if p.Path[len(modPath)] != '/' { 605 return module.ImportPath{}, false 606 } 607 if p.Version != "" && modVers != "" && p.Version != modVers { 608 return module.ImportPath{}, false 609 } 610 p.Path = "." + p.Path[len(modPath):] 611 p.Version = "" 612 return p, true 613 }