cuelang.org/go@v0.10.1/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  	"cmp"
    19  	"fmt"
    20  	"io"
    21  	"io/fs"
    22  	pathpkg "path"
    23  	"path/filepath"
    24  	"slices"
    25  	"strings"
    26  
    27  	"cuelang.org/go/cue/build"
    28  	"cuelang.org/go/cue/errors"
    29  	"cuelang.org/go/cue/token"
    30  	"cuelang.org/go/internal/filetypes"
    31  	"cuelang.org/go/mod/module"
    32  )
    33  
    34  // importPkg returns details about the CUE package named by the import path,
    35  // interpreting local import paths relative to l.cfg.Dir.
    36  // If the path is a local import path naming a package that can be imported
    37  // using a standard import path, the returned package will set p.ImportPath
    38  // to that path.
    39  //
    40  // In the directory and ancestor directories up to including one with a
    41  // cue.mod file, all .cue files are considered part of the package except for:
    42  //
    43  //   - files starting with _ or . (likely editor temporary files)
    44  //   - files with build constraints not satisfied by the context
    45  //
    46  // If an error occurs, importPkg sets the error in the returned instance,
    47  // which then may contain partial information.
    48  //
    49  // pkgName indicates which packages to load. It supports the following
    50  // values:
    51  //
    52  //	""      the default package for the directory, if only one
    53  //	        is present.
    54  //	_       anonymous files (which may be marked with _)
    55  //	*       all packages
    56  func (l *loader) importPkg(pos token.Pos, p *build.Instance) []*build.Instance {
    57  	retErr := func(errs errors.Error) []*build.Instance {
    58  		// XXX: move this loop to ReportError
    59  		for _, err := range errors.Errors(errs) {
    60  			p.ReportError(err)
    61  		}
    62  		return []*build.Instance{p}
    63  	}
    64  
    65  	for _, item := range l.stk {
    66  		if item == p.ImportPath {
    67  			return retErr(&PackageError{Message: errors.NewMessagef("package import cycle not allowed")})
    68  		}
    69  	}
    70  	l.stk.Push(p.ImportPath)
    71  	defer l.stk.Pop()
    72  
    73  	cfg := l.cfg
    74  	ctxt := cfg.fileSystem
    75  
    76  	if p.Err != nil {
    77  		return []*build.Instance{p}
    78  	}
    79  
    80  	fp := newFileProcessor(cfg, p, l.tagger)
    81  
    82  	if p.PkgName == "" {
    83  		if l.cfg.Package == "*" {
    84  			fp.allPackages = true
    85  			p.PkgName = "_"
    86  		} else {
    87  			p.PkgName = l.cfg.Package
    88  		}
    89  	}
    90  	if p.PkgName != "" {
    91  		// If we have an explicit package name, we can ignore other packages.
    92  		fp.ignoreOther = true
    93  	}
    94  
    95  	var dirs [][2]string
    96  	genDir := GenPath(cfg.ModuleRoot)
    97  	if strings.HasPrefix(p.Dir, genDir) {
    98  		dirs = append(dirs, [2]string{genDir, p.Dir})
    99  		// && p.PkgName != "_"
   100  		for _, sub := range []string{"pkg", "usr"} {
   101  			rel, err := filepath.Rel(genDir, p.Dir)
   102  			if err != nil {
   103  				// should not happen
   104  				return retErr(errors.Wrapf(err, token.NoPos, "invalid path"))
   105  			}
   106  			base := filepath.Join(cfg.ModuleRoot, modDir, sub)
   107  			dir := filepath.Join(base, rel)
   108  			dirs = append(dirs, [2]string{base, dir})
   109  		}
   110  	} else {
   111  		dirs = append(dirs, [2]string{cfg.ModuleRoot, p.Dir})
   112  	}
   113  
   114  	found := false
   115  	for _, d := range dirs {
   116  		info, err := ctxt.stat(d[1])
   117  		if err == nil && info.IsDir() {
   118  			found = true
   119  			break
   120  		}
   121  	}
   122  
   123  	if !found {
   124  		return retErr(
   125  			&PackageError{
   126  				Message: errors.NewMessagef("cannot find package %q", p.DisplayPath),
   127  			})
   128  	}
   129  
   130  	// This algorithm assumes that multiple directories within cue.mod/*/
   131  	// have the same module scope and that there are no invalid modules.
   132  	inModule := false // if pkg == "_"
   133  	for _, d := range dirs {
   134  		if l.cfg.findModRoot(d[1]) != "" {
   135  			inModule = true
   136  			break
   137  		}
   138  	}
   139  
   140  	// Walk the parent directories up to the module root to add their files as well,
   141  	// since a package foo/bar/baz inherits from parent packages foo/bar and foo.
   142  	// See https://cuelang.org/docs/concept/modules-packages-instances/#instances.
   143  	for _, d := range dirs {
   144  		dir := filepath.Clean(d[1])
   145  		for {
   146  			sd, ok := l.dirCachedBuildFiles[dir]
   147  			if !ok {
   148  				sd = l.scanDir(dir)
   149  				l.dirCachedBuildFiles[dir] = sd
   150  			}
   151  			if err := sd.err; err != nil {
   152  				if errors.Is(err, fs.ErrNotExist) {
   153  					break
   154  				}
   155  				return retErr(errors.Wrapf(err, token.NoPos, "import failed reading dir %v", dir))
   156  			}
   157  			for _, name := range sd.filenames {
   158  				file, err := filetypes.ParseFileAndType(name, "", filetypes.Input)
   159  				if err != nil {
   160  					p.UnknownFiles = append(p.UnknownFiles, &build.File{
   161  						Filename:      name,
   162  						ExcludeReason: errors.Newf(token.NoPos, "unknown filetype"),
   163  					})
   164  				} else {
   165  					fp.add(dir, file, 0)
   166  				}
   167  			}
   168  			if p.PkgName == "" || !inModule || l.cfg.isModRoot(dir) || dir == d[0] {
   169  				break
   170  			}
   171  
   172  			// From now on we just ignore files that do not belong to the same
   173  			// package.
   174  			fp.ignoreOther = true
   175  
   176  			parent, _ := filepath.Split(dir)
   177  			parent = filepath.Clean(parent)
   178  
   179  			if parent == dir || len(parent) < len(d[0]) {
   180  				break
   181  			}
   182  			dir = parent
   183  		}
   184  	}
   185  
   186  	all := []*build.Instance{}
   187  
   188  	for _, p := range fp.pkgs {
   189  		impPath, err := addImportQualifier(importPath(p.ImportPath), p.PkgName)
   190  		p.ImportPath = string(impPath)
   191  		if err != nil {
   192  			p.ReportError(errors.Promote(err, ""))
   193  		}
   194  
   195  		if len(p.BuildFiles) == 0 &&
   196  			len(p.IgnoredFiles) == 0 &&
   197  			len(p.OrphanedFiles) == 0 &&
   198  			len(p.InvalidFiles) == 0 &&
   199  			len(p.UnknownFiles) == 0 {
   200  			// The package has no files in it. This can happen
   201  			// when the default package added in newFileProcessor
   202  			// doesn't have any associated files.
   203  			continue
   204  		}
   205  		all = append(all, p)
   206  		rewriteFiles(p, cfg.ModuleRoot, false)
   207  		if errs := fp.finalize(p); errs != nil {
   208  			p.ReportError(errs)
   209  			return all
   210  		}
   211  
   212  		l.addFiles(p)
   213  		_ = p.Complete()
   214  	}
   215  	slices.SortFunc(all, func(a, b *build.Instance) int {
   216  		// Instances may share the same directory but have different package names.
   217  		// Sort by directory first, then by package name.
   218  		if c := cmp.Compare(a.Dir, b.Dir); c != 0 {
   219  			return c
   220  		}
   221  
   222  		return cmp.Compare(a.PkgName, b.PkgName)
   223  	})
   224  	return all
   225  }
   226  
   227  func (l *loader) scanDir(dir string) cachedDirFiles {
   228  	files, err := l.cfg.fileSystem.readDir(dir)
   229  	if err != nil {
   230  		return cachedDirFiles{
   231  			err: err,
   232  		}
   233  	}
   234  	filenames := make([]string, 0, len(files))
   235  	for _, f := range files {
   236  		if f.IsDir() {
   237  			continue
   238  		}
   239  		name := f.Name()
   240  		if name == "-" {
   241  			// The name "-" has a special significance to the file types
   242  			// logic, but only when specified directly on the command line.
   243  			// We don't want an actual file named "-" to have special
   244  			// significant, so avoid that by making sure we don't see a naked "-"
   245  			// even when a file named "-" is present in a directory.
   246  			name = "./-"
   247  		}
   248  		filenames = append(filenames, name)
   249  	}
   250  	return cachedDirFiles{
   251  		filenames: filenames,
   252  	}
   253  }
   254  
   255  func setFileSource(cfg *Config, f *build.File) error {
   256  	if f.Source != nil {
   257  		return nil
   258  	}
   259  	fullPath := f.Filename
   260  	if fullPath == "-" {
   261  		b, err := io.ReadAll(cfg.stdin())
   262  		if err != nil {
   263  			return errors.Newf(token.NoPos, "read stdin: %v", err)
   264  		}
   265  		f.Source = b
   266  		return nil
   267  	}
   268  	if !filepath.IsAbs(fullPath) {
   269  		fullPath = filepath.Join(cfg.Dir, fullPath)
   270  		// Ensure that encoding.NewDecoder will work correctly.
   271  		f.Filename = fullPath
   272  	}
   273  	if fi := cfg.fileSystem.getOverlay(fullPath); fi != nil {
   274  		if fi.file != nil {
   275  			f.Source = fi.file
   276  		} else {
   277  			f.Source = fi.contents
   278  		}
   279  	}
   280  	return nil
   281  }
   282  
   283  func (l *loader) loadFunc() build.LoadFunc {
   284  	if l.cfg.SkipImports {
   285  		return nil
   286  	}
   287  	return l._loadFunc
   288  }
   289  
   290  func (l *loader) _loadFunc(pos token.Pos, path string) *build.Instance {
   291  	impPath := importPath(path)
   292  	if isLocalImport(path) {
   293  		return l.cfg.newErrInstance(errors.Newf(pos, "relative import paths not allowed (%q)", path))
   294  	}
   295  
   296  	if isStdlibPackage(path) {
   297  		// It looks like a builtin.
   298  		return nil
   299  	}
   300  
   301  	p := l.newInstance(pos, impPath)
   302  	_ = l.importPkg(pos, p)
   303  	return p
   304  }
   305  
   306  // newRelInstance returns a build instance from the given
   307  // relative import path.
   308  func (l *loader) newRelInstance(pos token.Pos, path, pkgName string) *build.Instance {
   309  	if !isLocalImport(path) {
   310  		panic(fmt.Errorf("non-relative import path %q passed to newRelInstance", path))
   311  	}
   312  
   313  	p := l.cfg.Context.NewInstance(path, l.loadFunc())
   314  	p.PkgName = pkgName
   315  	p.DisplayPath = filepath.ToSlash(path)
   316  	// p.ImportPath = string(dir) // compute unique ID.
   317  	p.Root = l.cfg.ModuleRoot
   318  	p.Module = l.cfg.Module
   319  
   320  	var err errors.Error
   321  	if path != cleanImport(path) {
   322  		err = errors.Append(err, l.errPkgf(nil,
   323  			"non-canonical import path: %q should be %q", path, pathpkg.Clean(path)))
   324  	}
   325  
   326  	dir := filepath.Join(l.cfg.Dir, filepath.FromSlash(path))
   327  	if pkgPath, e := importPathFromAbsDir(l.cfg, dir, path); e != nil {
   328  		// Detect later to keep error messages consistent.
   329  	} else {
   330  		// Add package qualifier if the configuration requires it.
   331  		name := l.cfg.Package
   332  		switch name {
   333  		case "_", "*":
   334  			name = ""
   335  		}
   336  		pkgPath, e := addImportQualifier(pkgPath, name)
   337  		if e != nil {
   338  			// Detect later to keep error messages consistent.
   339  		} else {
   340  			p.ImportPath = string(pkgPath)
   341  		}
   342  	}
   343  
   344  	p.Dir = dir
   345  
   346  	if filepath.IsAbs(path) || strings.HasPrefix(path, "/") {
   347  		err = errors.Append(err, errors.Newf(pos,
   348  			"absolute import path %q not allowed", path))
   349  	}
   350  	if err != nil {
   351  		p.Err = errors.Append(p.Err, err)
   352  		p.Incomplete = true
   353  	}
   354  
   355  	return p
   356  }
   357  
   358  func importPathFromAbsDir(c *Config, absDir string, origPath string) (importPath, error) {
   359  	if c.ModuleRoot == "" {
   360  		return "", fmt.Errorf("cannot determine import path for %q (root undefined)", origPath)
   361  	}
   362  
   363  	dir := filepath.Clean(absDir)
   364  	if !strings.HasPrefix(dir, c.ModuleRoot) {
   365  		return "", fmt.Errorf("cannot determine import path for %q (dir outside of root)", origPath)
   366  	}
   367  
   368  	pkg := filepath.ToSlash(dir[len(c.ModuleRoot):])
   369  	switch {
   370  	case strings.HasPrefix(pkg, "/cue.mod/"):
   371  		pkg = pkg[len("/cue.mod/"):]
   372  		if pkg == "" {
   373  			return "", fmt.Errorf("invalid package %q (root of %s)", origPath, modDir)
   374  		}
   375  
   376  	case c.Module == "":
   377  		return "", fmt.Errorf("cannot determine import path for %q (no module)", origPath)
   378  	default:
   379  		impPath := module.ParseImportPath(c.Module)
   380  		impPath.Path += pkg
   381  		impPath.Qualifier = ""
   382  		pkg = impPath.String()
   383  	}
   384  	return importPath(pkg), nil
   385  }
   386  
   387  func (l *loader) newInstance(pos token.Pos, p importPath) *build.Instance {
   388  	dir, modPath, err := l.absDirFromImportPath(pos, p)
   389  	i := l.cfg.Context.NewInstance(dir, l.loadFunc())
   390  	i.Err = errors.Append(i.Err, err)
   391  	i.Dir = dir
   392  
   393  	parts := module.ParseImportPath(string(p))
   394  	i.PkgName = parts.Qualifier
   395  	if i.PkgName == "" {
   396  		i.Err = errors.Append(i.Err, l.errPkgf([]token.Pos{pos}, "cannot determine package name for %q; set it explicitly with ':'", p))
   397  	} else if i.PkgName == "_" {
   398  		i.Err = errors.Append(i.Err, l.errPkgf([]token.Pos{pos}, "_ is not a valid import path qualifier in %q", p))
   399  	}
   400  	i.DisplayPath = string(p)
   401  	i.ImportPath = string(p)
   402  	i.Root = l.cfg.ModuleRoot
   403  	i.Module = modPath
   404  
   405  	return i
   406  }
   407  
   408  // absDirFromImportPath converts a giving import path to an absolute directory
   409  // and a package name. The root directory must be set.
   410  //
   411  // The returned directory may not exist.
   412  func (l *loader) absDirFromImportPath(pos token.Pos, p importPath) (dir string, modPath string, _ errors.Error) {
   413  	dir, modPath, err := l.absDirFromImportPath1(pos, p)
   414  	if err != nil {
   415  		// Any error trying to determine the package location
   416  		// is a PackageError.
   417  		return "", "", l.errPkgf([]token.Pos{pos}, "%s", err.Error())
   418  	}
   419  	return dir, modPath, nil
   420  }
   421  
   422  func (l *loader) absDirFromImportPath1(pos token.Pos, p importPath) (absDir string, modPath string, err error) {
   423  	if p == "" {
   424  		return "", "", fmt.Errorf("empty import path")
   425  	}
   426  	if l.cfg.ModuleRoot == "" {
   427  		return "", "", fmt.Errorf("cannot import %q (root undefined)", p)
   428  	}
   429  	if isStdlibPackage(string(p)) {
   430  		return "", "", fmt.Errorf("standard library import path %q cannot be imported as a CUE package", p)
   431  	}
   432  	// Extract the package name.
   433  	parts := module.ParseImportPath(string(p))
   434  	unqualified := parts.Unqualified().String()
   435  	if l.cfg.Registry != nil {
   436  		if l.pkgs == nil {
   437  			return "", "", fmt.Errorf("imports are unavailable because there is no cue.mod/module.cue file")
   438  		}
   439  		// TODO predicate registry-aware lookup on module.cue-declared CUE version?
   440  
   441  		// Note: use the canonical form of the import path because
   442  		// that's the form passed to [modpkgload.LoadPackages]
   443  		// and hence it's available by that name via Pkg.
   444  		pkg := l.pkgs.Pkg(parts.Canonical().String())
   445  		// TODO(mvdan): using "unqualified" for the errors below doesn't seem right,
   446  		// should we not be using either the original path or the canonical path?
   447  		// The unqualified import path should only be used for filepath.FromSlash further below.
   448  		if pkg == nil {
   449  			return "", "", fmt.Errorf("no dependency found for package %q", unqualified)
   450  		}
   451  		if err := pkg.Error(); err != nil {
   452  			return "", "", fmt.Errorf("cannot find package %q: %v", unqualified, err)
   453  		}
   454  		if mv := pkg.Mod(); mv.IsLocal() {
   455  			// It's a local package that's present inside one or both of the gen, usr or pkg
   456  			// directories. Even though modpkgload tells us exactly what those directories
   457  			// are, the rest of the cue/load logic expects only a single directory for now,
   458  			// so just use that.
   459  			absDir = filepath.Join(GenPath(l.cfg.ModuleRoot), parts.Path)
   460  		} else {
   461  			locs := pkg.Locations()
   462  			if len(locs) > 1 {
   463  				return "", "", fmt.Errorf("package %q unexpectedly found in multiple locations", unqualified)
   464  			}
   465  			if len(locs) == 0 {
   466  				return "", "", fmt.Errorf("no location found for package %q", unqualified)
   467  			}
   468  			var err error
   469  			absDir, err = absPathForSourceLoc(locs[0])
   470  			if err != nil {
   471  				return "", "", fmt.Errorf("cannot determine source directory for package %q: %v", unqualified, err)
   472  			}
   473  		}
   474  		return absDir, pkg.Mod().Path(), nil
   475  	}
   476  
   477  	// Determine the directory without using the registry.
   478  
   479  	sub := filepath.FromSlash(unqualified)
   480  	switch hasPrefix := strings.HasPrefix(unqualified, l.cfg.Module); {
   481  	case hasPrefix && len(sub) == len(l.cfg.Module):
   482  		modPath = l.cfg.Module
   483  		absDir = l.cfg.ModuleRoot
   484  
   485  	case hasPrefix && unqualified[len(l.cfg.Module)] == '/':
   486  		modPath = l.cfg.Module
   487  		absDir = filepath.Join(l.cfg.ModuleRoot, sub[len(l.cfg.Module)+1:])
   488  
   489  	default:
   490  		modPath = "local"
   491  		absDir = filepath.Join(GenPath(l.cfg.ModuleRoot), sub)
   492  	}
   493  	return absDir, modPath, err
   494  }
   495  
   496  func absPathForSourceLoc(loc module.SourceLoc) (string, error) {
   497  	osfs, ok := loc.FS.(module.OSRootFS)
   498  	if !ok {
   499  		return "", fmt.Errorf("cannot get absolute path for FS of type %T", loc.FS)
   500  	}
   501  	osPath := osfs.OSRoot()
   502  	if osPath == "" {
   503  		return "", fmt.Errorf("cannot get absolute path for FS of type %T", loc.FS)
   504  	}
   505  	return filepath.Join(osPath, loc.Dir), nil
   506  }
   507  
   508  // isStdlibPackage reports whether pkgPath looks like
   509  // an import from the standard library.
   510  func isStdlibPackage(pkgPath string) bool {
   511  	firstElem, _, _ := strings.Cut(pkgPath, "/")
   512  	if firstElem == "" {
   513  		return false // absolute paths like "/foo/bar"
   514  	}
   515  	// Paths like ".foo/bar", "./foo/bar", or "foo.com/bar" are not standard library import paths.
   516  	return strings.IndexByte(firstElem, '.') == -1
   517  }