cuelang.org/go@v0.10.1/cue/load/loader_common.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  	"cmp"
    19  	pathpkg "path"
    20  	"path/filepath"
    21  	"slices"
    22  	"sort"
    23  	"strconv"
    24  	"strings"
    25  
    26  	"cuelang.org/go/cue/build"
    27  	"cuelang.org/go/cue/errors"
    28  	"cuelang.org/go/cue/token"
    29  )
    30  
    31  // An importMode controls the behavior of the Import method.
    32  type importMode uint
    33  
    34  const (
    35  	allowAnonymous = 1 << iota
    36  	allowExcludedFiles
    37  )
    38  
    39  var errExclude = errors.New("file rejected")
    40  
    41  type cueError = errors.Error
    42  type excludeError struct {
    43  	cueError
    44  }
    45  
    46  func (e excludeError) Is(err error) bool { return err == errExclude }
    47  
    48  func rewriteFiles(p *build.Instance, root string, isLocal bool) {
    49  	p.Root = root
    50  
    51  	normalizeFiles(p.BuildFiles)
    52  	normalizeFiles(p.IgnoredFiles)
    53  	normalizeFiles(p.OrphanedFiles)
    54  	normalizeFiles(p.InvalidFiles)
    55  	normalizeFiles(p.UnknownFiles)
    56  }
    57  
    58  // normalizeFiles sorts the files so that files contained by a parent directory
    59  // always come before files contained in sub-directories, and that filenames in
    60  // the same directory are sorted lexically byte-wise, like Go's `<` operator.
    61  func normalizeFiles(files []*build.File) {
    62  	slices.SortFunc(files, func(a, b *build.File) int {
    63  		fa := a.Filename
    64  		fb := b.Filename
    65  		ca := strings.Count(fa, string(filepath.Separator))
    66  		cb := strings.Count(fb, string(filepath.Separator))
    67  		if c := cmp.Compare(ca, cb); c != 0 {
    68  			return c
    69  		}
    70  		return cmp.Compare(fa, fb)
    71  	})
    72  }
    73  
    74  func cleanImport(path string) string {
    75  	orig := path
    76  	path = pathpkg.Clean(path)
    77  	if strings.HasPrefix(orig, "./") && path != ".." && !strings.HasPrefix(path, "../") {
    78  		path = "./" + path
    79  	}
    80  	return path
    81  }
    82  
    83  // An importStack is a stack of import paths, possibly with the suffix " (test)" appended.
    84  // The import path of a test package is the import path of the corresponding
    85  // non-test package with the suffix "_test" added.
    86  type importStack []string
    87  
    88  func (s *importStack) Push(p string) {
    89  	*s = append(*s, p)
    90  }
    91  
    92  func (s *importStack) Pop() {
    93  	*s = (*s)[0 : len(*s)-1]
    94  }
    95  
    96  func (s *importStack) Copy() []string {
    97  	return slices.Clone(*s)
    98  }
    99  
   100  type fileProcessor struct {
   101  	firstFile   string
   102  	imported    map[string][]token.Pos
   103  	ignoreOther bool // ignore files from other packages
   104  	allPackages bool
   105  
   106  	c      *fileProcessorConfig
   107  	tagger *tagger
   108  	pkgs   map[string]*build.Instance
   109  	pkg    *build.Instance
   110  
   111  	err errors.Error
   112  }
   113  
   114  type fileProcessorConfig = Config
   115  
   116  func newFileProcessor(c *fileProcessorConfig, p *build.Instance, tg *tagger) *fileProcessor {
   117  	return &fileProcessor{
   118  		imported: make(map[string][]token.Pos),
   119  		c:        c,
   120  		pkgs:     map[string]*build.Instance{"_": p},
   121  		pkg:      p,
   122  		tagger:   tg,
   123  	}
   124  }
   125  
   126  func countCUEFiles(c *fileProcessorConfig, p *build.Instance) int {
   127  	count := len(p.BuildFiles)
   128  	for _, f := range p.IgnoredFiles {
   129  		if c.Tools && strings.HasSuffix(f.Filename, "_tool.cue") {
   130  			count++
   131  		}
   132  		if c.Tests && strings.HasSuffix(f.Filename, "_test.cue") {
   133  			count++
   134  		}
   135  	}
   136  	return count
   137  }
   138  
   139  func (fp *fileProcessor) finalize(p *build.Instance) errors.Error {
   140  	if fp.err != nil {
   141  		return fp.err
   142  	}
   143  	if countCUEFiles(fp.c, p) == 0 &&
   144  		!fp.c.DataFiles &&
   145  		(p.PkgName != "_" || !fp.allPackages) {
   146  		fp.err = errors.Append(fp.err, &NoFilesError{Package: p, ignored: len(p.IgnoredFiles) > 0})
   147  		return fp.err
   148  	}
   149  
   150  	p.ImportPaths, _ = cleanImports(fp.imported)
   151  
   152  	return nil
   153  }
   154  
   155  // add adds the given file to the appropriate package in fp.
   156  func (fp *fileProcessor) add(root string, file *build.File, mode importMode) {
   157  	fullPath := file.Filename
   158  	if fullPath != "-" {
   159  		if !filepath.IsAbs(fullPath) {
   160  			fullPath = filepath.Join(root, fullPath)
   161  		}
   162  		file.Filename = fullPath
   163  	}
   164  
   165  	base := filepath.Base(fullPath)
   166  
   167  	// special * and _
   168  	p := fp.pkg // default package
   169  
   170  	// sameDir holds whether the file should be considered to be
   171  	// part of the same directory as the default package. This is
   172  	// true when the file is part of the original package directory
   173  	// or when allowExcludedFiles is specified, signifying that the
   174  	// file is part of an explicit set of files provided on the
   175  	// command line.
   176  	sameDir := filepath.Dir(fullPath) == p.Dir || (mode&allowExcludedFiles) != 0
   177  
   178  	// badFile := func(p *build.Instance, err errors.Error) bool {
   179  	badFile := func(err errors.Error) {
   180  		fp.err = errors.Append(fp.err, err)
   181  		file.ExcludeReason = fp.err
   182  		p.InvalidFiles = append(p.InvalidFiles, file)
   183  		return
   184  	}
   185  	if err := setFileSource(fp.c, file); err != nil {
   186  		badFile(errors.Promote(err, ""))
   187  		return
   188  	}
   189  
   190  	if file.Encoding != build.CUE {
   191  		// Not a CUE file.
   192  		if sameDir {
   193  			p.OrphanedFiles = append(p.OrphanedFiles, file)
   194  		}
   195  		return
   196  	}
   197  	if (mode & allowExcludedFiles) == 0 {
   198  		var badPrefix string
   199  		for _, prefix := range []string{".", "_"} {
   200  			if strings.HasPrefix(base, prefix) {
   201  				badPrefix = prefix
   202  			}
   203  		}
   204  		if badPrefix != "" {
   205  			if !sameDir {
   206  				return
   207  			}
   208  			file.ExcludeReason = errors.Newf(token.NoPos, "filename starts with a '%s'", badPrefix)
   209  			if file.Interpretation == "" {
   210  				p.IgnoredFiles = append(p.IgnoredFiles, file)
   211  			} else {
   212  				p.OrphanedFiles = append(p.OrphanedFiles, file)
   213  			}
   214  			return
   215  		}
   216  	}
   217  	// Note: when path is "-" (stdin), it will already have
   218  	// been read and file.Source set to the resulting data
   219  	// by setFileSource.
   220  	pf, perr := fp.c.fileSystem.getCUESyntax(file)
   221  	if perr != nil {
   222  		badFile(errors.Promote(perr, "add failed"))
   223  		return
   224  	}
   225  
   226  	pkg := pf.PackageName()
   227  	if pkg == "" {
   228  		pkg = "_"
   229  	}
   230  	pos := pf.Pos()
   231  
   232  	switch {
   233  	case pkg == p.PkgName && (sameDir || pkg != "_"):
   234  		// We've got the exact package that's being looked for.
   235  		// It will already be present in fp.pkgs.
   236  	case mode&allowAnonymous != 0 && sameDir:
   237  		// It's an anonymous file that's not in a parent directory.
   238  	case fp.allPackages && pkg != "_":
   239  		q := fp.pkgs[pkg]
   240  		if q == nil && !sameDir {
   241  			// It's a file in a parent directory that doesn't correspond
   242  			// to a package in the original directory.
   243  			return
   244  		}
   245  		if q == nil {
   246  			q = &build.Instance{
   247  				PkgName: pkg,
   248  
   249  				Dir:         p.Dir,
   250  				DisplayPath: p.DisplayPath,
   251  				ImportPath:  p.ImportPath + ":" + pkg,
   252  				Root:        p.Root,
   253  				Module:      p.Module,
   254  			}
   255  			fp.pkgs[pkg] = q
   256  		}
   257  		p = q
   258  
   259  	case pkg != "_":
   260  		// We're loading a single package and we either haven't matched
   261  		// the earlier selected package or we haven't selected a package
   262  		// yet. In either case, the default package is the one we want to use.
   263  	default:
   264  		if sameDir {
   265  			file.ExcludeReason = excludeError{errors.Newf(pos, "no package name")}
   266  			p.IgnoredFiles = append(p.IgnoredFiles, file)
   267  		}
   268  		return
   269  	}
   270  
   271  	if !fp.c.AllCUEFiles {
   272  		tagIsSet := fp.tagger.tagIsSet
   273  		if p.Module != "" && p.Module != fp.c.Module {
   274  			// The file is outside the main module so treat all build tag keys as unset.
   275  			// Note that if there's no module, we don't consider it to be outside
   276  			// the main module, because otherwise @if tags in non-package files
   277  			// explicitly specified on the command line will not work.
   278  			tagIsSet = func(string) bool {
   279  				return false
   280  			}
   281  		}
   282  		if err := shouldBuildFile(pf, tagIsSet); err != nil {
   283  			if !errors.Is(err, errExclude) {
   284  				fp.err = errors.Append(fp.err, err)
   285  			}
   286  			file.ExcludeReason = err
   287  			p.IgnoredFiles = append(p.IgnoredFiles, file)
   288  			return
   289  		}
   290  	}
   291  
   292  	if pkg != "" && pkg != "_" {
   293  		if p.PkgName == "" {
   294  			p.PkgName = pkg
   295  			fp.firstFile = base
   296  		} else if pkg != p.PkgName {
   297  			if fp.ignoreOther {
   298  				file.ExcludeReason = excludeError{errors.Newf(pos,
   299  					"package is %s, want %s", pkg, p.PkgName)}
   300  				p.IgnoredFiles = append(p.IgnoredFiles, file)
   301  				return
   302  			}
   303  			if !fp.allPackages {
   304  				badFile(&MultiplePackageError{
   305  					Dir:      p.Dir,
   306  					Packages: []string{p.PkgName, pkg},
   307  					Files:    []string{fp.firstFile, base},
   308  				})
   309  				return
   310  			}
   311  		}
   312  	}
   313  
   314  	isTest := strings.HasSuffix(base, "_test"+cueSuffix)
   315  	isTool := strings.HasSuffix(base, "_tool"+cueSuffix)
   316  
   317  	for _, spec := range pf.Imports {
   318  		quoted := spec.Path.Value
   319  		path, err := strconv.Unquote(quoted)
   320  		if err != nil {
   321  			badFile(errors.Newf(
   322  				spec.Path.Pos(),
   323  				"%s: parser returned invalid quoted string: <%s>", fullPath, quoted,
   324  			))
   325  		}
   326  		if !isTest || fp.c.Tests {
   327  			fp.imported[path] = append(fp.imported[path], spec.Pos())
   328  		}
   329  	}
   330  	switch {
   331  	case isTest:
   332  		if fp.c.Tests {
   333  			p.BuildFiles = append(p.BuildFiles, file)
   334  		} else {
   335  			file.ExcludeReason = excludeError{errors.Newf(pos,
   336  				"_test.cue files excluded in non-test mode")}
   337  			p.IgnoredFiles = append(p.IgnoredFiles, file)
   338  		}
   339  	case isTool:
   340  		if fp.c.Tools {
   341  			p.BuildFiles = append(p.BuildFiles, file)
   342  		} else {
   343  			file.ExcludeReason = excludeError{errors.Newf(pos,
   344  				"_tool.cue files excluded in non-cmd mode")}
   345  			p.IgnoredFiles = append(p.IgnoredFiles, file)
   346  		}
   347  	default:
   348  		p.BuildFiles = append(p.BuildFiles, file)
   349  	}
   350  }
   351  
   352  func cleanImports(m map[string][]token.Pos) ([]string, map[string][]token.Pos) {
   353  	all := make([]string, 0, len(m))
   354  	for path := range m {
   355  		all = append(all, path)
   356  	}
   357  	sort.Strings(all)
   358  	return all, m
   359  }
   360  
   361  // isLocalImport reports whether the import path is
   362  // a local import path, like ".", "..", "./foo", or "../foo".
   363  func isLocalImport(path string) bool {
   364  	return path == "." || path == ".." ||
   365  		strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../")
   366  }
   367  
   368  // warnUnmatched warns about patterns that didn't match any packages.
   369  func warnUnmatched(matches []*match) {
   370  	for _, m := range matches {
   371  		if len(m.Pkgs) == 0 {
   372  			m.Err = errors.Newf(token.NoPos, "cue: %q matched no packages", m.Pattern)
   373  		}
   374  	}
   375  }
   376  
   377  // cleanPatterns returns the patterns to use for the given
   378  // command line. It canonicalizes the patterns but does not
   379  // evaluate any matches.
   380  func cleanPatterns(patterns []string) []string {
   381  	if len(patterns) == 0 {
   382  		return []string{"."}
   383  	}
   384  	var out []string
   385  	for _, a := range patterns {
   386  		// Arguments are supposed to be import paths, but
   387  		// as a courtesy to Windows developers, rewrite \ to /
   388  		// in command-line arguments. Handles .\... and so on.
   389  		if filepath.Separator == '\\' {
   390  			a = strings.Replace(a, `\`, `/`, -1)
   391  		}
   392  
   393  		// Put argument in canonical form, but preserve leading "./".
   394  		if strings.HasPrefix(a, "./") {
   395  			a = "./" + pathpkg.Clean(a)
   396  			if a == "./." {
   397  				a = "."
   398  			}
   399  		} else if a != "" {
   400  			a = pathpkg.Clean(a)
   401  		}
   402  		out = append(out, a)
   403  	}
   404  	return out
   405  }
   406  
   407  // isMetaPackage checks if name is a reserved package name that expands to multiple packages.
   408  // TODO: none of these package names are actually recognized anywhere else
   409  // and at least one (cmd) doesn't seem like it belongs in the CUE world.
   410  func isMetaPackage(name string) bool {
   411  	return name == "std" || name == "cmd" || name == "all"
   412  }
   413  
   414  // hasFilepathPrefix reports whether the path s begins with the
   415  // elements in prefix.
   416  func hasFilepathPrefix(s, prefix string) bool {
   417  	switch {
   418  	default:
   419  		return false
   420  	case len(s) == len(prefix):
   421  		return s == prefix
   422  	case len(s) > len(prefix):
   423  		if prefix != "" && prefix[len(prefix)-1] == filepath.Separator {
   424  			return strings.HasPrefix(s, prefix)
   425  		}
   426  		return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == prefix
   427  	}
   428  }