Skip to content

Commit f4b8fc5

Browse files
jefft0D4ryl00thehowl
authored andcommitted
feat: Add vm/qdoc with function comments (#3459)
Addresses #522 (comment) by adding a query for `vm/qdoc` that uses a similar code path to `gno doc` which includes comments. For example, `gnokey query vm/qdoc -data "gno.land/r/gnoland/valopers/v2" -remote tcp://127.0.0.1:26657` returns the following JSON doc for [valopers](https://github.com/gnolang/gno/blob/master/examples/gno.land/r/gnoland/valopers/v2/valopers.gno). ``` { "package_path": "gno.land/r/gnoland/valopers/v2", "package_line": "package valopers // import \"valopers\"", "package_doc": "Package valopers is designed around the permissionless lifecycle of valoper profiles. It also includes parts designed for govdao to propose valset changes based on registered valopers.\n", "values": [ { "signature": "const (\n\terrValoperExists = \"valoper already exists\"\n\terrValoperMissing = \"valoper does not exist\" // Valoper is missing\n\terrInvalidAddressUpdate = \"valoper updated address exists\"\n\terrValoperNotCaller = \"valoper is not the caller\"\n)", "const": true, "values": [ { "name": "errValoperExists", "doc": "", "type": "" }, { "name": "errValoperMissing", "doc": "", "type": "" }, { "name": "errInvalidAddressUpdate", "doc": "", "type": "" }, { "name": "errValoperNotCaller", "doc": "", "type": "" } ], "doc": "" }, { "signature": "var valopers *avl.Tree // Address -> Valoper\n", "const": false, "values": [ { "name": "valopers", "doc": "// Address -> Valoper\n", "type": "*avl.Tree" } ], "doc": "valopers keeps track of all the active validator operators\n" } ], "funcs": [ { "type": "", "name": "GovDAOProposal", "signature": "func GovDAOProposal(address std.Address)", "doc": "GovDAOProposal creates a proposal to the GovDAO for adding the given valoper to the validator set. This function is meant to serve as a helper for generating the govdao proposal\n", "params": [ { "Name": "address", "Type": "std.Address" } ], "results": [] }, { "type": "", "name": "Register", "signature": "func Register(v Valoper)", "doc": "Register registers a new valoper\n", "params": [ { "Name": "v", "Type": "Valoper" } ], "results": [] }, { "type": "", "name": "Render", "signature": "func Render(_ string) string", "doc": "Render renders the current valoper set\n", "params": [ { "Name": "_", "Type": "string" } ], "results": [ { "Name": "", "Type": "string" } ] }, { "type": "", "name": "Update", "signature": "func Update(address std.Address, v Valoper)", "doc": "Update updates an existing valoper\n", "params": [ { "Name": "address", "Type": "std.Address" }, { "Name": "v", "Type": "Valoper" } ], "results": [] }, { "type": "", "name": "init", "signature": "func init()", "doc": "", "params": [], "results": [] }, { "type": "", "name": "isValoper", "signature": "func isValoper(address std.Address) bool", "doc": "isValoper checks if the valoper exists\n", "params": [ { "Name": "address", "Type": "std.Address" } ], "results": [ { "Name": "", "Type": "bool" } ] }, { "type": "", "name": "GetByAddr", "signature": "func GetByAddr(address std.Address) Valoper", "doc": "GetByAddr fetches the valoper using the address, if present\n", "params": [ { "Name": "address", "Type": "std.Address" } ], "results": [ { "Name": "", "Type": "Valoper" } ] }, { "type": "Valoper", "name": "Render", "signature": "func (v Valoper) Render() string", "doc": "Render renders a single valoper with their information\n", "params": [], "results": [ { "Name": "", "Type": "string" } ] } ], "types": [ { "name": "Valoper", "signature": "type Valoper struct {\n\tName string // the display name of the valoper\n\tMoniker string // the moniker of the valoper\n\tDescription string // the description of the valoper\n\n\tAddress std.Address // The bech32 gno address of the validator\n\tPubKey string // the bech32 public key of the validator\n\tP2PAddresses []string // the publicly reachable P2P addresses of the validator\n\tActive bool // flag indicating if the valoper is active\n}", "doc": "Valoper represents a validator operator profile\n" } ] } ``` --------- Signed-off-by: Jeff Thompson <[email protected]> Signed-off-by: D4ryl00 <[email protected]> Co-authored-by: D4ryl00 <[email protected]> Co-authored-by: Morgan <[email protected]>
1 parent 787131a commit f4b8fc5

File tree

11 files changed

+720
-63
lines changed

11 files changed

+720
-63
lines changed

docs/gno-tooling/cli/gnokey/querying-a-network.md

+72
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Below is a list of queries a user can make with `gnokey`:
2424
- `bank/balances/{ADDRESS}` - returns balances of an account
2525
- `vm/qfuncs` - returns the exported functions for a given pkgpath
2626
- `vm/qfile` - returns package contents for a given pkgpath
27+
- `vm/qdoc` - Returns the JSON of the doc for a given pkgpath, suitable for printing
2728
- `vm/qeval` - evaluates an expression in read-only mode on and returns the results
2829
- `vm/qrender` - shorthand for evaluating `vm/qeval Render("")` for a given pkgpath
2930

@@ -176,6 +177,77 @@ const (
176177
...
177178
```
178179

180+
181+
## `vm/qdoc`
182+
183+
Using the `vm/qdoc` query, we can fetch the docs, for functions, types and variables from a specific
184+
package path. To specify the path we want to query, we can use the `-data` flag:
185+
186+
```bash
187+
gnokey query vm/qdoc --data "gno.land/r/gnoland/valopers/v2" -remote https://rpc.gno.land:443
188+
```
189+
190+
The output is a JSON string containing doc strings of the package, functions, etc., including comments for `valopers` realm:
191+
192+
```json
193+
height: 0
194+
data: {
195+
"package_path": "gno.land/r/gnoland/valopers/v2",
196+
"package_line": "package valopers // import \"valopers\"",
197+
"package_doc": "Package valopers is designed around the permissionless lifecycle of valoper profiles. It also includes parts designed for govdao to propose valset changes based on registered valopers.\n",
198+
"values": [
199+
{
200+
"name": "valopers",
201+
"doc": "// Address -> Valoper\n",
202+
"type": "*avl.Tree"
203+
}
204+
// other values
205+
],
206+
"funcs": [
207+
{
208+
"type": "",
209+
"name": "GetByAddr",
210+
"signature": "func GetByAddr(address std.Address) Valoper",
211+
"doc": "GetByAddr fetches the valoper using the address, if present\n",
212+
"params": [
213+
{
214+
"Name": "address",
215+
"Type": "std.Address"
216+
}
217+
],
218+
"results": [
219+
{
220+
"Name": "",
221+
"Type": "Valoper"
222+
}
223+
]
224+
}
225+
// other funcs
226+
{
227+
"type": "Valoper",
228+
"name": "Render",
229+
"signature": "func (v Valoper) Render() string",
230+
"doc": "Render renders a single valoper with their information\n",
231+
"params": [],
232+
"results": [
233+
{
234+
"Name": "",
235+
"Type": "string"
236+
}
237+
]
238+
}
239+
// other methods (in this case of the Valoper type)
240+
],
241+
"types": [
242+
{
243+
"name": "Valoper",
244+
"signature": "type Valoper struct {\n\tName string // the display name of the valoper\n\tMoniker string // the moniker of the valoper\n\tDescription string // the description of the valoper\n\n\tAddress std.Address // The bech32 gno address of the validator\n\tPubKey string // the bech32 public key of the validator\n\tP2PAddresses []string // the publicly reachable P2P addresses of the validator\n\tActive bool // flag indicating if the valoper is active\n}",
245+
"doc": "Valoper represents a validator operator profile\n"
246+
}
247+
]
248+
}
249+
```
250+
179251
## `vm/qeval`
180252

181253
`vm/qeval` allows us to evaluate a call to an exported function without using gas,

docs/reference/rpc-endpoints.md

+1
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@ Call with the `/abci_query` to get information via the ABCI Query.
459459
| `bank/balances/{ADDRESS}` | Returns the balance information about the account. |
460460
| `vm/qfuncs` | Returns public facing function signatures as JSON. |
461461
| `vm/qfile` | Returns the file bytes, or list of files if directory. |
462+
| `vm/qdoc` | Returns JSON of the package doc, suitable for printing. |
462463
| `vm/qrender` | Calls `.Render(<path>)` in readonly mode. |
463464
| `vm/qeval` | Evaluates any expression in readonly mode and returns the results. |
464465
| `vm/store` | (not yet supported) Fetches items from the store. |

gno.land/pkg/sdk/vm/handler.go

+15
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ const (
7272
QueryFuncs = "qfuncs"
7373
QueryEval = "qeval"
7474
QueryFile = "qfile"
75+
QueryDoc = "qdoc"
7576
)
7677

7778
func (vh vmHandler) Query(ctx sdk.Context, req abci.RequestQuery) abci.ResponseQuery {
@@ -89,6 +90,8 @@ func (vh vmHandler) Query(ctx sdk.Context, req abci.RequestQuery) abci.ResponseQ
8990
res = vh.queryEval(ctx, req)
9091
case QueryFile:
9192
res = vh.queryFile(ctx, req)
93+
case QueryDoc:
94+
res = vh.queryDoc(ctx, req)
9295
default:
9396
return sdk.ABCIResponseQueryFromError(
9497
std.ErrUnknownRequest(fmt.Sprintf(
@@ -183,6 +186,18 @@ func (vh vmHandler) queryFile(ctx sdk.Context, req abci.RequestQuery) (res abci.
183186
return
184187
}
185188

189+
// queryDoc returns the JSON of the doc for a given pkgpath, suitable for printing
190+
func (vh vmHandler) queryDoc(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) {
191+
filepath := string(req.Data)
192+
jsonDoc, err := vh.vm.QueryDoc(ctx, filepath)
193+
if err != nil {
194+
res = sdk.ABCIResponseQueryFromError(err)
195+
return
196+
}
197+
res.Data = []byte(jsonDoc.JSON())
198+
return
199+
}
200+
186201
// ----------------------------------------
187202
// misc
188203

gno.land/pkg/sdk/vm/handler_test.go

+116
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66

77
"github.com/gnolang/gno/gnovm"
8+
"github.com/gnolang/gno/gnovm/pkg/doc"
89
abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
910
"github.com/gnolang/gno/tm2/pkg/crypto"
1011
"github.com/gnolang/gno/tm2/pkg/std"
@@ -327,3 +328,118 @@ func TestVmHandlerQuery_File(t *testing.T) {
327328
})
328329
}
329330
}
331+
332+
func TestVmHandlerQuery_Doc(t *testing.T) {
333+
expected := &doc.JSONDocumentation{
334+
PackagePath: "gno.land/r/hello",
335+
PackageLine: "package hello // import \"hello\"",
336+
PackageDoc: "hello is a package for testing\n",
337+
Values: []*doc.JSONValueDecl{
338+
{
339+
Signature: "const prefix = \"Hello\"",
340+
Const: true,
341+
Doc: "The prefix for the hello message\n",
342+
Values: []*doc.JSONValue{
343+
{
344+
Name: "prefix",
345+
Doc: "",
346+
Type: "",
347+
},
348+
},
349+
},
350+
},
351+
Funcs: []*doc.JSONFunc{
352+
{
353+
Type: "",
354+
Name: "Hello",
355+
Signature: "func Hello(msg string) (res string)",
356+
Doc: "",
357+
Params: []*doc.JSONField{
358+
{Name: "msg", Type: "string"},
359+
},
360+
Results: []*doc.JSONField{
361+
{Name: "res", Type: "string"},
362+
},
363+
},
364+
{
365+
Type: "myStruct",
366+
Name: "Foo",
367+
Signature: "func (ms myStruct) Foo() string",
368+
Doc: "",
369+
Params: []*doc.JSONField{},
370+
Results: []*doc.JSONField{
371+
{Name: "", Type: "string"},
372+
},
373+
},
374+
},
375+
Types: []*doc.JSONType{
376+
{
377+
Name: "myStruct",
378+
Signature: "type myStruct struct{ a int }",
379+
Doc: "myStruct is a struct for testing\n",
380+
},
381+
},
382+
}
383+
384+
tt := []struct {
385+
input []byte
386+
expectedResult string
387+
expectedErrorMatch string
388+
}{
389+
// valid queries
390+
{input: []byte(`gno.land/r/hello`), expectedResult: expected.JSON()},
391+
{input: []byte(`gno.land/r/doesnotexist`), expectedErrorMatch: `invalid package path`},
392+
}
393+
394+
for _, tc := range tt {
395+
name := string(tc.input)
396+
t.Run(name, func(t *testing.T) {
397+
env := setupTestEnv()
398+
ctx := env.vmk.MakeGnoTransactionStore(env.ctx)
399+
vmHandler := env.vmh
400+
401+
// Give "addr1" some gnots.
402+
addr := crypto.AddressFromPreimage([]byte("addr1"))
403+
acc := env.acck.NewAccountWithAddress(ctx, addr)
404+
env.acck.SetAccount(ctx, acc)
405+
env.bank.SetCoins(ctx, addr, std.MustParseCoins("10000000ugnot"))
406+
assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins("10000000ugnot")))
407+
408+
// Create test package.
409+
files := []*gnovm.MemFile{
410+
{Name: "hello.gno", Body: `
411+
// hello is a package for testing
412+
package hello
413+
414+
// myStruct is a struct for testing
415+
type myStruct struct{a int}
416+
func (ms myStruct) Foo() string { return "myStruct.Foo" }
417+
// The prefix for the hello message
418+
const prefix = "Hello"
419+
func Hello(msg string) (res string) { res = prefix+" "+msg; return }
420+
`},
421+
}
422+
pkgPath := "gno.land/r/hello"
423+
msg1 := NewMsgAddPackage(addr, pkgPath, files)
424+
err := env.vmk.AddPackage(ctx, msg1)
425+
assert.NoError(t, err)
426+
427+
req := abci.RequestQuery{
428+
Path: "vm/qdoc",
429+
Data: tc.input,
430+
}
431+
432+
res := vmHandler.Query(env.ctx, req)
433+
if tc.expectedErrorMatch == "" {
434+
assert.True(t, res.IsOK(), "should not have error")
435+
if tc.expectedResult != "" {
436+
assert.Equal(t, tc.expectedResult, string(res.Data))
437+
}
438+
} else {
439+
assert.False(t, res.IsOK(), "should have an error")
440+
errmsg := res.Error.Error()
441+
assert.Regexp(t, tc.expectedErrorMatch, errmsg)
442+
}
443+
})
444+
}
445+
}

gno.land/pkg/sdk/vm/keeper.go

+17
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"time"
1717

1818
"github.com/gnolang/gno/gnovm"
19+
"github.com/gnolang/gno/gnovm/pkg/doc"
1920
gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
2021
"github.com/gnolang/gno/gnovm/stdlibs"
2122
"github.com/gnolang/gno/tm2/pkg/crypto"
@@ -827,6 +828,22 @@ func (vm *VMKeeper) QueryFile(ctx sdk.Context, filepath string) (res string, err
827828
}
828829
}
829830

831+
func (vm *VMKeeper) QueryDoc(ctx sdk.Context, pkgPath string) (*doc.JSONDocumentation, error) {
832+
store := vm.newGnoTransactionStore(ctx) // throwaway (never committed)
833+
834+
memPkg := store.GetMemPackage(pkgPath)
835+
if memPkg == nil {
836+
err := ErrInvalidPkgPath(fmt.Sprintf(
837+
"package not found: %s", pkgPath))
838+
return nil, err
839+
}
840+
d, err := doc.NewDocumentableFromMemPkg(memPkg, true, "", "")
841+
if err != nil {
842+
return nil, err
843+
}
844+
return d.WriteJSONDocumentation()
845+
}
846+
830847
// logTelemetry logs the VM processing telemetry
831848
func logTelemetry(
832849
gasUsed int64,

gnovm/pkg/doc/doc.go

+8-15
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,14 @@ type WriteDocumentationOptions struct {
3636
}
3737

3838
// Documentable is a package, symbol, or accessible which can be documented.
39-
type Documentable interface {
40-
WriteDocumentation(w io.Writer, opts *WriteDocumentationOptions) error
41-
}
42-
43-
// static implementation check
44-
var _ Documentable = (*documentable)(nil)
45-
46-
type documentable struct {
39+
type Documentable struct {
4740
bfsDir
4841
symbol string
4942
accessible string
5043
pkgData *pkgData
5144
}
5245

53-
func (d *documentable) WriteDocumentation(w io.Writer, o *WriteDocumentationOptions) error {
46+
func (d *Documentable) WriteDocumentation(w io.Writer, o *WriteDocumentationOptions) error {
5447
if o == nil {
5548
o = &WriteDocumentationOptions{}
5649
}
@@ -110,7 +103,7 @@ func (d *documentable) WriteDocumentation(w io.Writer, o *WriteDocumentationOpti
110103
return d.output(pp)
111104
}
112105

113-
func (d *documentable) output(pp *pkgPrinter) (err error) {
106+
func (d *Documentable) output(pp *pkgPrinter) (err error) {
114107
defer func() {
115108
// handle the case of errFatal.
116109
// this will have been generated by pkg.Fatalf, so get the error
@@ -163,7 +156,7 @@ var fpAbs = filepath.Abs
163156
// dirs specifies the gno system directories to scan which specify full import paths
164157
// in their directories, such as @/examples and @/gnovm/stdlibs; modDirs specifies
165158
// directories which contain a gno.mod file.
166-
func ResolveDocumentable(dirs, modDirs, args []string, unexported bool) (Documentable, error) {
159+
func ResolveDocumentable(dirs, modDirs, args []string, unexported bool) (*Documentable, error) {
167160
d := newDirs(dirs, modDirs)
168161

169162
parsed, ok := parseArgs(args)
@@ -173,7 +166,7 @@ func ResolveDocumentable(dirs, modDirs, args []string, unexported bool) (Documen
173166
return resolveDocumentable(d, parsed, unexported)
174167
}
175168

176-
func resolveDocumentable(dirs *bfsDirs, parsed docArgs, unexported bool) (Documentable, error) {
169+
func resolveDocumentable(dirs *bfsDirs, parsed docArgs, unexported bool) (*Documentable, error) {
177170
var candidates []bfsDir
178171

179172
// if we have a candidate package name, search dirs for a dir that matches it.
@@ -208,13 +201,13 @@ func resolveDocumentable(dirs *bfsDirs, parsed docArgs, unexported bool) (Docume
208201
}
209202
// we wanted documentation about a package, and we found one!
210203
if parsed.sym == "" {
211-
return &documentable{bfsDir: candidates[0]}, nil
204+
return &Documentable{bfsDir: candidates[0]}, nil
212205
}
213206

214207
// we also have a symbol, and maybe accessible.
215208
// search for the symbol through the candidates
216209

217-
doc := &documentable{
210+
doc := &Documentable{
218211
symbol: parsed.sym,
219212
accessible: parsed.acc,
220213
}
@@ -246,7 +239,7 @@ func resolveDocumentable(dirs *bfsDirs, parsed docArgs, unexported bool) (Docume
246239
}
247240
doc.bfsDir = candidate
248241
doc.pkgData = pd
249-
// match found. return this as documentable.
242+
// match found. return this as Documentable.
250243
return doc, multierr.Combine(errs...)
251244
}
252245
}

0 commit comments

Comments
 (0)