cuelang.org/go@v0.13.0/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  	"maps"
    20  	pathpkg "path"
    21  	"path/filepath"
    22  	"slices"
    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 = slices.Sorted(maps.Keys(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  	}
   184  	if err := setFileSource(fp.c, file); err != nil {
   185  		badFile(errors.Promote(err, ""))
   186  		return
   187  	}
   188  
   189  	if file.Encoding != build.CUE {
   190  		// Not a CUE file.
   191  		if sameDir {
   192  			p.OrphanedFiles = append(p.OrphanedFiles, file)
   193  		}
   194  		return
   195  	}
   196  	if (mode & allowExcludedFiles) == 0 {
   197  		var badPrefix string
   198  		for _, prefix := range []string{".", "_"} {
   199  			if strings.HasPrefix(base, prefix) {
   200  				badPrefix = prefix
   201  			}
   202  		}
   203  		if badPrefix != "" {
   204  			if !sameDir {
   205  				return
   206  			}
   207  			file.ExcludeReason = errors.Newf(token.NoPos, "filename starts with a '%s'", badPrefix)
   208  			if file.Interpretation == "" {
   209  				p.IgnoredFiles = append(p.IgnoredFiles, file)
   210  			} else {
   211  				p.OrphanedFiles = append(p.OrphanedFiles, file)
   212  			}
   213  			return
   214  		}
   215  	}
   216  	// Note: when path is "-" (stdin), it will already have
   217  	// been read and file.Source set to the resulting data
   218  	// by setFileSource.
   219  	pf, perr := fp.c.fileSystem.getCUESyntax(file)
   220  	if perr != nil {
   221  		badFile(errors.Promote(perr, "add failed"))
   222  		return
   223  	}
   224  
   225  	pkg := pf.PackageName()
   226  	if pkg == "" {
   227  		pkg = "_"
   228  	}
   229  	pos := pf.Pos()
   230  
   231  	switch {
   232  	case pkg == p.PkgName && (sameDir || pkg != "_"):
   233  		// We've got the exact package that's being looked for.
   234  		// It will already be present in fp.pkgs.
   235  	case mode&allowAnonymous != 0 && sameDir:
   236  		// It's an anonymous file that's not in a parent directory.
   237  	case fp.allPackages && pkg != "_":
   238  		q := fp.pkgs[pkg]
   239  		if q == nil && !sameDir {
   240  			// It's a file in a parent directory that doesn't correspond
   241  			// to a package in the original directory.
   242  			return
   243  		}
   244  		if q == nil {
   245  			q = fp.c.Context.NewInstance(p.Dir, nil)
   246  			q.PkgName = pkg
   247  			q.DisplayPath = p.DisplayPath
   248  			q.ImportPath = p.ImportPath + ":" + pkg
   249  			q.Root = p.Root
   250  			q.Module = p.Module
   251  			fp.pkgs[pkg] = q
   252  		}
   253  		p = q
   254  
   255  	case pkg != "_":
   256  		// We're loading a single package and we either haven't matched
   257  		// the earlier selected package or we haven't selected a package
   258  		// yet. In either case, the default package is the one we want to use.
   259  	default:
   260  		if sameDir {
   261  			file.ExcludeReason = excludeError{errors.Newf(pos, "no package name")}
   262  			p.IgnoredFiles = append(p.IgnoredFiles, file)
   263  		}
   264  		return
   265  	}
   266  
   267  	if !fp.c.AllCUEFiles {
   268  		tagIsSet := fp.tagger.tagIsSet
   269  		if p.Module != "" && p.Module != fp.c.Module {
   270  			// The file is outside the main module so treat all build tag keys as unset.
   271  			// Note that if there's no module, we don't consider it to be outside
   272  			// the main module, because otherwise @if tags in non-package files
   273  			// explicitly specified on the command line will not work.
   274  			tagIsSet = func(string) bool {
   275  				return false
   276  			}
   277  		}
   278  		if err := shouldBuildFile(pf, tagIsSet); err != nil {
   279  			if !errors.Is(err, errExclude) {
   280  				fp.err = errors.Append(fp.err, err)
   281  			}
   282  			file.ExcludeReason = err
   283  			p.IgnoredFiles = append(p.IgnoredFiles, file)
   284  			return
   285  		}
   286  	}
   287  
   288  	if pkg != "" && pkg != "_" {
   289  		if p.PkgName == "" {
   290  			p.PkgName = pkg
   291  			fp.firstFile = base
   292  		} else if pkg != p.PkgName {
   293  			if fp.ignoreOther {
   294  				file.ExcludeReason = excludeError{errors.Newf(pos,
   295  					"package is %s, want %s", pkg, p.PkgName)}
   296  				p.IgnoredFiles = append(p.IgnoredFiles, file)
   297  				return
   298  			}
   299  			if !fp.allPackages {
   300  				badFile(&MultiplePackageError{
   301  					Dir:      p.Dir,
   302  					Packages: []string{p.PkgName, pkg},
   303  					Files:    []string{fp.firstFile, base},
   304  				})
   305  				return
   306  			}
   307  		}
   308  	}
   309  
   310  	isTest := strings.HasSuffix(base, "_test"+cueSuffix)
   311  	isTool := strings.HasSuffix(base, "_tool"+cueSuffix)
   312  
   313  	for _, spec := range pf.Imports {
   314  		quoted := spec.Path.Value
   315  		path, err := strconv.Unquote(quoted)
   316  		if err != nil {
   317  			badFile(errors.Newf(
   318  				spec.Path.Pos(),
   319  				"%s: parser returned invalid quoted string: <%s>", fullPath, quoted,
   320  			))
   321  		}
   322  		if !isTest || fp.c.Tests {
   323  			fp.imported[path] = append(fp.imported[path], spec.Pos())
   324  		}
   325  	}
   326  	switch {
   327  	case isTest:
   328  		if fp.c.Tests {
   329  			p.BuildFiles = append(p.BuildFiles, file)
   330  		} else {
   331  			file.ExcludeReason = excludeError{errors.Newf(pos,
   332  				"_test.cue files excluded in non-test mode")}
   333  			p.IgnoredFiles = append(p.IgnoredFiles, file)
   334  		}
   335  	case isTool:
   336  		if fp.c.Tools {
   337  			p.BuildFiles = append(p.BuildFiles, file)
   338  		} else {
   339  			file.ExcludeReason = excludeError{errors.Newf(pos,
   340  				"_tool.cue files excluded in non-cmd mode")}
   341  			p.IgnoredFiles = append(p.IgnoredFiles, file)
   342  		}
   343  	default:
   344  		p.BuildFiles = append(p.BuildFiles, file)
   345  	}
   346  }
   347  
   348  // isLocalImport reports whether the import path is
   349  // a local import path, like ".", "..", "./foo", or "../foo".
   350  func isLocalImport(path string) bool {
   351  	return path == "." || path == ".." ||
   352  		strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../")
   353  }
   354  
   355  // warnUnmatched warns about patterns that didn't match any packages.
   356  func warnUnmatched(matches []*match) {
   357  	for _, m := range matches {
   358  		if len(m.Pkgs) == 0 {
   359  			m.Err = errors.Newf(token.NoPos, "cue: %q matched no packages", m.Pattern)
   360  		}
   361  	}
   362  }
   363  
   364  // cleanPatterns returns the patterns to use for the given
   365  // command line. It canonicalizes the patterns but does not
   366  // evaluate any matches.
   367  func cleanPatterns(patterns []string) []string {
   368  	if len(patterns) == 0 {
   369  		return []string{"."}
   370  	}
   371  	var out []string
   372  	for _, a := range patterns {
   373  		// Arguments are supposed to be import paths, but
   374  		// as a courtesy to Windows developers, rewrite \ to /
   375  		// in command-line arguments. Handles .\... and so on.
   376  		if filepath.Separator == '\\' {
   377  			a = strings.Replace(a, `\`, `/`, -1)
   378  		}
   379  
   380  		// Put argument in canonical form, but preserve leading "./".
   381  		if strings.HasPrefix(a, "./") {
   382  			a = "./" + pathpkg.Clean(a)
   383  			if a == "./." {
   384  				a = "."
   385  			}
   386  		} else if a != "" {
   387  			a = pathpkg.Clean(a)
   388  		}
   389  		out = append(out, a)
   390  	}
   391  	return out
   392  }
   393  
   394  // isMetaPackage checks if name is a reserved package name that expands to multiple packages.
   395  // TODO: none of these package names are actually recognized anywhere else
   396  // and at least one (cmd) doesn't seem like it belongs in the CUE world.
   397  func isMetaPackage(name string) bool {
   398  	return name == "std" || name == "cmd" || name == "all"
   399  }
   400  
   401  // hasFilepathPrefix reports whether the path s begins with the
   402  // elements in prefix.
   403  func hasFilepathPrefix(s, prefix string) bool {
   404  	switch {
   405  	default:
   406  		return false
   407  	case len(s) == len(prefix):
   408  		return s == prefix
   409  	case len(s) > len(prefix):
   410  		if prefix != "" && prefix[len(prefix)-1] == filepath.Separator {
   411  			return strings.HasPrefix(s, prefix)
   412  		}
   413  		return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == prefix
   414  	}
   415  }