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

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