blob: 57ab9a556ec5bf1a3811c57bb7aff8231767cc62 [file] [log] [blame]
Marcel van Lohuizenbc4d65d2018-12-10 15:40:02 +01001// 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
15package load
16
17import (
18 "fmt" // TODO: remove this usage
Marcel van Lohuizenbc4d65d2018-12-10 15:40:02 +010019 "os"
20 "path"
21 "path/filepath"
22 "regexp"
23 "strings"
24
25 build "cuelang.org/go/cue/build"
26)
27
28// A match represents the result of matching a single package pattern.
29type match struct {
30 Pattern string // the pattern itself
31 Literal bool // whether it is a literal (no wildcards)
32 Pkgs []*build.Instance
33 Err error
34}
35
36// TODO: should be matched from module file only.
37// The pattern is either "all" (all packages), "std" (standard packages),
38// "cmd" (standard commands), or a path including "...".
39func (l *loader) matchPackages(pattern string) *match {
40 // cfg := l.cfg
41 m := &match{
42 Pattern: pattern,
43 Literal: false,
44 }
45 // match := func(string) bool { return true }
46 // treeCanMatch := func(string) bool { return true }
47 // if !isMetaPackage(pattern) {
48 // match = matchPattern(pattern)
49 // treeCanMatch = treeCanMatchPattern(pattern)
50 // }
51
52 // have := map[string]bool{
53 // "builtin": true, // ignore pseudo-package that exists only for documentation
54 // }
55
56 // for _, src := range cfg.srcDirs() {
57 // if pattern == "std" || pattern == "cmd" {
58 // continue
59 // }
60 // src = filepath.Clean(src) + string(filepath.Separator)
61 // root := src
62 // if pattern == "cmd" {
63 // root += "cmd" + string(filepath.Separator)
64 // }
65 // filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
66 // if err != nil || path == src {
67 // return nil
68 // }
69
70 // want := true
71 // // Avoid .foo, _foo, and testdata directory trees.
72 // _, elem := filepath.Split(path)
73 // if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" {
74 // want = false
75 // }
76
77 // name := filepath.ToSlash(path[len(src):])
78 // if pattern == "std" && (!isStandardImportPath(name) || name == "cmd") {
79 // // The name "std" is only the standard library.
80 // // If the name is cmd, it's the root of the command tree.
81 // want = false
82 // }
83 // if !treeCanMatch(name) {
84 // want = false
85 // }
86
87 // if !fi.IsDir() {
88 // if fi.Mode()&os.ModeSymlink != 0 && want {
89 // if target, err := os.Stat(path); err == nil && target.IsDir() {
90 // fmt.Fprintf(os.Stderr, "warning: ignoring symlink %s\n", path)
91 // }
92 // }
93 // return nil
94 // }
95 // if !want {
96 // return filepath.SkipDir
97 // }
98
99 // if have[name] {
100 // return nil
101 // }
102 // have[name] = true
103 // if !match(name) {
104 // return nil
105 // }
106 // pkg := l.importPkg(".", path)
107 // if err := pkg.Error; err != nil {
108 // if _, noGo := err.(*noCUEError); noGo {
109 // return nil
110 // }
111 // }
112
113 // // If we are expanding "cmd", skip main
114 // // packages under cmd/vendor. At least as of
115 // // March, 2017, there is one there for the
116 // // vendored pprof tool.
117 // if pattern == "cmd" && strings.HasPrefix(pkg.DisplayPath, "cmd/vendor") && pkg.PkgName == "main" {
118 // return nil
119 // }
120
121 // m.Pkgs = append(m.Pkgs, pkg)
122 // return nil
123 // })
124 // }
125 return m
126}
127
128// matchPackagesInFS is like allPackages but is passed a pattern
129// beginning ./ or ../, meaning it should scan the tree rooted
130// at the given directory. There are ... in the pattern too.
131// (See go help packages for pattern syntax.)
132func (l *loader) matchPackagesInFS(pattern string) *match {
133 c := l.cfg
134 m := &match{
135 Pattern: pattern,
136 Literal: false,
137 }
138
139 // Find directory to begin the scan.
140 // Could be smarter but this one optimization
141 // is enough for now, since ... is usually at the
142 // end of a path.
143 i := strings.Index(pattern, "...")
144 dir, _ := path.Split(pattern[:i])
145
146 root := l.abs(dir)
147
148 if c.modRoot != "" {
149 if !hasFilepathPrefix(root, c.modRoot) {
150 m.Err = fmt.Errorf(
151 "cue: pattern %s refers to dir %s, outside module root %s",
152 pattern, root, c.modRoot)
153 return m
154 }
155 }
156
Marcel van Lohuizendb461442019-04-10 18:02:24 +0200157 pkgDir := filepath.Join(root, "pkg")
158
Marcel van Lohuizenbc4d65d2018-12-10 15:40:02 +0100159 filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
160 if err != nil || !fi.IsDir() {
161 return nil
162 }
Marcel van Lohuizendb461442019-04-10 18:02:24 +0200163 if path == pkgDir {
164 return filepath.SkipDir
165 }
Marcel van Lohuizenbc4d65d2018-12-10 15:40:02 +0100166
167 top := path == root
168
169 // Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..".
170 _, elem := filepath.Split(path)
171 dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".."
172 if dot || strings.HasPrefix(elem, "_") || (elem == "testdata" && !top) {
173 return filepath.SkipDir
174 }
175
176 if !top {
177 // Ignore other modules found in subdirectories.
178 if _, err := os.Stat(filepath.Join(path, modFile)); err == nil {
179 return filepath.SkipDir
180 }
181 }
182
183 // name := prefix + filepath.ToSlash(path)
184 // if !match(name) {
185 // return nil
186 // }
187
188 // We keep the directory if we can import it, or if we can't import it
189 // due to invalid CUE source files. This means that directories
190 // containing parse errors will be built (and fail) instead of being
191 // silently skipped as not matching the pattern.
192 p := l.importPkg("."+path[len(root):], root)
193 if err := p.Err; err != nil && (p == nil || len(p.InvalidCUEFiles) == 0) {
194 switch err.(type) {
195 case nil:
196 break
197 case *noCUEError:
198 if c.DataFiles && len(p.DataFiles) > 0 {
199 break
200 }
201 return nil
202 default:
Marcel van Lohuizen15fab6c2018-12-19 08:51:47 +0100203 m.Err = err
Marcel van Lohuizenbc4d65d2018-12-10 15:40:02 +0100204 }
205 }
206
207 m.Pkgs = append(m.Pkgs, p)
208 return nil
209 })
210 return m
211}
212
213// treeCanMatchPattern(pattern)(name) reports whether
214// name or children of name can possibly match pattern.
215// Pattern is the same limited glob accepted by matchPattern.
216func treeCanMatchPattern(pattern string) func(name string) bool {
217 wildCard := false
218 if i := strings.Index(pattern, "..."); i >= 0 {
219 wildCard = true
220 pattern = pattern[:i]
221 }
222 return func(name string) bool {
223 return len(name) <= len(pattern) && hasPathPrefix(pattern, name) ||
224 wildCard && strings.HasPrefix(name, pattern)
225 }
226}
227
228// matchPattern(pattern)(name) reports whether
229// name matches pattern. Pattern is a limited glob
230// pattern in which '...' means 'any string' and there
231// is no other special syntax.
232// Unfortunately, there are two special cases. Quoting "go help packages":
233//
234// First, /... at the end of the pattern can match an empty string,
235// so that net/... matches both net and packages in its subdirectories, like net/http.
236// Second, any slash-separted pattern element containing a wildcard never
237// participates in a match of the "vendor" element in the path of a vendored
238// package, so that ./... does not match packages in subdirectories of
239// ./vendor or ./mycode/vendor, but ./vendor/... and ./mycode/vendor/... do.
240// Note, however, that a directory named vendor that itself contains code
241// is not a vendored package: cmd/vendor would be a command named vendor,
242// and the pattern cmd/... matches it.
243func matchPattern(pattern string) func(name string) bool {
244 // Convert pattern to regular expression.
245 // The strategy for the trailing /... is to nest it in an explicit ? expression.
246 // The strategy for the vendor exclusion is to change the unmatchable
247 // vendor strings to a disallowed code point (vendorChar) and to use
248 // "(anything but that codepoint)*" as the implementation of the ... wildcard.
249 // This is a bit complicated but the obvious alternative,
250 // namely a hand-written search like in most shell glob matchers,
251 // is too easy to make accidentally exponential.
252 // Using package regexp guarantees linear-time matching.
253
254 const vendorChar = "\x00"
255
256 if strings.Contains(pattern, vendorChar) {
257 return func(name string) bool { return false }
258 }
259
260 re := regexp.QuoteMeta(pattern)
261 re = replaceVendor(re, vendorChar)
262 switch {
263 case strings.HasSuffix(re, `/`+vendorChar+`/\.\.\.`):
264 re = strings.TrimSuffix(re, `/`+vendorChar+`/\.\.\.`) + `(/vendor|/` + vendorChar + `/\.\.\.)`
265 case re == vendorChar+`/\.\.\.`:
266 re = `(/vendor|/` + vendorChar + `/\.\.\.)`
267 case strings.HasSuffix(re, `/\.\.\.`):
268 re = strings.TrimSuffix(re, `/\.\.\.`) + `(/\.\.\.)?`
269 }
270 re = strings.Replace(re, `\.\.\.`, `[^`+vendorChar+`]*`, -1)
271
272 reg := regexp.MustCompile(`^` + re + `$`)
273
274 return func(name string) bool {
275 if strings.Contains(name, vendorChar) {
276 return false
277 }
278 return reg.MatchString(replaceVendor(name, vendorChar))
279 }
280}
281
282// replaceVendor returns the result of replacing
283// non-trailing vendor path elements in x with repl.
284func replaceVendor(x, repl string) string {
285 if !strings.Contains(x, "vendor") {
286 return x
287 }
288 elem := strings.Split(x, "/")
289 for i := 0; i < len(elem)-1; i++ {
290 if elem[i] == "vendor" {
291 elem[i] = repl
292 }
293 }
294 return strings.Join(elem, "/")
295}
296
297// warnUnmatched warns about patterns that didn't match any packages.
298func warnUnmatched(matches []*match) {
299 for _, m := range matches {
300 if len(m.Pkgs) == 0 {
301 m.Err =
302 fmt.Errorf("cue: %q matched no packages\n", m.Pattern)
303 }
304 }
305}
306
307// importPaths returns the matching paths to use for the given command line.
308// It calls ImportPathsQuiet and then WarnUnmatched.
309func (l *loader) importPaths(patterns []string) []*match {
310 matches := l.importPathsQuiet(patterns)
311 warnUnmatched(matches)
312 return matches
313}
314
315// importPathsQuiet is like ImportPaths but does not warn about patterns with no matches.
316func (l *loader) importPathsQuiet(patterns []string) []*match {
317 var out []*match
318 for _, a := range cleanPatterns(patterns) {
319 if isMetaPackage(a) {
320 out = append(out, l.matchPackages(a))
321 continue
322 }
323 if strings.Contains(a, "...") {
324 if isLocalImport(a) {
325 out = append(out, l.matchPackagesInFS(a))
326 } else {
327 out = append(out, l.matchPackages(a))
328 }
329 continue
330 }
331
332 pkg := l.importPkg(a, l.cfg.Dir)
333 out = append(out, &match{Pattern: a, Literal: true, Pkgs: []*build.Instance{pkg}})
334 }
335 return out
336}
337
338// cleanPatterns returns the patterns to use for the given
339// command line. It canonicalizes the patterns but does not
340// evaluate any matches.
341func cleanPatterns(patterns []string) []string {
342 if len(patterns) == 0 {
343 return []string{"."}
344 }
345 var out []string
346 for _, a := range patterns {
347 // Arguments are supposed to be import paths, but
348 // as a courtesy to Windows developers, rewrite \ to /
349 // in command-line arguments. Handles .\... and so on.
350 if filepath.Separator == '\\' {
351 a = strings.Replace(a, `\`, `/`, -1)
352 }
353
354 // Put argument in canonical form, but preserve leading ./.
355 if strings.HasPrefix(a, "./") {
356 a = "./" + path.Clean(a)
357 if a == "./." {
358 a = "."
359 }
360 } else {
361 a = path.Clean(a)
362 }
363 out = append(out, a)
364 }
365 return out
366}
367
368// isMetaPackage checks if name is a reserved package name that expands to multiple packages.
369func isMetaPackage(name string) bool {
370 return name == "std" || name == "cmd" || name == "all"
371}
372
373// hasPathPrefix reports whether the path s begins with the
374// elements in prefix.
375func hasPathPrefix(s, prefix string) bool {
376 switch {
377 default:
378 return false
379 case len(s) == len(prefix):
380 return s == prefix
381 case len(s) > len(prefix):
382 if prefix != "" && prefix[len(prefix)-1] == '/' {
383 return strings.HasPrefix(s, prefix)
384 }
385 return s[len(prefix)] == '/' && s[:len(prefix)] == prefix
386 }
387}
388
389// hasFilepathPrefix reports whether the path s begins with the
390// elements in prefix.
391func hasFilepathPrefix(s, prefix string) bool {
392 switch {
393 default:
394 return false
395 case len(s) == len(prefix):
396 return s == prefix
397 case len(s) > len(prefix):
398 if prefix != "" && prefix[len(prefix)-1] == filepath.Separator {
399 return strings.HasPrefix(s, prefix)
400 }
401 return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == prefix
402 }
403}
404
405// isStandardImportPath reports whether $GOROOT/src/path should be considered
406// part of the standard distribution. For historical reasons we allow people to add
407// their own code to $GOROOT instead of using $GOPATH, but we assume that
408// code will start with a domain name (dot in the first element).
409//
410// Note that this function is meant to evaluate whether a directory found in GOROOT
411// should be treated as part of the standard library. It should not be used to decide
412// that a directory found in GOPATH should be rejected: directories in GOPATH
413// need not have dots in the first element, and they just take their chances
414// with future collisions in the standard library.
415func isStandardImportPath(path string) bool {
416 i := strings.Index(path, "/")
417 if i < 0 {
418 i = len(path)
419 }
420 elem := path[:i]
421 return !strings.Contains(elem, ".")
422}
423
424// isRelativePath reports whether pattern should be interpreted as a directory
425// path relative to the current directory, as opposed to a pattern matching
426// import paths.
427func isRelativePath(pattern string) bool {
428 return strings.HasPrefix(pattern, "./") || strings.HasPrefix(pattern, "../") || pattern == "." || pattern == ".."
429}
430
431// inDir checks whether path is in the file tree rooted at dir.
432// If so, inDir returns an equivalent path relative to dir.
433// If not, inDir returns an empty string.
434// inDir makes some effort to succeed even in the presence of symbolic links.
435// TODO(rsc): Replace internal/test.inDir with a call to this function for Go 1.12.
436func inDir(path, dir string) string {
437 if rel := inDirLex(path, dir); rel != "" {
438 return rel
439 }
440 xpath, err := filepath.EvalSymlinks(path)
441 if err != nil || xpath == path {
442 xpath = ""
443 } else {
444 if rel := inDirLex(xpath, dir); rel != "" {
445 return rel
446 }
447 }
448
449 xdir, err := filepath.EvalSymlinks(dir)
450 if err == nil && xdir != dir {
451 if rel := inDirLex(path, xdir); rel != "" {
452 return rel
453 }
454 if xpath != "" {
455 if rel := inDirLex(xpath, xdir); rel != "" {
456 return rel
457 }
458 }
459 }
460 return ""
461}
462
463// inDirLex is like inDir but only checks the lexical form of the file names.
464// It does not consider symbolic links.
465// TODO(rsc): This is a copy of str.HasFilePathPrefix, modified to
466// return the suffix. Most uses of str.HasFilePathPrefix should probably
467// be calling InDir instead.
468func inDirLex(path, dir string) string {
469 pv := strings.ToUpper(filepath.VolumeName(path))
470 dv := strings.ToUpper(filepath.VolumeName(dir))
471 path = path[len(pv):]
472 dir = dir[len(dv):]
473 switch {
474 default:
475 return ""
476 case pv != dv:
477 return ""
478 case len(path) == len(dir):
479 if path == dir {
480 return "."
481 }
482 return ""
483 case dir == "":
484 return path
485 case len(path) > len(dir):
486 if dir[len(dir)-1] == filepath.Separator {
487 if path[:len(dir)] == dir {
488 return path[len(dir):]
489 }
490 return ""
491 }
492 if path[len(dir)] == filepath.Separator && path[:len(dir)] == dir {
493 if len(path) == len(dir)+1 {
494 return "."
495 }
496 return path[len(dir)+1:]
497 }
498 return ""
499 }
500}