github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/gnovm/pkg/doc/dirs.go (about)

     1  // Mostly copied from go source at tip, commit d922c0a.
     2  //
     3  // Copyright 2015 The Go Authors. All rights reserved.
     4  
     5  package doc
     6  
     7  import (
     8  	"log"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"sort"
    13  	"strings"
    14  
    15  	"github.com/gnolang/gno/gnovm/pkg/gnomod"
    16  )
    17  
    18  // A bfsDir describes a directory holding code by specifying
    19  // the expected import path and the file system directory.
    20  type bfsDir struct {
    21  	importPath string // import path for that dir
    22  	dir        string // file system directory
    23  }
    24  
    25  // dirs is a structure for scanning the directory tree.
    26  // Its Next method returns the next Go source directory it finds.
    27  // Although it can be used to scan the tree multiple times, it
    28  // only walks the tree once, caching the data it finds.
    29  type bfsDirs struct {
    30  	scan   chan bfsDir // Directories generated by walk.
    31  	hist   []bfsDir    // History of reported Dirs.
    32  	offset int         // Counter for Next.
    33  }
    34  
    35  // newDirs begins scanning the given stdlibs directory.
    36  // dirs are "gopath-like" directories, such as @/gnovm/stdlibs, whose path
    37  // relative to the root specify the import path.
    38  // modDirs are user directories, expected to have gno.mod files
    39  func newDirs(dirs []string, modDirs []string) *bfsDirs {
    40  	d := &bfsDirs{
    41  		hist: make([]bfsDir, 0, 256),
    42  		scan: make(chan bfsDir),
    43  	}
    44  
    45  	roots := make([]bfsDir, 0, len(dirs)+len(modDirs))
    46  	for _, dir := range dirs {
    47  		roots = append(roots, bfsDir{
    48  			dir:        dir,
    49  			importPath: "",
    50  		})
    51  	}
    52  
    53  	for _, mdir := range modDirs {
    54  		gm, err := gnomod.ParseGnoMod(filepath.Join(mdir, "gno.mod"))
    55  		if err != nil {
    56  			log.Printf("%v", err)
    57  			continue
    58  		}
    59  		roots = append(roots, bfsDir{
    60  			dir:        mdir,
    61  			importPath: gm.Module.Mod.Path,
    62  		})
    63  		roots = append(roots, getGnoModDirs(gm)...)
    64  	}
    65  
    66  	go d.walk(roots)
    67  	return d
    68  }
    69  
    70  func getGnoModDirs(gm *gnomod.File) []bfsDir {
    71  	// cmd/go makes use of the go list command, we don't have that here.
    72  
    73  	dirs := make([]bfsDir, 0, len(gm.Require))
    74  	for _, r := range gm.Require {
    75  		mv := gm.Resolve(r)
    76  		path := gnomod.PackageDir("", mv)
    77  		if _, err := os.Stat(path); err != nil {
    78  			// only give directories which actually exist and don't give
    79  			// an error when accessing
    80  			if !os.IsNotExist(err) {
    81  				log.Println("open source directories from gno.mod:", err)
    82  			}
    83  			continue
    84  		}
    85  		dirs = append(dirs, bfsDir{
    86  			importPath: mv.Path,
    87  			dir:        path,
    88  		})
    89  	}
    90  
    91  	return dirs
    92  }
    93  
    94  // Reset puts the scan back at the beginning.
    95  func (d *bfsDirs) Reset() {
    96  	d.offset = 0
    97  }
    98  
    99  // Next returns the next directory in the scan. The boolean
   100  // is false when the scan is done.
   101  func (d *bfsDirs) Next() (bfsDir, bool) {
   102  	if d.offset < len(d.hist) {
   103  		dir := d.hist[d.offset]
   104  		d.offset++
   105  		return dir, true
   106  	}
   107  	dir, ok := <-d.scan
   108  	if !ok {
   109  		return bfsDir{}, false
   110  	}
   111  	d.hist = append(d.hist, dir)
   112  	d.offset++
   113  	return dir, ok
   114  }
   115  
   116  // walk walks the trees in the given roots.
   117  func (d *bfsDirs) walk(roots []bfsDir) {
   118  	for _, root := range roots {
   119  		d.bfsWalkRoot(root)
   120  	}
   121  	close(d.scan)
   122  }
   123  
   124  // bfsWalkRoot walks a single directory hierarchy in breadth-first lexical order.
   125  // Each Go source directory it finds is delivered on d.scan.
   126  func (d *bfsDirs) bfsWalkRoot(root bfsDir) {
   127  	root.dir = filepath.Clean(root.dir)
   128  
   129  	// this is the queue of directories to examine in this pass.
   130  	this := []bfsDir{}
   131  	// next is the queue of directories to examine in the next pass.
   132  	next := []bfsDir{root}
   133  
   134  	for len(next) > 0 {
   135  		this, next = next, this[:0]
   136  		for _, dir := range this {
   137  			fd, err := os.Open(dir.dir)
   138  			if err != nil {
   139  				log.Print(err)
   140  				continue
   141  			}
   142  
   143  			// read dir entries.
   144  			entries, err := fd.ReadDir(0)
   145  			fd.Close()
   146  			if err != nil {
   147  				log.Print(err)
   148  				continue
   149  			}
   150  
   151  			// stop at module boundaries
   152  			if dir.dir != root.dir && containsGnoMod(entries) {
   153  				continue
   154  			}
   155  
   156  			hasGnoFiles := false
   157  			for _, entry := range entries {
   158  				name := entry.Name()
   159  				// For plain files, remember if this directory contains any .gno
   160  				// source files, but ignore them otherwise.
   161  				if !entry.IsDir() {
   162  					if !hasGnoFiles && strings.HasSuffix(name, ".gno") {
   163  						hasGnoFiles = true
   164  					}
   165  					continue
   166  				}
   167  				// Entry is a directory.
   168  
   169  				// Ignore same directories ignored by the go tool.
   170  				if name[0] == '.' || name[0] == '_' || name == "testdata" {
   171  					continue
   172  				}
   173  				// Remember this (fully qualified) directory for the next pass.
   174  				next = append(next, bfsDir{
   175  					dir:        filepath.Join(dir.dir, name),
   176  					importPath: path.Join(dir.importPath, name),
   177  				})
   178  			}
   179  			if hasGnoFiles {
   180  				// It's a candidate.
   181  				d.scan <- dir
   182  			}
   183  		}
   184  	}
   185  }
   186  
   187  func containsGnoMod(entries []os.DirEntry) bool {
   188  	for _, entry := range entries {
   189  		if entry.Name() == "gno.mod" && !entry.IsDir() {
   190  			return true
   191  		}
   192  	}
   193  	return false
   194  }
   195  
   196  // findPackage finds a package iterating over d where the import path has
   197  // name as a suffix (which may be a package name or a fully-qualified path).
   198  // returns a list of possible directories. If a directory's import path matched
   199  // exactly, it will be returned as first.
   200  func (d *bfsDirs) findPackage(name string) []bfsDir {
   201  	d.Reset()
   202  	candidates := make([]bfsDir, 0, 4)
   203  	for dir, ok := d.Next(); ok; dir, ok = d.Next() {
   204  		// want either exact matches or suffixes
   205  		if dir.importPath == name || strings.HasSuffix(dir.importPath, "/"+name) {
   206  			candidates = append(candidates, dir)
   207  		}
   208  	}
   209  	sort.Slice(candidates, func(i, j int) bool {
   210  		// prefer shorter paths -- if we have an exact match it will be of the
   211  		// shortest possible pkg path.
   212  		ci := strings.Count(candidates[i].importPath, "/")
   213  		cj := strings.Count(candidates[j].importPath, "/")
   214  		if ci != cj {
   215  			return ci < cj
   216  		}
   217  		// use alphabetical ordering otherwise.
   218  		return candidates[i].importPath < candidates[j].importPath
   219  	})
   220  	return candidates
   221  }
   222  
   223  // findDir determines if the given absdir is present in the Dirs.
   224  // If not, the nil slice is returned. It returns always at most one dir.
   225  func (d *bfsDirs) findDir(absdir string) []bfsDir {
   226  	d.Reset()
   227  	for dir, ok := d.Next(); ok; dir, ok = d.Next() {
   228  		if dir.dir == absdir {
   229  			return []bfsDir{dir}
   230  		}
   231  	}
   232  	return nil
   233  }