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