Skip to content

Commit

Permalink
feat(cmd/gno): initial documentation command for stdlibs and example …
Browse files Browse the repository at this point in the history
…packages (#610)

* feat(gnodev): add preliminary doc command, refactor common flags verbose and root-dir

* feat(gnodev): initial documentation command for stdlibs packages

* chore: remove accidental log line

* chore: linter

* feat(commands/doc): add support for examples directory

* chore: prefer writing to stdout directly instead of using a buffer

* fix(doc): improve arg parsing, more tests

* tests: add tests for Document

* chore: fix typo

* update to new dir structure

* fmt

* Update gnovm/cmd/gno/doc.go

Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com>

* typo

* Revert "feat(gnodev): add preliminary doc command, refactor common flags verbose and root-dir"

This reverts commit 243d24c.

* change documentable iface

* add doc test

* address changes requested from code review

* fix typo

* code review changes

* unexport dirs

* some typos, some error logging

* better and more consistent error handling

* use errors.Is for comparison

* empty commit to trigger github workflow

---------

Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com>
  • Loading branch information
thehowl and moul authored May 12, 2023
1 parent f2620e8 commit 0985fb1
Show file tree
Hide file tree
Showing 20 changed files with 2,217 additions and 1 deletion.
94 changes: 94 additions & 0 deletions gnovm/cmd/gno/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package main

import (
"context"
"flag"
"path/filepath"

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

type docCfg struct {
all bool
src bool
unexported bool
short bool
rootDir string
}

func newDocCmd(io *commands.IO) *commands.Command {
c := &docCfg{}
return commands.NewCommand(
commands.Metadata{
Name: "doc",
ShortUsage: "doc [flags] <pkgsym>",
ShortHelp: "get documentation for the specified package or symbol (type, function, method, or variable/constant).",
},
c,
func(_ context.Context, args []string) error {
return execDoc(c, args, io)
},
)
}

func (c *docCfg) RegisterFlags(fs *flag.FlagSet) {
fs.BoolVar(
&c.all,
"all",
false,
"show documentation for all symbols in package",
)

fs.BoolVar(
&c.src,
"src",
false,
"show source code for symbols",
)

fs.BoolVar(
&c.unexported,
"u",
false,
"show unexported symbols as well as exported",
)

fs.BoolVar(
&c.short,
"short",
false,
"show a one line representation for each symbol",
)

fs.StringVar(
&c.rootDir,
"root-dir",
"",
"clone location of github.com/gnolang/gno (gnodev tries to guess it)",
)
}

func execDoc(cfg *docCfg, args []string, io *commands.IO) error {
// guess opts.RootDir
if cfg.rootDir == "" {
cfg.rootDir = guessRootDir()
}
dirs := []string{filepath.Join(cfg.rootDir, "gnovm/stdlibs"), filepath.Join(cfg.rootDir, "examples")}
res, err := doc.ResolveDocumentable(dirs, args, cfg.unexported)
if res == nil {
return err
}
if err != nil {
io.Printfln("warning: error parsing some candidate packages:\n%v", err)
}
return res.WriteDocumentation(
io.Out,
&doc.WriteDocumentationOptions{
ShowAll: cfg.all,
Source: cfg.src,
Unexported: cfg.unexported,
Short: false,
},
)
}
29 changes: 29 additions & 0 deletions gnovm/cmd/gno/doc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package main

import "testing"

func TestGnoDoc(t *testing.T) {
tc := []testMainCase{
{
args: []string{"doc", "io.Writer"},
stdoutShouldContain: "Writer is the interface that wraps",
},
{
args: []string{"doc", "avl"},
stdoutShouldContain: "func NewNode",
},
{
args: []string{"doc", "-u", "avl.Node"},
stdoutShouldContain: "node *Node",
},
{
args: []string{"doc", "dkfdkfkdfjkdfj"},
errShouldContain: "package not found",
},
{
args: []string{"doc", "There.Are.Too.Many.Dots"},
errShouldContain: "invalid arguments",
},
}
testMainCaseRun(t, tc)
}
2 changes: 1 addition & 1 deletion gnovm/cmd/gno/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func newGnodevCmd(io *commands.IO) *commands.Command {
newTestCmd(io),
newModCmd(io),
newReplCmd(),
newDocCmd(io),
// fmt -- gofmt
// clean
// graph
Expand All @@ -43,7 +44,6 @@ func newGnodevCmd(io *commands.IO) *commands.Command {
// render -- call render()?
// publish/release
// generate
// doc -- godoc
// "vm" -- starts an in-memory chain that can be interacted with?
// bug -- start a bug report
// version -- show gnodev, golang versions
Expand Down
163 changes: 163 additions & 0 deletions gnovm/pkg/doc/dirs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Mostly copied from go source at tip, commit d922c0a.
//
// Copyright 2015 The Go Authors. All rights reserved.

package doc

import (
"log"
"os"
"path/filepath"
"sort"
"strings"
)

// A bfsDir describes a directory holding code by specifying
// the expected import path and the file system directory.
type bfsDir struct {
importPath string // import path for that dir
dir string // file system directory
}

// dirs is a structure for scanning the directory tree.
// Its Next method returns the next Go source directory it finds.
// Although it can be used to scan the tree multiple times, it
// only walks the tree once, caching the data it finds.
type bfsDirs struct {
scan chan bfsDir // Directories generated by walk.
hist []bfsDir // History of reported Dirs.
offset int // Counter for Next.
}

// newDirs begins scanning the given stdlibs directory.
func newDirs(dirs ...string) *bfsDirs {
d := &bfsDirs{
hist: make([]bfsDir, 0, 256),
scan: make(chan bfsDir),
}
go d.walk(dirs)
return d
}

// Reset puts the scan back at the beginning.
func (d *bfsDirs) Reset() {
d.offset = 0
}

// Next returns the next directory in the scan. The boolean
// is false when the scan is done.
func (d *bfsDirs) Next() (bfsDir, bool) {
if d.offset < len(d.hist) {
dir := d.hist[d.offset]
d.offset++
return dir, true
}
dir, ok := <-d.scan
if !ok {
return bfsDir{}, false
}
d.hist = append(d.hist, dir)
d.offset++
return dir, ok
}

// walk walks the trees in the given roots.
func (d *bfsDirs) walk(roots []string) {
for _, root := range roots {
d.bfsWalkRoot(root)
}
close(d.scan)
}

// 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)

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

for len(next) > 0 {
this, next = next, this[:0]
for _, dir := range this {
fd, err := os.Open(dir)
if err != nil {
log.Print(err)
continue
}
entries, err := fd.Readdir(0)
fd.Close()
if err != nil {
log.Print(err)
continue
}
hasGnoFiles := false
for _, entry := range entries {
name := entry.Name()
// For plain files, remember if this directory contains any .gno
// source files, but ignore them otherwise.
if !entry.IsDir() {
if !hasGnoFiles && strings.HasSuffix(name, ".gno") {
hasGnoFiles = true
}
continue
}
// Entry is a directory.

// Ignore same directories ignored by the go tool.
if name[0] == '.' || name[0] == '_' || name == "testdata" {
continue
}
// Remember this (fully qualified) directory for the next pass.
next = append(next, filepath.Join(dir, 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}
}
}
}
}

// 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
// exactly, it will be returned as first.
func (d *bfsDirs) findPackage(name string) []bfsDir {
d.Reset()
candidates := make([]bfsDir, 0, 4)
for dir, ok := d.Next(); ok; dir, ok = d.Next() {
// want either exact matches or suffixes
if dir.importPath == name || strings.HasSuffix(dir.importPath, "/"+name) {
candidates = append(candidates, dir)
}
}
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
}
return candidates[i].importPath < candidates[j].importPath
})
return candidates
}

// findDir determines if the given absdir is present in the Dirs.
// If not, the nil slice is returned. It returns always at most one dir.
func (d *bfsDirs) findDir(absdir string) []bfsDir {
d.Reset()
for dir, ok := d.Next(); ok; dir, ok = d.Next() {
if dir.dir == absdir {
return []bfsDir{dir}
}
}
return nil
}
74 changes: 74 additions & 0 deletions gnovm/pkg/doc/dirs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package doc

import (
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

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

func TestDirs_findPackage(t *testing.T) {
abs, 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")},
}},
{"crypto/rand", []bfsDir{
{importPath: "crypto/rand", dir: filepath.Join(abs, "crypto/rand")},
}},
{"math", []bfsDir{
{importPath: "math", dir: filepath.Join(abs, "math")},
}},
{"ath", []bfsDir{}},
{"/math", []bfsDir{}},
{"", []bfsDir{}},
}
for _, tc := range tt {
tc := tc
t.Run("name_"+strings.Replace(tc.name, "/", "_", -1), func(t *testing.T) {
res := d.findPackage(tc.name)
assert.Equal(t, tc.res, res, "dirs returned should be the equal")
})
}
}

func TestDirs_findDir(t *testing.T) {
abs, d := tNewDirs(t)
tt := []struct {
name string
in string
res []bfsDir
}{
{"rand", filepath.Join(abs, "rand"), []bfsDir{
{importPath: "rand", dir: filepath.Join(abs, "rand")},
}},
{"crypto/rand", filepath.Join(abs, "crypto/rand"), []bfsDir{
{importPath: "crypto/rand", dir: filepath.Join(abs, "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},
{"xx2", "/xx2", nil},
}
for _, tc := range tt {
tc := tc
t.Run(strings.Replace(tc.name, "/", "_", -1), func(t *testing.T) {
res := d.findDir(tc.in)
assert.Equal(t, tc.res, res, "dirs returned should be the equal")
})
}
}
Loading

0 comments on commit 0985fb1

Please sign in to comment.