Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
34 changes: 28 additions & 6 deletions gnovm/cmd/gno/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,23 @@ import (
"io/fs"
"os"
"path/filepath"
"time"

"github.com/gnolang/gno/gnovm/pkg/doc"
"github.com/gnolang/gno/gnovm/pkg/gnoenv"
"github.com/gnolang/gno/gnovm/pkg/gnomod"
"github.com/gnolang/gno/tm2/pkg/bft/rpc/client"
"github.com/gnolang/gno/tm2/pkg/commands"
)

type docCfg struct {
all bool
src bool
unexported bool
short bool
rootDir string
all bool
src bool
unexported bool
short bool
rootDir string
remote string
remoteTimeout time.Duration
}

func newDocCmd(io commands.IO) *commands.Command {
Expand Down Expand Up @@ -73,6 +77,20 @@ func (c *docCfg) RegisterFlags(fs *flag.FlagSet) {
"",
"clone location of github.com/gnolang/gno (gno binary tries to guess it)",
)

fs.StringVar(
&c.remote,
"remote",
"https://rpc.gno.land:443",
"remote gno.land node address (if needed)",
)

fs.DurationVar(
&c.remoteTimeout,
"remote-timeout",
time.Minute,
"defined how much time a request to the node should live before timeout",
)
}

func execDoc(cfg *docCfg, args []string, io commands.IO) error {
Expand Down Expand Up @@ -101,7 +119,11 @@ func execDoc(cfg *docCfg, args []string, io commands.IO) error {

// select dirs from which to gather directories
dirs := []string{filepath.Join(cfg.rootDir, "gnovm/stdlibs")}
res, err := doc.ResolveDocumentable(dirs, modDirs, args, cfg.unexported)
queryClient, err := client.NewHTTPClient(cfg.remote, client.WithRequestTimeout(cfg.remoteTimeout))
if err != nil {
return err
}
res, err := doc.ResolveDocumentable(dirs, modDirs, args, cfg.unexported, queryClient)
if res == nil {
return err
}
Expand Down
85 changes: 71 additions & 14 deletions gnovm/pkg/doc/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package doc

import (
"context"
"errors"
"fmt"
"go/token"
Expand All @@ -15,9 +16,17 @@ import (
"path/filepath"
"strings"

"github.com/gnolang/gno/tm2/pkg/amino"
ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types"
"go.uber.org/multierr"
)

// ABCIQueryClient is a simplified interface for just calling ABCIQuery.
// It is implemented by RPCClient and others, but can be implemented for mock test
type ABCIQueryClient interface {
ABCIQuery(ctx context.Context, path string, data []byte) (*ctypes.ResultABCIQuery, error)
}

// WriteDocumentationOptions represents the possible options when requesting
// documentation through Documentable.
type WriteDocumentationOptions struct {
Expand All @@ -39,6 +48,7 @@ type Documentable struct {
symbol string
accessible string
pkgData *pkgData
doc *JSONDocumentation
}

func (d *Documentable) WriteDocumentation(w io.Writer, o *WriteDocumentationOptions) error {
Expand All @@ -47,21 +57,30 @@ func (d *Documentable) WriteDocumentation(w io.Writer, o *WriteDocumentationOpti
}
o.w = w

var doc *JSONDocumentation
var pkgName string
var err error
// pkgData may already be initialised if we already had to look to see
// if it had the symbol we wanted; otherwise initialise it now.
if d.pkgData == nil {
d.pkgData, err = newPkgData(d.bfsDir, o.Unexported)
if d.doc != nil {
// Already got the JSONDocumentation (from vm/qdoc)
doc = d.doc
pkgName = doc.PackagePath
} else {
// pkgData may already be initialised if we already had to look to see
// if it had the symbol we wanted; otherwise initialise it now.
if d.pkgData == nil {
d.pkgData, err = newPkgData(d.bfsDir, o.Unexported)
if err != nil {
return err
}
}

pkgName = d.pkgData.name
doc, err = d.WriteJSONDocumentation(o)
if err != nil {
return err
}
}

doc, err := d.WriteJSONDocumentation(o)
if err != nil {
return err
}

// copied from go source - map vars, constants and constructors to their respective types.
typedValue := make(map[string]string)
constructor := make(map[string]string)
Expand Down Expand Up @@ -106,7 +125,7 @@ func (d *Documentable) WriteDocumentation(w io.Writer, o *WriteDocumentationOpti
}

pp := &pkgPrinter{
name: d.pkgData.name,
name: pkgName,
doc: doc,
typedValue: typedValue,
constructor: constructor,
Expand Down Expand Up @@ -174,17 +193,18 @@ var fpAbs = filepath.Abs
// dirs specifies the gno system directories to scan which specify full import paths
// in their directories, such as @/examples and @/gnovm/stdlibs; modDirs specifies
// directories which contain a gno.mod file.
func ResolveDocumentable(dirs, modDirs, args []string, unexported bool) (*Documentable, error) {
// If the package is not found locally, query the remote vm/qdoc using the queryClient (if not nil)
func ResolveDocumentable(dirs, modDirs, args []string, unexported bool, queryClient ABCIQueryClient) (*Documentable, error) {
d := newDirs(dirs, modDirs)

parsed, ok := parseArgs(args)
if !ok {
return nil, fmt.Errorf("commands/doc: invalid arguments: %v", args)
}
return resolveDocumentable(d, parsed, unexported)
return resolveDocumentable(d, parsed, unexported, queryClient)
}

func resolveDocumentable(dirs *bfsDirs, parsed docArgs, unexported bool) (*Documentable, error) {
func resolveDocumentable(dirs *bfsDirs, parsed docArgs, unexported bool, queryClient ABCIQueryClient) (*Documentable, error) {
var candidates []bfsDir

// if we have a candidate package name, search dirs for a dir that matches it.
Expand All @@ -208,14 +228,21 @@ func resolveDocumentable(dirs *bfsDirs, parsed docArgs, unexported bool) (*Docum
candidates = dirs.findPackage(parsed.pkg)
}

if len(candidates) == 0 {
jdoc := queryQDoc(parsed.pkg, queryClient)
if jdoc != nil {
return &Documentable{doc: jdoc}, nil
}
}

if len(candidates) == 0 {
// there are no candidates.
// if this is ambiguous, remove ambiguity and try parsing args using pkg as the symbol.
if !parsed.pkgAmbiguous {
return nil, fmt.Errorf("commands/doc: package not found: %q", parsed.pkg)
}
parsed = docArgs{pkg: ".", sym: parsed.pkg, acc: parsed.sym}
return resolveDocumentable(dirs, parsed, unexported)
return resolveDocumentable(dirs, parsed, unexported, queryClient)
}
// we wanted documentation about a package, and we found one!
if parsed.sym == "" {
Expand Down Expand Up @@ -267,6 +294,36 @@ func resolveDocumentable(dirs *bfsDirs, parsed docArgs, unexported bool) (*Docum
)
}

// queryQDoc uses the queryClient to query the remote vm/qdoc for the pkg path and returns the JSONDocumentation.
// If queryClient is nil, do nothing and return nil.
// If error, log to the console and return nil.
func queryQDoc(pkg string, queryClient ABCIQueryClient) *JSONDocumentation {
if queryClient == nil {
return nil
}

const qpath = "vm/qdoc"
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
qres, err := queryClient.ABCIQuery(ctx, qpath, []byte(pkg))
if err != nil {
log.Printf("unable to query qdoc for %q: %q", pkg, err)
return nil
}
if qres.Response.Error != nil {
log.Printf("error querying qdoc for %q: %q", pkg, qres.Response.Error)
return nil
}

jdoc := &JSONDocumentation{}
if err := amino.UnmarshalJSON(qres.Response.Data, jdoc); err != nil {
log.Printf("unable to unmarshal qdoc: %q", err)
return nil
}

return jdoc
}

// docArgs represents the parsed args of the doc command.
// sym could be a symbol, but the accessibles of types should also be shown if they match sym.
type docArgs struct {
Expand Down
85 changes: 60 additions & 25 deletions gnovm/pkg/doc/doc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,38 @@ package doc

import (
"bytes"
"context"
"os"
"path/filepath"
"strings"
"testing"

abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

type mockQueryClient struct{}

func (m *mockQueryClient) ABCIQuery(ctx context.Context, path string, data []byte) (*ctypes.ResultABCIQuery, error) {
if string(data) == "gno.land/r/test/mock" {
return &ctypes.ResultABCIQuery{
Response: abci.ResponseQuery{
ResponseBase: abci.ResponseBase{
Data: []byte(`{"package_path":"gno.land/r/test/mock","package_line":"package mock // import \"mock\"","package_doc":""}`),
},
},
}, nil
} else {
return &ctypes.ResultABCIQuery{
Response: abci.ResponseQuery{
ResponseBase: abci.ResponseBase{Error: abci.StringError("?"), Data: nil},
},
}, nil
}
}

func TestResolveDocumentable(t *testing.T) {
p, err := os.Getwd()
require.NoError(t, err)
Expand All @@ -22,86 +45,98 @@ func TestResolveDocumentable(t *testing.T) {
require.NoError(t, err)
return pd
}
queryClient := &mockQueryClient{}

tt := []struct {
name string
args []string
unexp bool
queryClient ABCIQueryClient
expect *Documentable
errContains string
}{
{"package", []string{"crypto/rand"}, false, &Documentable{bfsDir: getDir("crypto/rand")}, ""},
{"packageMod", []string{"gno.land/mod"}, false, nil, `package not found`},
{"dir", []string{"./testdata/integ/crypto/rand"}, false, &Documentable{bfsDir: getDir("crypto/rand")}, ""},
{"dirMod", []string{"./testdata/integ/mod"}, false, &Documentable{bfsDir: getDir("mod")}, ""},
{"dirAbs", []string{path("crypto/rand")}, false, &Documentable{bfsDir: getDir("crypto/rand")}, ""},
{"package", []string{"crypto/rand"}, false, nil, &Documentable{bfsDir: getDir("crypto/rand")}, ""},
// Use queryClient to test failing to fetch from vm/qdoc
{"packageMod", []string{"gno.land/mod"}, false, queryClient, nil, `package not found`},
{"dir", []string{"./testdata/integ/crypto/rand"}, false, nil, &Documentable{bfsDir: getDir("crypto/rand")}, ""},
{"dirMod", []string{"./testdata/integ/mod"}, false, nil, &Documentable{bfsDir: getDir("mod")}, ""},
{"dirAbs", []string{path("crypto/rand")}, false, nil, &Documentable{bfsDir: getDir("crypto/rand")}, ""},
// test_notapkg exists in local dir and also path("test_notapkg").
// ResolveDocumentable should first try local dir, and seeing as it is not a valid dir, try searching it as a package.
{"dirLocalMisleading", []string{"test_notapkg"}, false, &Documentable{bfsDir: getDir("test_notapkg")}, ""},
{"dirLocalMisleading", []string{"test_notapkg"}, false, nil, &Documentable{bfsDir: getDir("test_notapkg")}, ""},
{
"normalSymbol",
[]string{"crypto/rand.Flag"},
false,
false, nil,
&Documentable{bfsDir: getDir("crypto/rand"), symbol: "Flag", pkgData: pdata("crypto/rand", false)}, "",
},
{
"normalAccessible",
[]string{"crypto/rand.Generate"},
false,
false, nil,
&Documentable{bfsDir: getDir("crypto/rand"), symbol: "Generate", pkgData: pdata("crypto/rand", false)}, "",
},
{
"normalSymbolUnexp",
[]string{"crypto/rand.unexp"},
true,
true, nil,
&Documentable{bfsDir: getDir("crypto/rand"), symbol: "unexp", pkgData: pdata("crypto/rand", true)}, "",
},
{
"normalAccessibleFull",
[]string{"crypto/rand.Rand.Name"},
false,
false, nil,
&Documentable{bfsDir: getDir("crypto/rand"), symbol: "Rand", accessible: "Name", pkgData: pdata("crypto/rand", false)}, "",
},
{
"disambiguate",
[]string{"rand.Flag"},
false,
false, nil,
&Documentable{bfsDir: getDir("crypto/rand"), symbol: "Flag", pkgData: pdata("crypto/rand", false)}, "",
},
{
"disambiguate2",
[]string{"rand.Crypto"},
false,
false, nil,
&Documentable{bfsDir: getDir("crypto/rand"), symbol: "Crypto", pkgData: pdata("crypto/rand", false)}, "",
},
{
"disambiguate3",
[]string{"rand.Normal"},
false,
false, nil,
&Documentable{bfsDir: getDir("rand"), symbol: "Normal", pkgData: pdata("rand", false)}, "",
},
{
"disambiguate4", // just "rand" should use the directory that matches it exactly.
[]string{"rand"},
false,
false, nil,
&Documentable{bfsDir: getDir("rand")}, "",
},
{
"wdSymbol",
[]string{"WdConst"},
false,
false, nil,
&Documentable{bfsDir: getDir("wd"), symbol: "WdConst", pkgData: pdata("wd", false)}, "",
},
{
"packageMock",
[]string{"gno.land/r/test/mock"},
false, queryClient,
&Documentable{doc: &JSONDocumentation{
PackagePath: "gno.land/r/test/mock",
PackageLine: "package mock // import \"mock\"",
}}, "",
},

{"errInvalidArgs", []string{"1", "2", "3"}, false, nil, "invalid arguments: [1 2 3]"},
{"errNoCandidates", []string{"math", "Big"}, false, nil, `package not found: "math"`},
{"errNoCandidates2", []string{"LocalSymbol"}, false, nil, `package not found`},
{"errNoCandidates3", []string{"Symbol.Accessible"}, false, nil, `package not found`},
{"errNonExisting", []string{"rand.NotExisting"}, false, nil, `could not resolve arguments`},
{"errIgnoredMod", []string{"modignored"}, false, &Documentable{bfsDir: getDir("modignored")}, ""},
{"errIgnoredMod2", []string{"./testdata/integ/modignored"}, false, &Documentable{bfsDir: getDir("modignored")}, ""},
{"errUnexp", []string{"crypto/rand.unexp"}, false, nil, "could not resolve arguments"},
{"errDirNotapkg", []string{"./test_notapkg"}, false, nil, `package not found: "./test_notapkg"`},
{"errInvalidArgs", []string{"1", "2", "3"}, false, nil, nil, "invalid arguments: [1 2 3]"},
{"errNoCandidates", []string{"math", "Big"}, false, nil, nil, `package not found: "math"`},
{"errNoCandidates2", []string{"LocalSymbol"}, false, nil, nil, `package not found`},
{"errNoCandidates3", []string{"Symbol.Accessible"}, false, nil, nil, `package not found`},
{"errNonExisting", []string{"rand.NotExisting"}, false, nil, nil, `could not resolve arguments`},
{"errIgnoredMod", []string{"modignored"}, false, nil, &Documentable{bfsDir: getDir("modignored")}, ""},
{"errIgnoredMod2", []string{"./testdata/integ/modignored"}, false, nil, &Documentable{bfsDir: getDir("modignored")}, ""},
{"errUnexp", []string{"crypto/rand.unexp"}, false, nil, nil, "could not resolve arguments"},
{"errDirNotapkg", []string{"./test_notapkg"}, false, nil, nil, `package not found: "./test_notapkg"`},
}

for _, tc := range tt {
Expand All @@ -116,7 +151,7 @@ func TestResolveDocumentable(t *testing.T) {
}
result, err := ResolveDocumentable(
[]string{path("")}, []string{path("mod")},
tc.args, tc.unexp,
tc.args, tc.unexp, tc.queryClient,
)
// we use stripFset because d.pkgData.fset contains sync/atomic values,
// which in turn makes reflect.DeepEqual compare the two sync.Atomic values.
Expand Down
Loading