cuelang.org/go@v0.10.1/internal/mod/modimports/modimports.go (about)

     1  package modimports
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/fs"
     7  	"path"
     8  	"sort"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"cuelang.org/go/cue/ast"
    13  	"cuelang.org/go/cue/parser"
    14  	"cuelang.org/go/internal/cueimports"
    15  	"cuelang.org/go/mod/module"
    16  )
    17  
    18  type ModuleFile struct {
    19  	// FilePath holds the path of the module file
    20  	// relative to the root of the fs. This will be
    21  	// valid even if there's an associated error.
    22  	//
    23  	// If there's an error, it might not a be CUE file.
    24  	FilePath string
    25  
    26  	// Syntax includes only the portion of the file up to and including
    27  	// the imports. It will be nil if there was an error reading the file.
    28  	Syntax *ast.File
    29  }
    30  
    31  // AllImports returns a sorted list of all the package paths
    32  // imported by the module files produced by modFilesIter
    33  // in canonical form.
    34  func AllImports(modFilesIter func(func(ModuleFile, error) bool)) (_ []string, retErr error) {
    35  	pkgPaths := make(map[string]bool)
    36  	modFilesIter(func(mf ModuleFile, err error) bool {
    37  		if err != nil {
    38  			retErr = fmt.Errorf("cannot read %q: %v", mf.FilePath, err)
    39  			return false
    40  		}
    41  		// TODO look at build tags and omit files with "ignore" tags.
    42  		for _, imp := range mf.Syntax.Imports {
    43  			pkgPath, err := strconv.Unquote(imp.Path.Value)
    44  			if err != nil {
    45  				// TODO location formatting
    46  				retErr = fmt.Errorf("invalid import path %q in %s", imp.Path.Value, mf.FilePath)
    47  				return false
    48  			}
    49  			// Canonicalize the path.
    50  			pkgPath = module.ParseImportPath(pkgPath).Canonical().String()
    51  			pkgPaths[pkgPath] = true
    52  		}
    53  		return true
    54  	})
    55  	if retErr != nil {
    56  		return nil, retErr
    57  	}
    58  	// TODO use maps.Keys when we can.
    59  	pkgPathSlice := make([]string, 0, len(pkgPaths))
    60  	for p := range pkgPaths {
    61  		pkgPathSlice = append(pkgPathSlice, p)
    62  	}
    63  	sort.Strings(pkgPathSlice)
    64  	return pkgPathSlice, nil
    65  }
    66  
    67  // PackageFiles returns an iterator that produces all the CUE files
    68  // inside the package with the given name at the given location.
    69  // If pkgQualifier is "*", files from all packages in the directory will be produced.
    70  //
    71  // TODO(mvdan): this should now be called InstanceFiles, to follow the naming from
    72  // https://cuelang.org/docs/concept/modules-packages-instances/#instances.
    73  func PackageFiles(fsys fs.FS, dir string, pkgQualifier string) func(func(ModuleFile, error) bool) {
    74  	return func(yield func(ModuleFile, error) bool) {
    75  		// Start at the target directory, but also include package files
    76  		// from packages with the same name(s) in parent directories.
    77  		// Stop the iteration when we find a cue.mod entry, signifying
    78  		// the module root. If the location is inside a `cue.mod` directory
    79  		// already, do not look at parent directories - this mimics historic
    80  		// behavior.
    81  		selectPackage := func(pkg string) bool {
    82  			if pkgQualifier == "*" {
    83  				return true
    84  			}
    85  			return pkg == pkgQualifier
    86  		}
    87  		inCUEMod := false
    88  		if before, after, ok := strings.Cut(dir, "cue.mod"); ok {
    89  			// We're underneath a cue.mod directory if some parent
    90  			// element is cue.mod.
    91  			inCUEMod =
    92  				(before == "" || strings.HasSuffix(before, "/")) &&
    93  					(after == "" || strings.HasPrefix(after, "/"))
    94  		}
    95  		var matchedPackages map[string]bool
    96  		for {
    97  			entries, err := fs.ReadDir(fsys, dir)
    98  			if err != nil {
    99  				yield(ModuleFile{
   100  					FilePath: dir,
   101  				}, err)
   102  				return
   103  			}
   104  			inModRoot := false
   105  			for _, e := range entries {
   106  				if e.Name() == "cue.mod" {
   107  					inModRoot = true
   108  				}
   109  				pkgName, cont := yieldPackageFile(fsys, path.Join(dir, e.Name()), selectPackage, yield)
   110  				if !cont {
   111  					return
   112  				}
   113  				if pkgName != "" {
   114  					if matchedPackages == nil {
   115  						matchedPackages = make(map[string]bool)
   116  					}
   117  					matchedPackages[pkgName] = true
   118  				}
   119  			}
   120  			if inModRoot || inCUEMod {
   121  				// We're at the module root or we're inside the cue.mod
   122  				// directory. Don't go any further up the hierarchy.
   123  				return
   124  			}
   125  			if matchedPackages == nil {
   126  				// No packages possible in parent directories if there are
   127  				// no matching package files in the package directory itself.
   128  				return
   129  			}
   130  			selectPackage = func(pkgName string) bool {
   131  				return matchedPackages[pkgName]
   132  			}
   133  			parent := path.Dir(dir)
   134  			if len(parent) >= len(dir) {
   135  				// No more parent directories.
   136  				return
   137  			}
   138  			dir = parent
   139  		}
   140  	}
   141  }
   142  
   143  // AllModuleFiles returns an iterator that produces all the CUE files inside the
   144  // module at the given root.
   145  //
   146  // The caller may assume that files from the same package are always adjacent.
   147  func AllModuleFiles(fsys fs.FS, root string) func(func(ModuleFile, error) bool) {
   148  	return func(yield func(ModuleFile, error) bool) {
   149  		yieldAllModFiles(fsys, root, true, yield)
   150  	}
   151  }
   152  
   153  // yieldAllModFiles implements AllModuleFiles by recursing into directories.
   154  //
   155  // Note that we avoid [fs.WalkDir]; it yields directory entries in lexical order,
   156  // so we would walk `foo/bar.cue` before walking `foo/cue.mod/` and realizing
   157  // that `foo/` is a nested module that we should be ignoring entirely.
   158  // That could be avoided via extra `fs.Stat` calls, but those are extra fs calls.
   159  // Using [fs.ReadDir] avoids this issue entirely, as we can loop twice.
   160  func yieldAllModFiles(fsys fs.FS, fpath string, topDir bool, yield func(ModuleFile, error) bool) bool {
   161  	entries, err := fs.ReadDir(fsys, fpath)
   162  	if err != nil {
   163  		if !yield(ModuleFile{
   164  			FilePath: fpath,
   165  		}, err) {
   166  			return false
   167  		}
   168  	}
   169  	// Skip nested submodules entirely.
   170  	if !topDir {
   171  		for _, entry := range entries {
   172  			if entry.Name() == "cue.mod" {
   173  				return true
   174  			}
   175  		}
   176  	}
   177  	// Generate all entries for the package before moving onto packages
   178  	// in subdirectories.
   179  	for _, entry := range entries {
   180  		if entry.IsDir() {
   181  			continue
   182  		}
   183  		fpath := path.Join(fpath, entry.Name())
   184  		if _, ok := yieldPackageFile(fsys, fpath, func(string) bool { return true }, yield); !ok {
   185  			return false
   186  		}
   187  	}
   188  
   189  	for _, entry := range entries {
   190  		name := entry.Name()
   191  		if !entry.IsDir() {
   192  			continue
   193  		}
   194  		if name == "cue.mod" || strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") {
   195  			continue
   196  		}
   197  		fpath := path.Join(fpath, name)
   198  		if !yieldAllModFiles(fsys, fpath, false, yield) {
   199  			return false
   200  		}
   201  	}
   202  	return true
   203  }
   204  
   205  // yieldPackageFile invokes yield with the contents of the package file
   206  // at the given path if selectPackage returns true for the file's
   207  // package name.
   208  //
   209  // It returns the yielded package name (if any) and reports whether
   210  // the iteration should continue.
   211  func yieldPackageFile(fsys fs.FS, fpath string, selectPackage func(pkgName string) bool, yield func(ModuleFile, error) bool) (pkgName string, cont bool) {
   212  	if !strings.HasSuffix(fpath, ".cue") {
   213  		return "", true
   214  	}
   215  	pf := ModuleFile{
   216  		FilePath: fpath,
   217  	}
   218  	var syntax *ast.File
   219  	var err error
   220  	if cueFS, ok := fsys.(module.ReadCUEFS); ok {
   221  		// The FS implementation supports reading CUE syntax directly.
   222  		// A notable FS implementation that does this is the one
   223  		// provided by cue/load, allowing that package to cache
   224  		// the parsed CUE.
   225  		syntax, err = cueFS.ReadCUEFile(fpath)
   226  		if err != nil && !errors.Is(err, errors.ErrUnsupported) {
   227  			return "", yield(pf, err)
   228  		}
   229  	}
   230  	if syntax == nil {
   231  		// Either the FS doesn't implement [module.ReadCUEFS]
   232  		// or the ReadCUEFile method returned ErrUnsupported,
   233  		// so we need to acquire the syntax ourselves.
   234  
   235  		f, err := fsys.Open(fpath)
   236  		if err != nil {
   237  			return "", yield(pf, err)
   238  		}
   239  		defer f.Close()
   240  
   241  		// Note that we use cueimports.Read before parser.ParseFile as cue/parser
   242  		// will always consume the whole input reader, which is often wasteful.
   243  		//
   244  		// TODO(mvdan): the need for cueimports.Read can go once cue/parser can work
   245  		// on a reader in a streaming manner.
   246  		data, err := cueimports.Read(f)
   247  		if err != nil {
   248  			return "", yield(pf, err)
   249  		}
   250  		// Add a leading "./" so that a parse error filename is consistent
   251  		// with the other error filenames created elsewhere in the codebase.
   252  		syntax, err = parser.ParseFile("./"+fpath, data, parser.ImportsOnly)
   253  		if err != nil {
   254  			return "", yield(pf, err)
   255  		}
   256  	}
   257  
   258  	if !selectPackage(syntax.PackageName()) {
   259  		return "", true
   260  	}
   261  	pf.Syntax = syntax
   262  	return syntax.PackageName(), yield(pf, nil)
   263  }