github.com/joomcode/cue@v0.4.4-0.20221111115225-539fe3512047/cue/load/import.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  	"bytes"
    19  	"os"
    20  	"path/filepath"
    21  	"sort"
    22  	"strconv"
    23  	"strings"
    24  	"unicode"
    25  	"unicode/utf8"
    26  
    27  	"github.com/joomcode/cue/cue/ast"
    28  	"github.com/joomcode/cue/cue/build"
    29  	"github.com/joomcode/cue/cue/errors"
    30  	"github.com/joomcode/cue/cue/parser"
    31  	"github.com/joomcode/cue/cue/token"
    32  	"github.com/joomcode/cue/internal"
    33  	"github.com/joomcode/cue/internal/filetypes"
    34  )
    35  
    36  // An importMode controls the behavior of the Import method.
    37  type importMode uint
    38  
    39  const (
    40  	// If findOnly is set, Import stops after locating the directory
    41  	// that should contain the sources for a package. It does not
    42  	// read any files in the directory.
    43  	findOnly importMode = 1 << iota
    44  
    45  	// If importComment is set, parse import comments on package statements.
    46  	// Import returns an error if it finds a comment it cannot understand
    47  	// or finds conflicting comments in multiple source files.
    48  	// See golang.org/s/go14customimport for more information.
    49  	importComment
    50  
    51  	allowAnonymous
    52  )
    53  
    54  // importPkg returns details about the CUE package named by the import path,
    55  // interpreting local import paths relative to the srcDir directory.
    56  // If the path is a local import path naming a package that can be imported
    57  // using a standard import path, the returned package will set p.ImportPath
    58  // to that path.
    59  //
    60  // In the directory and ancestor directories up to including one with a
    61  // cue.mod file, all .cue files are considered part of the package except for:
    62  //
    63  //	- files starting with _ or . (likely editor temporary files)
    64  //	- files with build constraints not satisfied by the context
    65  //
    66  // If an error occurs, importPkg sets the error in the returned instance,
    67  // which then may contain partial information.
    68  //
    69  // pkgName indicates which packages to load. It supports the following
    70  // values:
    71  //     ""      the default package for the directory, if only one
    72  //             is present.
    73  //     _       anonymous files (which may be marked with _)
    74  //     *       all packages
    75  //
    76  func (l *loader) importPkg(pos token.Pos, p *build.Instance) []*build.Instance {
    77  	l.stk.Push(p.ImportPath)
    78  	defer l.stk.Pop()
    79  
    80  	cfg := l.cfg
    81  	ctxt := &cfg.fileSystem
    82  
    83  	if p.Err != nil {
    84  		return []*build.Instance{p}
    85  	}
    86  
    87  	retErr := func(errs errors.Error) []*build.Instance {
    88  		// XXX: move this loop to ReportError
    89  		for _, err := range errors.Errors(errs) {
    90  			p.ReportError(err)
    91  		}
    92  		return []*build.Instance{p}
    93  	}
    94  
    95  	if !strings.HasPrefix(p.Dir, cfg.ModuleRoot) {
    96  		err := errors.Newf(token.NoPos, "module root not defined", p.DisplayPath)
    97  		return retErr(err)
    98  	}
    99  
   100  	fp := newFileProcessor(cfg, p)
   101  
   102  	if p.PkgName == "" {
   103  		if l.cfg.Package == "*" {
   104  			fp.ignoreOther = true
   105  			fp.allPackages = true
   106  			p.PkgName = "_"
   107  		} else {
   108  			p.PkgName = l.cfg.Package
   109  		}
   110  	}
   111  	if p.PkgName != "" {
   112  		// If we have an explicit package name, we can ignore other packages.
   113  		fp.ignoreOther = true
   114  	}
   115  
   116  	if !strings.HasPrefix(p.Dir, cfg.ModuleRoot) {
   117  		panic("")
   118  	}
   119  
   120  	var dirs [][2]string
   121  	genDir := GenPath(cfg.ModuleRoot)
   122  	if strings.HasPrefix(p.Dir, genDir) {
   123  		dirs = append(dirs, [2]string{genDir, p.Dir})
   124  		// TODO(legacy): don't support "pkg"
   125  		// && p.PkgName != "_"
   126  		if filepath.Base(genDir) != "pkg" {
   127  			for _, sub := range []string{"pkg", "usr"} {
   128  				rel, err := filepath.Rel(genDir, p.Dir)
   129  				if err != nil {
   130  					// should not happen
   131  					return retErr(
   132  						errors.Wrapf(err, token.NoPos, "invalid path"))
   133  				}
   134  				base := filepath.Join(cfg.ModuleRoot, modDir, sub)
   135  				dir := filepath.Join(base, rel)
   136  				dirs = append(dirs, [2]string{base, dir})
   137  			}
   138  		}
   139  	} else {
   140  		dirs = append(dirs, [2]string{cfg.ModuleRoot, p.Dir})
   141  	}
   142  
   143  	found := false
   144  	for _, d := range dirs {
   145  		info, err := ctxt.stat(d[1])
   146  		if err == nil && info.IsDir() {
   147  			found = true
   148  			break
   149  		}
   150  	}
   151  
   152  	if !found {
   153  		return retErr(
   154  			&PackageError{
   155  				Message: errors.NewMessage("cannot find package %q",
   156  					[]interface{}{p.DisplayPath}),
   157  			})
   158  	}
   159  
   160  	// This algorithm assumes that multiple directories within cue.mod/*/
   161  	// have the same module scope and that there are no invalid modules.
   162  	inModule := false // if pkg == "_"
   163  	for _, d := range dirs {
   164  		if l.cfg.findRoot(d[1]) != "" {
   165  			inModule = true
   166  			break
   167  		}
   168  	}
   169  
   170  	for _, d := range dirs {
   171  		for dir := filepath.Clean(d[1]); ctxt.isDir(dir); {
   172  			files, err := ctxt.readDir(dir)
   173  			if err != nil && !os.IsNotExist(err) {
   174  				return retErr(errors.Wrapf(err, pos, "import failed reading dir %v", dirs[0][1]))
   175  			}
   176  			for _, f := range files {
   177  				if f.IsDir() {
   178  					continue
   179  				}
   180  				if f.Name() == "-" {
   181  					if _, err := cfg.fileSystem.stat("-"); !os.IsNotExist(err) {
   182  						continue
   183  					}
   184  				}
   185  				file, err := filetypes.ParseFile(f.Name(), filetypes.Input)
   186  				if err != nil {
   187  					p.UnknownFiles = append(p.UnknownFiles, &build.File{
   188  						Filename:      f.Name(),
   189  						ExcludeReason: errors.Newf(token.NoPos, "unknown filetype"),
   190  					})
   191  					continue // skip unrecognized file types
   192  				}
   193  				fp.add(pos, dir, file, importComment)
   194  			}
   195  
   196  			if p.PkgName == "" || !inModule || l.cfg.isRoot(dir) || dir == d[0] {
   197  				break
   198  			}
   199  
   200  			// From now on we just ignore files that do not belong to the same
   201  			// package.
   202  			fp.ignoreOther = true
   203  
   204  			parent, _ := filepath.Split(dir)
   205  			parent = filepath.Clean(parent)
   206  
   207  			if parent == dir || len(parent) < len(d[0]) {
   208  				break
   209  			}
   210  			dir = parent
   211  		}
   212  	}
   213  
   214  	all := []*build.Instance{}
   215  
   216  	for _, p := range fp.pkgs {
   217  		impPath, err := addImportQualifier(importPath(p.ImportPath), p.PkgName)
   218  		p.ImportPath = string(impPath)
   219  		if err != nil {
   220  			p.ReportError(err)
   221  		}
   222  
   223  		all = append(all, p)
   224  		rewriteFiles(p, cfg.ModuleRoot, false)
   225  		if errs := fp.finalize(p); errs != nil {
   226  			p.ReportError(errs)
   227  			return all
   228  		}
   229  
   230  		l.addFiles(cfg.ModuleRoot, p)
   231  		_ = p.Complete()
   232  	}
   233  	sort.Slice(all, func(i, j int) bool {
   234  		return all[i].Dir < all[j].Dir
   235  	})
   236  	return all
   237  }
   238  
   239  // loadFunc creates a LoadFunc that can be used to create new build.Instances.
   240  func (l *loader) loadFunc() build.LoadFunc {
   241  
   242  	return func(pos token.Pos, path string) *build.Instance {
   243  		cfg := l.cfg
   244  
   245  		impPath := importPath(path)
   246  		if isLocalImport(path) {
   247  			return cfg.newErrInstance(pos, impPath,
   248  				errors.Newf(pos, "relative import paths not allowed (%q)", path))
   249  		}
   250  
   251  		// is it a builtin?
   252  		if strings.IndexByte(strings.Split(path, "/")[0], '.') == -1 {
   253  			if l.cfg.StdRoot != "" {
   254  				p := cfg.newInstance(pos, impPath)
   255  				_ = l.importPkg(pos, p)
   256  				return p
   257  			}
   258  			return nil
   259  		}
   260  
   261  		p := cfg.newInstance(pos, impPath)
   262  		_ = l.importPkg(pos, p)
   263  		return p
   264  	}
   265  }
   266  
   267  func rewriteFiles(p *build.Instance, root string, isLocal bool) {
   268  	p.Root = root
   269  
   270  	normalizeFiles(p.BuildFiles)
   271  	normalizeFiles(p.IgnoredFiles)
   272  	normalizeFiles(p.OrphanedFiles)
   273  	normalizeFiles(p.InvalidFiles)
   274  	normalizeFiles(p.UnknownFiles)
   275  }
   276  
   277  func normalizeFiles(a []*build.File) {
   278  	sort.Slice(a, func(i, j int) bool {
   279  		return len(filepath.Dir(a[i].Filename)) < len(filepath.Dir(a[j].Filename))
   280  	})
   281  }
   282  
   283  type fileProcessor struct {
   284  	firstFile        string
   285  	firstCommentFile string
   286  	imported         map[string][]token.Pos
   287  	allTags          map[string]bool
   288  	allFiles         bool
   289  	ignoreOther      bool // ignore files from other packages
   290  	allPackages      bool
   291  
   292  	c    *Config
   293  	pkgs map[string]*build.Instance
   294  	pkg  *build.Instance
   295  
   296  	err errors.Error
   297  }
   298  
   299  func newFileProcessor(c *Config, p *build.Instance) *fileProcessor {
   300  	return &fileProcessor{
   301  		imported: make(map[string][]token.Pos),
   302  		allTags:  make(map[string]bool),
   303  		c:        c,
   304  		pkgs:     map[string]*build.Instance{"_": p},
   305  		pkg:      p,
   306  	}
   307  }
   308  
   309  func countCUEFiles(c *Config, p *build.Instance) int {
   310  	count := len(p.BuildFiles)
   311  	for _, f := range p.IgnoredFiles {
   312  		if c.Tools && strings.HasSuffix(f.Filename, "_tool.cue") {
   313  			count++
   314  		}
   315  		if c.Tests && strings.HasSuffix(f.Filename, "_test.cue") {
   316  			count++
   317  		}
   318  	}
   319  	return count
   320  }
   321  
   322  func (fp *fileProcessor) finalize(p *build.Instance) errors.Error {
   323  	if fp.err != nil {
   324  		return fp.err
   325  	}
   326  	if countCUEFiles(fp.c, p) == 0 &&
   327  		!fp.c.DataFiles &&
   328  		(p.PkgName != "_" || !fp.allPackages) {
   329  		fp.err = errors.Append(fp.err, &NoFilesError{Package: p, ignored: len(p.IgnoredFiles) > 0})
   330  		return fp.err
   331  	}
   332  
   333  	for tag := range fp.allTags {
   334  		p.AllTags = append(p.AllTags, tag)
   335  	}
   336  	sort.Strings(p.AllTags)
   337  
   338  	p.ImportPaths, _ = cleanImports(fp.imported)
   339  
   340  	return nil
   341  }
   342  
   343  func (fp *fileProcessor) add(pos token.Pos, root string, file *build.File, mode importMode) (added bool) {
   344  	fullPath := file.Filename
   345  	if fullPath != "-" {
   346  		if !filepath.IsAbs(fullPath) {
   347  			fullPath = filepath.Join(root, fullPath)
   348  		}
   349  	}
   350  	file.Filename = fullPath
   351  
   352  	base := filepath.Base(fullPath)
   353  
   354  	// special * and _
   355  	p := fp.pkg // default package
   356  
   357  	// badFile := func(p *build.Instance, err errors.Error) bool {
   358  	badFile := func(err errors.Error) bool {
   359  		fp.err = errors.Append(fp.err, err)
   360  		file.ExcludeReason = fp.err
   361  		p.InvalidFiles = append(p.InvalidFiles, file)
   362  		return true
   363  	}
   364  
   365  	match, data, err := matchFile(fp.c, file, true, fp.allFiles, fp.allTags)
   366  	switch {
   367  	case match:
   368  
   369  	case err == nil:
   370  		// Not a CUE file.
   371  		p.OrphanedFiles = append(p.OrphanedFiles, file)
   372  		return false
   373  
   374  	case !errors.Is(err, errExclude):
   375  		return badFile(err)
   376  
   377  	default:
   378  		file.ExcludeReason = err
   379  		if file.Interpretation == "" {
   380  			p.IgnoredFiles = append(p.IgnoredFiles, file)
   381  		} else {
   382  			p.OrphanedFiles = append(p.OrphanedFiles, file)
   383  		}
   384  		return false
   385  	}
   386  
   387  	pf, perr := parser.ParseFile(fullPath, data, parser.ImportsOnly, parser.ParseComments)
   388  	if perr != nil {
   389  		badFile(errors.Promote(perr, "add failed"))
   390  		return true
   391  	}
   392  
   393  	_, pkg, pos := internal.PackageInfo(pf)
   394  	if pkg == "" {
   395  		pkg = "_"
   396  	}
   397  
   398  	switch {
   399  	case pkg == p.PkgName, mode&allowAnonymous != 0:
   400  	case fp.allPackages && pkg != "_":
   401  		q := fp.pkgs[pkg]
   402  		if q == nil {
   403  			q = &build.Instance{
   404  				PkgName: pkg,
   405  
   406  				Dir:         p.Dir,
   407  				DisplayPath: p.DisplayPath,
   408  				ImportPath:  p.ImportPath + ":" + pkg,
   409  				Root:        p.Root,
   410  				Module:      p.Module,
   411  			}
   412  			fp.pkgs[pkg] = q
   413  		}
   414  		p = q
   415  
   416  	case pkg != "_":
   417  
   418  	default:
   419  		file.ExcludeReason = excludeError{errors.Newf(pos, "no package name")}
   420  		p.IgnoredFiles = append(p.IgnoredFiles, file)
   421  		return false // don't mark as added
   422  	}
   423  
   424  	if !fp.c.AllCUEFiles {
   425  		if err := shouldBuildFile(pf, fp); err != nil {
   426  			if !errors.Is(err, errExclude) {
   427  				fp.err = errors.Append(fp.err, err)
   428  			}
   429  			file.ExcludeReason = err
   430  			p.IgnoredFiles = append(p.IgnoredFiles, file)
   431  			return false
   432  		}
   433  	}
   434  
   435  	if pkg != "" && pkg != "_" {
   436  		if p.PkgName == "" {
   437  			p.PkgName = pkg
   438  			fp.firstFile = base
   439  		} else if pkg != p.PkgName {
   440  			if fp.ignoreOther {
   441  				file.ExcludeReason = excludeError{errors.Newf(pos,
   442  					"package is %s, want %s", pkg, p.PkgName)}
   443  				p.IgnoredFiles = append(p.IgnoredFiles, file)
   444  				return false
   445  			}
   446  			return badFile(&MultiplePackageError{
   447  				Dir:      p.Dir,
   448  				Packages: []string{p.PkgName, pkg},
   449  				Files:    []string{fp.firstFile, base},
   450  			})
   451  		}
   452  	}
   453  
   454  	isTest := strings.HasSuffix(base, "_test"+cueSuffix)
   455  	isTool := strings.HasSuffix(base, "_tool"+cueSuffix)
   456  
   457  	if mode&importComment != 0 {
   458  		qcom, line := findimportComment(data)
   459  		if line != 0 {
   460  			com, err := strconv.Unquote(qcom)
   461  			if err != nil {
   462  				badFile(errors.Newf(pos, "%s:%d: cannot parse import comment", fullPath, line))
   463  			} else if p.ImportComment == "" {
   464  				p.ImportComment = com
   465  				fp.firstCommentFile = base
   466  			} else if p.ImportComment != com {
   467  				badFile(errors.Newf(pos, "found import comments %q (%s) and %q (%s) in %s", p.ImportComment, fp.firstCommentFile, com, base, p.Dir))
   468  			}
   469  		}
   470  	}
   471  
   472  	for _, decl := range pf.Decls {
   473  		d, ok := decl.(*ast.ImportDecl)
   474  		if !ok {
   475  			continue
   476  		}
   477  		for _, spec := range d.Specs {
   478  			quoted := spec.Path.Value
   479  			path, err := strconv.Unquote(quoted)
   480  			if err != nil {
   481  				badFile(errors.Newf(
   482  					spec.Path.Pos(),
   483  					"%s: parser returned invalid quoted string: <%s>", fullPath, quoted,
   484  				))
   485  			}
   486  			if !isTest || fp.c.Tests {
   487  				fp.imported[path] = append(fp.imported[path], spec.Pos())
   488  			}
   489  		}
   490  	}
   491  	switch {
   492  	case isTest:
   493  		if fp.c.loader.cfg.Tests {
   494  			p.BuildFiles = append(p.BuildFiles, file)
   495  		} else {
   496  			file.ExcludeReason = excludeError{errors.Newf(pos,
   497  				"_test.cue files excluded in non-test mode")}
   498  			p.IgnoredFiles = append(p.IgnoredFiles, file)
   499  		}
   500  	case isTool:
   501  		if fp.c.loader.cfg.Tools {
   502  			p.BuildFiles = append(p.BuildFiles, file)
   503  		} else {
   504  			file.ExcludeReason = excludeError{errors.Newf(pos,
   505  				"_tool.cue files excluded in non-cmd mode")}
   506  			p.IgnoredFiles = append(p.IgnoredFiles, file)
   507  		}
   508  	default:
   509  		p.BuildFiles = append(p.BuildFiles, file)
   510  	}
   511  	return true
   512  }
   513  
   514  func findimportComment(data []byte) (s string, line int) {
   515  	// expect keyword package
   516  	word, data := parseWord(data)
   517  	if string(word) != "package" {
   518  		return "", 0
   519  	}
   520  
   521  	// expect package name
   522  	_, data = parseWord(data)
   523  
   524  	// now ready for import comment, a // comment
   525  	// beginning and ending on the current line.
   526  	for len(data) > 0 && (data[0] == ' ' || data[0] == '\t' || data[0] == '\r') {
   527  		data = data[1:]
   528  	}
   529  
   530  	var comment []byte
   531  	switch {
   532  	case bytes.HasPrefix(data, slashSlash):
   533  		i := bytes.Index(data, newline)
   534  		if i < 0 {
   535  			i = len(data)
   536  		}
   537  		comment = data[2:i]
   538  	}
   539  	comment = bytes.TrimSpace(comment)
   540  
   541  	// split comment into `import`, `"pkg"`
   542  	word, arg := parseWord(comment)
   543  	if string(word) != "import" {
   544  		return "", 0
   545  	}
   546  
   547  	line = 1 + bytes.Count(data[:cap(data)-cap(arg)], newline)
   548  	return strings.TrimSpace(string(arg)), line
   549  }
   550  
   551  var (
   552  	slashSlash = []byte("//")
   553  	newline    = []byte("\n")
   554  )
   555  
   556  // skipSpaceOrComment returns data with any leading spaces or comments removed.
   557  func skipSpaceOrComment(data []byte) []byte {
   558  	for len(data) > 0 {
   559  		switch data[0] {
   560  		case ' ', '\t', '\r', '\n':
   561  			data = data[1:]
   562  			continue
   563  		case '/':
   564  			if bytes.HasPrefix(data, slashSlash) {
   565  				i := bytes.Index(data, newline)
   566  				if i < 0 {
   567  					return nil
   568  				}
   569  				data = data[i+1:]
   570  				continue
   571  			}
   572  		}
   573  		break
   574  	}
   575  	return data
   576  }
   577  
   578  // parseWord skips any leading spaces or comments in data
   579  // and then parses the beginning of data as an identifier or keyword,
   580  // returning that word and what remains after the word.
   581  func parseWord(data []byte) (word, rest []byte) {
   582  	data = skipSpaceOrComment(data)
   583  
   584  	// Parse past leading word characters.
   585  	rest = data
   586  	for {
   587  		r, size := utf8.DecodeRune(rest)
   588  		if unicode.IsLetter(r) || '0' <= r && r <= '9' || r == '_' {
   589  			rest = rest[size:]
   590  			continue
   591  		}
   592  		break
   593  	}
   594  
   595  	word = data[:len(data)-len(rest)]
   596  	if len(word) == 0 {
   597  		return nil, nil
   598  	}
   599  
   600  	return word, rest
   601  }
   602  
   603  func cleanImports(m map[string][]token.Pos) ([]string, map[string][]token.Pos) {
   604  	all := make([]string, 0, len(m))
   605  	for path := range m {
   606  		all = append(all, path)
   607  	}
   608  	sort.Strings(all)
   609  	return all, m
   610  }
   611  
   612  // isLocalImport reports whether the import path is
   613  // a local import path, like ".", "..", "./foo", or "../foo".
   614  func isLocalImport(path string) bool {
   615  	return path == "." || path == ".." ||
   616  		strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../")
   617  }