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 }