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