Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cmd/gno): add support for gno.mod projects in gno doc #829

Merged
merged 15 commits into from
Jun 19, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion gnovm/cmd/gno/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ package main

import (
"context"
"errors"
"flag"
"fmt"
"os"
"path/filepath"

"github.com/gnolang/gno/gnovm/pkg/doc"
"github.com/gnolang/gno/gnovm/pkg/gnomod"
"github.com/gnolang/gno/tm2/pkg/commands"
)

Expand Down Expand Up @@ -74,8 +78,24 @@ func execDoc(cfg *docCfg, args []string, io *commands.IO) error {
if cfg.rootDir == "" {
cfg.rootDir = guessRootDir()
}

wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("could not determine working directory: %w", err)
}

rd, err := gnomod.FindRootDir(wd)
if err != nil && !errors.Is(err, gnomod.ErrGnoModNotFound) {
return fmt.Errorf("error determining root gno.mod file: %w", err)
}
var modDirs []string
if rd != "" {
modDirs = append(modDirs, rd)
}
thehowl marked this conversation as resolved.
Show resolved Hide resolved

// select dirs from which to gather directories
dirs := []string{filepath.Join(cfg.rootDir, "gnovm/stdlibs"), filepath.Join(cfg.rootDir, "examples")}
res, err := doc.ResolveDocumentable(dirs, args, cfg.unexported)
res, err := doc.ResolveDocumentable(dirs, modDirs, args, cfg.unexported)
if res == nil {
return err
}
Expand Down
48 changes: 24 additions & 24 deletions gnovm/docs/go-gno-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,27 +319,27 @@ Legend: full, partial, missing, TBD.

## Tooling (`gno` binary)

| go command | gno command | comment |
|-------------------|------------------|-----------------------------------------------|
| go bug | | see https://github.com/gnolang/gno/issues/733 |
| go build | gno build | same intention, limited compatibility |
| go clean | gno clean | same intention, limited compatibility |
| go doc | | see https://github.com/gnolang/gno/pull/610 |
| go env | | |
| go fix | | |
| go fmt | | |
| go generate | | |
| go get | | |
| go help | | |
| go install | | |
| go list | | |
| go mod | | |
| + go mod download | gno mod download | same behavior |
| | gno precompile | |
| go work | | |
| | gno repl | |
| go run | gno run | |
| go test | gno test | limited compatibility |
| go tool | | |
| go version | | |
| go vet | | |
| go command | gno command | comment |
|-------------------|------------------|-----------------------------------------------------------------------|
| go bug | | see https://github.com/gnolang/gno/issues/733 |
| go build | gno build | same intention, limited compatibility |
| go clean | gno clean | same intention, limited compatibility |
| go doc | gno doc | limited compatibility; see https://github.com/gnolang/gno/issues/522 |
| go env | | |
| go fix | | |
| go fmt | | |
| go generate | | |
| go get | | |
| go help | | |
| go install | | |
| go list | | |
| go mod | | |
| + go mod download | gno mod download | same behavior |
| | gno precompile | |
| go work | | |
| | gno repl | |
| go run | gno run | |
| go test | gno test | limited compatibility |
| go tool | | |
| go version | | |
| go vet | | |
131 changes: 111 additions & 20 deletions gnovm/pkg/doc/dirs.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ package doc
import (
"log"
"os"
"path"
"path/filepath"
"sort"
"strings"

"github.com/gnolang/gno/gnovm/pkg/gnomod"
)

// A bfsDir describes a directory holding code by specifying
Expand All @@ -30,15 +33,85 @@ type bfsDirs struct {
}

// newDirs begins scanning the given stdlibs directory.
func newDirs(dirs ...string) *bfsDirs {
// dirs are "gopath-like" directories, such as @/gnovm/stdlibs and @/examples.
// modDirs are user directories, expected to have gno.mod files
func newDirs(dirs []string, modDirs []string) *bfsDirs {
d := &bfsDirs{
hist: make([]bfsDir, 0, 256),
scan: make(chan bfsDir),
}
go d.walk(dirs)

roots := make([]bfsDir, 0, len(dirs)+len(modDirs))
for _, dir := range dirs {
roots = append(roots, bfsDir{
dir: dir,
importPath: "",
})
}

for _, mdir := range modDirs {
gm, exists := tryParseGnoMod(filepath.Join(mdir, "gno.mod"))
if gm == nil && exists {
// gno.mod could not be parsed but exists.
// as this would lead us to not having correct import paths (as we can't
// parse the module name from gno.mod), skip this directory; the user has
// received a console warning regardless from tryParseGnoMod.
continue
}
roots = append(roots, bfsDir{
dir: mdir,
importPath: gm.Module.Mod.Path,
})
roots = append(roots, getGnoModDirs(gm)...)
}

go d.walk(roots)
return d
}

// tries to parse gno mod file.
// second return parameter is whether gno.mod exists.
func tryParseGnoMod(fname string) (*gnomod.File, bool) {
file, err := os.Stat(fname)
// early exit for errors and non-file fname
if err != nil || file.IsDir() {
if err != nil && !os.IsNotExist(err) {
log.Printf("could not read go.mod file at %q: %v", fname, err)
}
return nil, !os.IsNotExist(err)
}

b, err := os.ReadFile(fname)
if err != nil {
log.Printf("could not read go.mod file at %q: %v", fname, err)
return nil, true
}
gm, err := gnomod.Parse(fname, b)
if err == nil {
err = gm.Validate()
}
if err != nil {
log.Printf("could not parse go.mod file at %q: %v", fname, err)
}
return gm, true
}

func getGnoModDirs(gm *gnomod.File) []bfsDir {
// cmd/go makes use of the go list command, we don't have that here.

dirs := make([]bfsDir, 0, len(gm.Require))
for _, r := range gm.Require {
mv := gm.Resolve(r)
path := gnomod.PackageDir("", mv)
dirs = append(dirs, bfsDir{
importPath: mv.Path,
dir: path,
})
}

return dirs
}

// Reset puts the scan back at the beginning.
func (d *bfsDirs) Reset() {
d.offset = 0
Expand All @@ -62,7 +135,7 @@ func (d *bfsDirs) Next() (bfsDir, bool) {
}

// walk walks the trees in the given roots.
func (d *bfsDirs) walk(roots []string) {
func (d *bfsDirs) walk(roots []bfsDir) {
for _, root := range roots {
d.bfsWalkRoot(root)
}
Expand All @@ -71,28 +144,36 @@ func (d *bfsDirs) walk(roots []string) {

// bfsWalkRoot walks a single directory hierarchy in breadth-first lexical order.
// Each Go source directory it finds is delivered on d.scan.
func (d *bfsDirs) bfsWalkRoot(root string) {
root = filepath.Clean(root)
func (d *bfsDirs) bfsWalkRoot(root bfsDir) {
root.dir = filepath.Clean(root.dir)

// this is the queue of directories to examine in this pass.
this := []string{}
this := []bfsDir{}
// next is the queue of directories to examine in the next pass.
next := []string{root}
next := []bfsDir{root}

for len(next) > 0 {
this, next = next, this[:0]
for _, dir := range this {
fd, err := os.Open(dir)
fd, err := os.Open(dir.dir)
if err != nil {
log.Print(err)
continue
}
entries, err := fd.Readdir(0)

// read dir entries.
entries, err := fd.ReadDir(0)
fd.Close()
if err != nil {
log.Print(err)
continue
}

// stop at module boundaries
if dir.dir != root.dir && containsGnoMod(entries) {
continue
}

hasGnoFiles := false
for _, entry := range entries {
name := entry.Name()
Expand All @@ -111,20 +192,28 @@ func (d *bfsDirs) bfsWalkRoot(root string) {
continue
}
// Remember this (fully qualified) directory for the next pass.
next = append(next, filepath.Join(dir, name))
next = append(next, bfsDir{
dir: filepath.Join(dir.dir, name),
importPath: path.Join(dir.importPath, name),
})
}
if hasGnoFiles {
// It's a candidate.
var importPath string
if len(dir) > len(root) {
importPath = filepath.ToSlash(dir[len(root)+1:])
}
d.scan <- bfsDir{importPath, dir}
d.scan <- dir
}
}
}
}

func containsGnoMod(entries []os.DirEntry) bool {
for _, entry := range entries {
if entry.Name() == "gno.mod" && !entry.IsDir() {
return true
}
}
return false
}

// findPackage finds a package iterating over d where the import path has
// name as a suffix (which may be a package name or a fully-qualified path).
// returns a list of possible directories. If a directory's import path matched
Expand All @@ -139,12 +228,14 @@ func (d *bfsDirs) findPackage(name string) []bfsDir {
}
}
sort.Slice(candidates, func(i, j int) bool {
// prefer exact matches with name
if candidates[i].importPath == name {
return true
} else if candidates[j].importPath == name {
return false
// prefer shorter paths -- if we have an exact match it will be of the
// shortest possible pkg path.
ci := strings.Count(candidates[i].importPath, "/")
cj := strings.Count(candidates[j].importPath, "/")
if ci != cj {
return ci < cj
}
// use alphabetical ordering otherwise.
return candidates[i].importPath < candidates[j].importPath
})
return candidates
Expand Down
54 changes: 39 additions & 15 deletions gnovm/pkg/doc/dirs_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package doc

import (
"os"
"path/filepath"
"strings"
"testing"
Expand All @@ -11,27 +12,50 @@ import (

func tNewDirs(t *testing.T) (string, *bfsDirs) {
t.Helper()
p, err := filepath.Abs("./testdata/dirs")

wd, err := os.Getwd()
require.NoError(t, err)
return p, newDirs(p)

// modify GNO_HOME to testdata/dirsdep -- this allows us to test
// dependency lookup by dirs.
old, ex := os.LookupEnv("GNO_HOME")
os.Setenv("GNO_HOME", filepath.Join(wd, "testdata/dirsdep"))
t.Cleanup(func() {
if ex {
os.Setenv("GNO_HOME", old)
} else {
os.Unsetenv("GNO_HOME")
}
})

return filepath.Join(wd, "testdata"),
newDirs([]string{filepath.Join(wd, "testdata/dirs")}, []string{filepath.Join(wd, "testdata/dirsmod")})
}

func TestDirs_findPackage(t *testing.T) {
abs, d := tNewDirs(t)
thehowl marked this conversation as resolved.
Show resolved Hide resolved
td, d := tNewDirs(t)
tt := []struct {
name string
res []bfsDir
}{
{"rand", []bfsDir{
{importPath: "rand", dir: filepath.Join(abs, "rand")},
{importPath: "crypto/rand", dir: filepath.Join(abs, "crypto/rand")},
{importPath: "math/rand", dir: filepath.Join(abs, "math/rand")},
{importPath: "rand", dir: filepath.Join(td, "dirs/rand")},
{importPath: "crypto/rand", dir: filepath.Join(td, "dirs/crypto/rand")},
{importPath: "math/rand", dir: filepath.Join(td, "dirs/math/rand")},
{importPath: "dirs.mod/prefix/math/rand", dir: filepath.Join(td, "dirsmod/math/rand")},
}},
{"crypto/rand", []bfsDir{
{importPath: "crypto/rand", dir: filepath.Join(abs, "crypto/rand")},
{importPath: "crypto/rand", dir: filepath.Join(td, "dirs/crypto/rand")},
}},
{"dep", []bfsDir{
{importPath: "dirs.mod/dep", dir: filepath.Join(td, "dirsdep/pkg/mod/dirs.mod/dep")},
}},
{"alpha", []bfsDir{
{importPath: "dirs.mod/dep/alpha", dir: filepath.Join(td, "dirsdep/pkg/mod/dirs.mod/dep/alpha")},
// no testdir/module/alpha as it is inside a module
}},
{"math", []bfsDir{
{importPath: "math", dir: filepath.Join(abs, "math")},
{importPath: "math", dir: filepath.Join(td, "dirs/math")},
}},
{"ath", []bfsDir{}},
{"/math", []bfsDir{}},
Expand All @@ -47,21 +71,21 @@ func TestDirs_findPackage(t *testing.T) {
}

func TestDirs_findDir(t *testing.T) {
abs, d := tNewDirs(t)
td, d := tNewDirs(t)
tt := []struct {
name string
in string
res []bfsDir
}{
{"rand", filepath.Join(abs, "rand"), []bfsDir{
{importPath: "rand", dir: filepath.Join(abs, "rand")},
{"rand", filepath.Join(td, "dirs/rand"), []bfsDir{
{importPath: "rand", dir: filepath.Join(td, "dirs/rand")},
}},
{"crypto/rand", filepath.Join(abs, "crypto/rand"), []bfsDir{
{importPath: "crypto/rand", dir: filepath.Join(abs, "crypto/rand")},
{"crypto/rand", filepath.Join(td, "dirs/crypto/rand"), []bfsDir{
{importPath: "crypto/rand", dir: filepath.Join(td, "dirs/crypto/rand")},
}},
// ignored (dir name testdata), so should not return anything.
{"crypto/testdata/rand", filepath.Join(abs, "crypto/testdata/rand"), nil},
{"xx", filepath.Join(abs, "xx"), nil},
{"crypto/testdata/rand", filepath.Join(td, "dirs/crypto/testdata/rand"), nil},
{"xx", filepath.Join(td, "dirs/xx"), nil},
{"xx2", "/xx2", nil},
}
for _, tc := range tt {
Expand Down
Loading