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