blob: edba60e62680f9c98c6e5e20115759fb1f2361c2 [file] [log] [blame]
// Copyright 2018 The CUE Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package load
import (
// TODO: remove this usage
"os"
"path"
"path/filepath"
"regexp"
"strings"
build "cuelang.org/go/cue/build"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/token"
)
// A match represents the result of matching a single package pattern.
type match struct {
Pattern string // the pattern itself
Literal bool // whether it is a literal (no wildcards)
Pkgs []*build.Instance
Err error
}
// TODO: should be matched from module file only.
// The pattern is either "all" (all packages), "std" (standard packages),
// "cmd" (standard commands), or a path including "...".
func (l *loader) matchPackages(pattern string) *match {
// cfg := l.cfg
m := &match{
Pattern: pattern,
Literal: false,
}
// match := func(string) bool { return true }
// treeCanMatch := func(string) bool { return true }
// if !isMetaPackage(pattern) {
// match = matchPattern(pattern)
// treeCanMatch = treeCanMatchPattern(pattern)
// }
// have := map[string]bool{
// "builtin": true, // ignore pseudo-package that exists only for documentation
// }
// for _, src := range cfg.srcDirs() {
// if pattern == "std" || pattern == "cmd" {
// continue
// }
// src = filepath.Clean(src) + string(filepath.Separator)
// root := src
// if pattern == "cmd" {
// root += "cmd" + string(filepath.Separator)
// }
// filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
// if err != nil || path == src {
// return nil
// }
// want := true
// // Avoid .foo, _foo, and testdata directory trees.
// _, elem := filepath.Split(path)
// if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" {
// want = false
// }
// name := filepath.ToSlash(path[len(src):])
// if pattern == "std" && (!isStandardImportPath(name) || name == "cmd") {
// // The name "std" is only the standard library.
// // If the name is cmd, it's the root of the command tree.
// want = false
// }
// if !treeCanMatch(name) {
// want = false
// }
// if !fi.IsDir() {
// if fi.Mode()&os.ModeSymlink != 0 && want {
// if target, err := os.Stat(path); err == nil && target.IsDir() {
// fmt.Fprintf(os.Stderr, "warning: ignoring symlink %s\n", path)
// }
// }
// return nil
// }
// if !want {
// return skipDir
// }
// if have[name] {
// return nil
// }
// have[name] = true
// if !match(name) {
// return nil
// }
// pkg := l.importPkg(".", path)
// if err := pkg.Error; err != nil {
// if _, noGo := err.(*noCUEError); noGo {
// return nil
// }
// }
// // If we are expanding "cmd", skip main
// // packages under cmd/vendor. At least as of
// // March, 2017, there is one there for the
// // vendored pprof tool.
// if pattern == "cmd" && strings.HasPrefix(pkg.DisplayPath, "cmd/vendor") && pkg.PkgName == "main" {
// return nil
// }
// m.Pkgs = append(m.Pkgs, pkg)
// return nil
// })
// }
return m
}
// matchPackagesInFS is like allPackages but is passed a pattern
// beginning ./ or ../, meaning it should scan the tree rooted
// at the given directory. There are ... in the pattern too.
// (See go help packages for pattern syntax.)
func (l *loader) matchPackagesInFS(pattern string) *match {
c := l.cfg
m := &match{
Pattern: pattern,
Literal: false,
}
// Find directory to begin the scan.
// Could be smarter but this one optimization
// is enough for now, since ... is usually at the
// end of a path.
i := strings.Index(pattern, "...")
dir, _ := path.Split(pattern[:i])
root := l.abs(dir)
// Find new module root from here or check there are no additional
// cue.mod files between here and the next module.
if !hasFilepathPrefix(root, c.ModuleRoot) {
m.Err = errors.Newf(token.NoPos,
"cue: pattern %s refers to dir %s, outside module root %s",
pattern, root, c.ModuleRoot)
return m
}
pkgDir := filepath.Join(root, modDir)
// TODO(legacy): remove
pkgDir2 := filepath.Join(root, "pkg")
_ = c.fileSystem.walk(root, func(path string, fi os.FileInfo, err errors.Error) errors.Error {
if err != nil || !fi.IsDir() {
return nil
}
if path == pkgDir || path == pkgDir2 {
return skipDir
}
top := path == root
// Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..".
_, elem := filepath.Split(path)
dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".."
if dot || strings.HasPrefix(elem, "_") || (elem == "testdata" && !top) {
return skipDir
}
if !top {
// Ignore other modules found in subdirectories.
if _, err := c.fileSystem.stat(filepath.Join(path, modDir)); err == nil {
return skipDir
}
}
// name := prefix + filepath.ToSlash(path)
// if !match(name) {
// return nil
// }
// We keep the directory if we can import it, or if we can't import it
// due to invalid CUE source files. This means that directories
// containing parse errors will be built (and fail) instead of being
// silently skipped as not matching the pattern.
// Do not take root, as we want to stay relative
// to one dir only.
dir, e := filepath.Rel(c.Dir, path)
if e != nil {
panic(err)
} else {
dir = "./" + dir
}
// TODO: consider not doing these checks here.
inst := c.newRelInstance(token.NoPos, dir)
p := l.importPkg(token.NoPos, inst)
if err := p.Err; err != nil && (p == nil || len(p.InvalidCUEFiles) == 0) {
switch err.(type) {
case nil:
break
case *noCUEError:
if c.DataFiles && len(p.DataFiles) > 0 {
break
}
return nil
default:
m.Err = err
}
}
m.Pkgs = append(m.Pkgs, p)
return nil
})
return m
}
// treeCanMatchPattern(pattern)(name) reports whether
// name or children of name can possibly match pattern.
// Pattern is the same limited glob accepted by matchPattern.
func treeCanMatchPattern(pattern string) func(name string) bool {
wildCard := false
if i := strings.Index(pattern, "..."); i >= 0 {
wildCard = true
pattern = pattern[:i]
}
return func(name string) bool {
return len(name) <= len(pattern) && hasPathPrefix(pattern, name) ||
wildCard && strings.HasPrefix(name, pattern)
}
}
// matchPattern(pattern)(name) reports whether
// name matches pattern. Pattern is a limited glob
// pattern in which '...' means 'any string' and there
// is no other special syntax.
// Unfortunately, there are two special cases. Quoting "go help packages":
//
// First, /... at the end of the pattern can match an empty string,
// so that net/... matches both net and packages in its subdirectories, like net/http.
// Second, any slash-separted pattern element containing a wildcard never
// participates in a match of the "vendor" element in the path of a vendored
// package, so that ./... does not match packages in subdirectories of
// ./vendor or ./mycode/vendor, but ./vendor/... and ./mycode/vendor/... do.
// Note, however, that a directory named vendor that itself contains code
// is not a vendored package: cmd/vendor would be a command named vendor,
// and the pattern cmd/... matches it.
func matchPattern(pattern string) func(name string) bool {
// Convert pattern to regular expression.
// The strategy for the trailing /... is to nest it in an explicit ? expression.
// The strategy for the vendor exclusion is to change the unmatchable
// vendor strings to a disallowed code point (vendorChar) and to use
// "(anything but that codepoint)*" as the implementation of the ... wildcard.
// This is a bit complicated but the obvious alternative,
// namely a hand-written search like in most shell glob matchers,
// is too easy to make accidentally exponential.
// Using package regexp guarantees linear-time matching.
const vendorChar = "\x00"
if strings.Contains(pattern, vendorChar) {
return func(name string) bool { return false }
}
re := regexp.QuoteMeta(pattern)
re = replaceVendor(re, vendorChar)
switch {
case strings.HasSuffix(re, `/`+vendorChar+`/\.\.\.`):
re = strings.TrimSuffix(re, `/`+vendorChar+`/\.\.\.`) + `(/vendor|/` + vendorChar + `/\.\.\.)`
case re == vendorChar+`/\.\.\.`:
re = `(/vendor|/` + vendorChar + `/\.\.\.)`
case strings.HasSuffix(re, `/\.\.\.`):
re = strings.TrimSuffix(re, `/\.\.\.`) + `(/\.\.\.)?`
}
re = strings.Replace(re, `\.\.\.`, `[^`+vendorChar+`]*`, -1)
reg := regexp.MustCompile(`^` + re + `$`)
return func(name string) bool {
if strings.Contains(name, vendorChar) {
return false
}
return reg.MatchString(replaceVendor(name, vendorChar))
}
}
// replaceVendor returns the result of replacing
// non-trailing vendor path elements in x with repl.
func replaceVendor(x, repl string) string {
if !strings.Contains(x, "vendor") {
return x
}
elem := strings.Split(x, "/")
for i := 0; i < len(elem)-1; i++ {
if elem[i] == "vendor" {
elem[i] = repl
}
}
return strings.Join(elem, "/")
}
// warnUnmatched warns about patterns that didn't match any packages.
func warnUnmatched(matches []*match) {
for _, m := range matches {
if len(m.Pkgs) == 0 {
m.Err =
errors.Newf(token.NoPos, "cue: %q matched no packages\n", m.Pattern)
}
}
}
// importPaths returns the matching paths to use for the given command line.
// It calls ImportPathsQuiet and then WarnUnmatched.
func (l *loader) importPaths(patterns []string) []*match {
matches := l.importPathsQuiet(patterns)
warnUnmatched(matches)
return matches
}
// importPathsQuiet is like ImportPaths but does not warn about patterns with no matches.
func (l *loader) importPathsQuiet(patterns []string) []*match {
var out []*match
for _, a := range cleanPatterns(patterns) {
if isMetaPackage(a) {
out = append(out, l.matchPackages(a))
continue
}
if strings.Contains(a, "...") {
if isLocalImport(a) {
out = append(out, l.matchPackagesInFS(a))
} else {
out = append(out, l.matchPackages(a))
}
continue
}
var p *build.Instance
if isLocalImport(a) {
p = l.cfg.newRelInstance(token.NoPos, a)
} else {
p = l.cfg.newInstance(token.NoPos, importPath(a))
}
pkg := l.importPkg(token.NoPos, p)
out = append(out, &match{Pattern: a, Literal: true, Pkgs: []*build.Instance{pkg}})
}
return out
}
// cleanPatterns returns the patterns to use for the given
// command line. It canonicalizes the patterns but does not
// evaluate any matches.
func cleanPatterns(patterns []string) []string {
if len(patterns) == 0 {
return []string{"."}
}
var out []string
for _, a := range patterns {
// Arguments are supposed to be import paths, but
// as a courtesy to Windows developers, rewrite \ to /
// in command-line arguments. Handles .\... and so on.
if filepath.Separator == '\\' {
a = strings.Replace(a, `\`, `/`, -1)
}
// Put argument in canonical form, but preserve leading ./.
if strings.HasPrefix(a, "./") {
a = "./" + path.Clean(a)
if a == "./." {
a = "."
}
} else {
a = path.Clean(a)
}
out = append(out, a)
}
return out
}
// isMetaPackage checks if name is a reserved package name that expands to multiple packages.
func isMetaPackage(name string) bool {
return name == "std" || name == "cmd" || name == "all"
}
// hasPathPrefix reports whether the path s begins with the
// elements in prefix.
func hasPathPrefix(s, prefix string) bool {
switch {
default:
return false
case len(s) == len(prefix):
return s == prefix
case len(s) > len(prefix):
if prefix != "" && prefix[len(prefix)-1] == '/' {
return strings.HasPrefix(s, prefix)
}
return s[len(prefix)] == '/' && s[:len(prefix)] == prefix
}
}
// hasFilepathPrefix reports whether the path s begins with the
// elements in prefix.
func hasFilepathPrefix(s, prefix string) bool {
switch {
default:
return false
case len(s) == len(prefix):
return s == prefix
case len(s) > len(prefix):
if prefix != "" && prefix[len(prefix)-1] == filepath.Separator {
return strings.HasPrefix(s, prefix)
}
return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == prefix
}
}
// isStandardImportPath reports whether $GOROOT/src/path should be considered
// part of the standard distribution. For historical reasons we allow people to add
// their own code to $GOROOT instead of using $GOPATH, but we assume that
// code will start with a domain name (dot in the first element).
//
// Note that this function is meant to evaluate whether a directory found in GOROOT
// should be treated as part of the standard library. It should not be used to decide
// that a directory found in GOPATH should be rejected: directories in GOPATH
// need not have dots in the first element, and they just take their chances
// with future collisions in the standard library.
func isStandardImportPath(path string) bool {
i := strings.Index(path, "/")
if i < 0 {
i = len(path)
}
elem := path[:i]
return !strings.Contains(elem, ".")
}
// isRelativePath reports whether pattern should be interpreted as a directory
// path relative to the current directory, as opposed to a pattern matching
// import paths.
func isRelativePath(pattern string) bool {
return strings.HasPrefix(pattern, "./") || strings.HasPrefix(pattern, "../") || pattern == "." || pattern == ".."
}
// inDir checks whether path is in the file tree rooted at dir.
// If so, inDir returns an equivalent path relative to dir.
// If not, inDir returns an empty string.
// inDir makes some effort to succeed even in the presence of symbolic links.
// TODO(rsc): Replace internal/test.inDir with a call to this function for Go 1.12.
func inDir(path, dir string) string {
if rel := inDirLex(path, dir); rel != "" {
return rel
}
xpath, err := filepath.EvalSymlinks(path)
if err != nil || xpath == path {
xpath = ""
} else {
if rel := inDirLex(xpath, dir); rel != "" {
return rel
}
}
xdir, err := filepath.EvalSymlinks(dir)
if err == nil && xdir != dir {
if rel := inDirLex(path, xdir); rel != "" {
return rel
}
if xpath != "" {
if rel := inDirLex(xpath, xdir); rel != "" {
return rel
}
}
}
return ""
}
// inDirLex is like inDir but only checks the lexical form of the file names.
// It does not consider symbolic links.
// TODO(rsc): This is a copy of str.HasFilePathPrefix, modified to
// return the suffix. Most uses of str.HasFilePathPrefix should probably
// be calling InDir instead.
func inDirLex(path, dir string) string {
pv := strings.ToUpper(filepath.VolumeName(path))
dv := strings.ToUpper(filepath.VolumeName(dir))
path = path[len(pv):]
dir = dir[len(dv):]
switch {
default:
return ""
case pv != dv:
return ""
case len(path) == len(dir):
if path == dir {
return "."
}
return ""
case dir == "":
return path
case len(path) > len(dir):
if dir[len(dir)-1] == filepath.Separator {
if path[:len(dir)] == dir {
return path[len(dir):]
}
return ""
}
if path[len(dir)] == filepath.Separator && path[:len(dir)] == dir {
if len(path) == len(dir)+1 {
return "."
}
return path[len(dir)+1:]
}
return ""
}
}