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 }