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): initial documentation command for stdlibs and example packages #610

Merged
merged 34 commits into from
May 12, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
243d24c
feat(gnodev): add preliminary doc command, refactor common flags verb…
thehowl Mar 13, 2023
34e7a95
feat(gnodev): initial documentation command for stdlibs packages
thehowl Mar 17, 2023
262c6d8
Merge branch 'master' of github.com:gnolang/gno into dev/morgan/gnode…
thehowl Mar 17, 2023
042f86e
chore: remove accidental log line
thehowl Mar 17, 2023
cbb8012
chore: linter
thehowl Mar 17, 2023
b3cb246
Merge branch 'master' of github.com:gnolang/gno into dev/morgan/gnode…
thehowl Mar 21, 2023
54d5ff9
feat(commands/doc): add support for examples directory
thehowl Mar 21, 2023
49bc623
chore: prefer writing to stdout directly instead of using a buffer
thehowl Mar 21, 2023
4ff36c8
fix(doc): improve arg parsing, more tests
thehowl Mar 22, 2023
46385a1
tests: add tests for Document
thehowl Mar 22, 2023
8987cf0
Merge branch 'master' of github.com:gnolang/gno into dev/morgan/gnode…
thehowl Mar 22, 2023
b731610
chore: fix typo
thehowl Mar 22, 2023
987b838
Merge branch 'master' of github.com:gnolang/gno into dev/morgan/gnode…
thehowl Apr 18, 2023
72f9d7f
update to new dir structure
thehowl Apr 18, 2023
3d2168e
fmt
thehowl Apr 18, 2023
af9030b
Merge branch 'master' into dev/morgan/gnodev-doc
thehowl Apr 18, 2023
d7a296d
Update gnovm/cmd/gno/doc.go
thehowl Apr 18, 2023
e6c21d5
typo
thehowl Apr 18, 2023
ec49331
Revert "feat(gnodev): add preliminary doc command, refactor common fl…
thehowl Apr 18, 2023
6a42bad
Merge branch 'dev/morgan/gnodev-doc' of github.com:thehowl/gno into d…
thehowl Apr 18, 2023
76b07dd
Merge branch 'master' of github.com:gnolang/gno into dev/morgan/gnode…
thehowl Apr 18, 2023
f33e938
change documentable iface
thehowl Apr 18, 2023
444eaf7
add doc test
thehowl Apr 18, 2023
77cc149
address changes requested from code review
thehowl Apr 19, 2023
6a60b55
Merge branch 'master' of github.com:gnolang/gno into dev/morgan/gnode…
thehowl Apr 19, 2023
23eb5ca
fix typo
thehowl Apr 19, 2023
1b5b5db
Merge branch 'master' of github.com:gnolang/gno into dev/morgan/gnode…
thehowl May 9, 2023
ed6b21e
code review changes
thehowl May 9, 2023
e0dc07c
unexport dirs
thehowl May 9, 2023
5f874a4
some typos, some error logging
thehowl May 10, 2023
d817e92
better and more consistent error handling
thehowl May 10, 2023
c712286
Merge branch 'master' of github.com:gnolang/gno into dev/morgan/gnode…
thehowl May 11, 2023
d481161
use errors.Is for comparison
thehowl May 11, 2023
a2965cc
empty commit to trigger github workflow
thehowl May 12, 2023
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
87 changes: 87 additions & 0 deletions gnovm/cmd/gno/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package main
thehowl marked this conversation as resolved.
Show resolved Hide resolved

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
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.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 := doc.NewDirs(filepath.Join(cfg.rootDir, "gnovm/stdlibs"), filepath.Join(cfg.rootDir, "examples"))
res, err := doc.ResolveDocumentable(dirs, args, cfg.unexported)
if res == nil {
thehowl marked this conversation as resolved.
Show resolved Hide resolved
thehowl marked this conversation as resolved.
Show resolved Hide resolved
return err
}
if err != nil {
io.Printfln("warning: error parsing some candidate packages:\n%v", err)
}
err = res.WriteDocumentation(
io.Out,
doc.WithShowAll(cfg.all),
doc.WithSource(cfg.src),
doc.WithUnexported(cfg.unexported),
)
if err != nil {
return err
}
return nil
}
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.
thehowl marked this conversation as resolved.
Show resolved Hide resolved
//
// Copyright 2015 The Go Authors. All rights reserved.

package doc

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

// A Dir describes a directory holding code by specifying
// the expected import path and the file system directory.
type Dir 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 Dirs struct {
scan chan Dir // Directories generated by walk.
hist []Dir // History of reported Dirs.
offset int // Counter for Next.
}

// NewDirs begins scanning the given stdlibs directory.
func NewDirs(dirs ...string) *Dirs {
d := &Dirs{
hist: make([]Dir, 0, 256),
thehowl marked this conversation as resolved.
Show resolved Hide resolved
scan: make(chan Dir),
}
go d.walk(dirs)
return d
}

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

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

// walk walks the trees in the given roots.
func (d *Dirs) 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 Gno source directory it finds is delivered on d.scan.
func (d *Dirs) 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)
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
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 <- Dir{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 *Dirs) findPackage(name string) []Dir {
d.Reset()
candidates := make([]Dir, 0, 4)
thehowl marked this conversation as resolved.
Show resolved Hide resolved
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 *Dirs) findDir(absdir string) []Dir {
d.Reset()
for dir, ok := d.Next(); ok; dir, ok = d.Next() {
if dir.dir == absdir {
return []Dir{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 newDirs(t *testing.T) (string, *Dirs) {
t.Helper()
p, err := filepath.Abs("./testdata/dirs")
require.NoError(t, err)
return p, NewDirs(p)
}

func TestDirs_findPackage(t *testing.T) {
abs, d := newDirs(t)
tt := []struct {
name string
res []Dir
}{
{"rand", []Dir{
{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", []Dir{
{importPath: "crypto/rand", dir: filepath.Join(abs, "crypto/rand")},
}},
{"math", []Dir{
{importPath: "math", dir: filepath.Join(abs, "math")},
}},
{"ath", []Dir{}},
{"/math", []Dir{}},
{"", []Dir{}},
}
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 := newDirs(t)
tt := []struct {
name string
in string
res []Dir
}{
{"rand", filepath.Join(abs, "rand"), []Dir{
{importPath: "rand", dir: filepath.Join(abs, "rand")},
}},
{"crypto/rand", filepath.Join(abs, "crypto/rand"), []Dir{
{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