Skip to content

Commit

Permalink
feat: initial 'gno lint' (#981)
Browse files Browse the repository at this point in the history
This PR introduces a new `gno lint` subcommand, currently focused on
checking for the presence of a `gno.mod` file in a package, and it's
linked with the CI. In the future, we plan to expand `gno lint` to
support additional checks, enhancing its utility for developers and CI
integration.

Addresses #850
Addresses #927
Related with
#965 (comment)

Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com>
Co-authored-by: Miloš Živković <milos@zmilos.com>
  • Loading branch information
moul and zivkovicmilos authored Jul 31, 2023
1 parent 6c9a1c6 commit 3ade982
Show file tree
Hide file tree
Showing 28 changed files with 240 additions and 22 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,23 @@ jobs:
- uses: actions/checkout@v3
- run: go install -v ./gnovm/cmd/gno
- run: go run ./gnovm/cmd/gno test --verbose ./examples
lint:
strategy:
fail-fast: false
matrix:
go-version: [ "1.19.x", "1.20.x" ]
# unittests: TODO: matrix with contracts
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- uses: actions/checkout@v3
- run: go install -v ./gnovm/cmd/gno
# testing official directories, basically examples/ minus examples/.../x/.
- run: go run ./gnovm/cmd/gno lint --verbose ./examples/gno.land/p
- run: go run ./gnovm/cmd/gno lint --verbose ./examples/gno.land/r/demo
- run: go run ./gnovm/cmd/gno lint --verbose ./examples/gno.land/r/gnoland
- run: go run ./gnovm/cmd/gno lint --verbose ./examples/gno.land/r/system
# TODO: track coverage
10 changes: 10 additions & 0 deletions examples/Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Official packages: more reliable and tested modules, distinct from the experimentation area.
OFFICIAL_PACKAGES = ./gno.land/p
OFFICIAL_PACKAGES += ./gno.land/r/demo
OFFICIAL_PACKAGES += ./gno.land/r/gnoland
OFFICIAL_PACKAGES += ./gno.land/r/system

.PHONY: help
help:
@echo "Available make commands:"
Expand All @@ -15,6 +21,10 @@ build: precompile
test:
go run ../gnovm/cmd/gno test --verbose .

.PHONY: lint
lint:
go run ../gnovm/cmd/gno lint $(OFFICIAL_PACKAGES)

.PHONY: test.sync
test.sync:
go run ../gnovm/cmd/gno test --verbose --update-golden-tests .
Expand Down
6 changes: 6 additions & 0 deletions examples/gno.land/p/demo/grc/exts/vault/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module gno.land/p/demo/grc/exts/vault

require (
"gno.land/p/demo/avl" v0.0.0-latest
"gno.land/p/demo/grc/grc20" v0.0.0-latest
)
1 change: 1 addition & 0 deletions examples/gno.land/p/demo/merkle/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/demo/merkle
7 changes: 7 additions & 0 deletions examples/gno.land/p/demo/microblog/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module gno.land/p/demo/microblog

require (
"gno.land/p/demo/avl" v0.0.0-latest
"gno.land/p/demo/ufmt" v0.0.0-latest
"gno.land/r/demo/users" v0.0.0-latest
)
6 changes: 6 additions & 0 deletions examples/gno.land/r/demo/microblog/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module gno.land/r/demo/microblog

require (
"gno.land/p/demo/microblog" v0.0.0-latest
"gno.land/r/demo/users" v0.0.0-latest
)
5 changes: 5 additions & 0 deletions examples/gno.land/r/demo/ui/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module gno.land/r/demo/ui

require (
"gno.land/p/demo/ui" v0.0.0-latest
)
3 changes: 0 additions & 3 deletions examples/gno.land/r/demo/x/upgrade/gno.mod

This file was deleted.

19 changes: 19 additions & 0 deletions examples/gno.land/r/x/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# `x/` Experimentation Area

Welcome to the `x/` directory, designed for experimentation.

Code in this area is less reviewed and more subject to change or break over
time. It's a great place to publish experiments and proposals for significant
challenges before they become an MVP and move to another directory.

While publishing code here provides core developers with more examples and edge
cases to work on through CI, consider alternative options such as working on
other repositories, keeping your work in a pull request, or exploring
[https://github.com/gnolang/hackerspace](https://github.com/gnolang/hackerspace)
for broader visibility and collaboration within the Gno community.

Exercise caution as code in this directory may be less stable or secure due to
its experimental nature.

Feel free to explore, experiment, and contribute to the exciting developments
happening in the `x/` directory. Together, we can shape the future of GnoVM.
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# r/x/outfmt
# r/x/manfred_outfmt

PoC of output formatting options.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module gno.land/r/demo/x/outfmt
module gno.land/r/x/manfred_outfmt

require (
"gno.land/p/demo/rand" v0.0.0-latest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ func genResult() Result {
func Render(path string) string {
if path == "" {
output := ""
output += "* [?fmt=stringer](/r/x/outfmt:?fmt=stringer)\n"
output += "* [?fmt=json](/r/x/outfmt:?fmt=json)\n"
output += "* [?fmt=jsonp](/r/x/outfmt:?fmt=jsonp)\n"
output += "* [?fmt=stringer](/r/x/manfred_outfmt:?fmt=stringer)\n"
output += "* [?fmt=json](/r/x/manfred_outfmt:?fmt=json)\n"
output += "* [?fmt=jsonp](/r/x/manfred_outfmt:?fmt=jsonp)\n"
return output
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ package outfmt
import (
"testing"

"gno.land/r/demo/x/outfmt"
"gno.land/r/x/manfred_outfmt"
)

func TestRender(t *testing.T) {
// home
{
got := outfmt.Render("")
expected := `* [?fmt=stringer](/r/x/outfmt:?fmt=stringer)
* [?fmt=json](/r/x/outfmt:?fmt=json)
* [?fmt=jsonp](/r/x/outfmt:?fmt=jsonp)
expected := `* [?fmt=stringer](/r/x/manfred_outfmt:?fmt=stringer)
* [?fmt=json](/r/x/manfred_outfmt:?fmt=json)
* [?fmt=jsonp](/r/x/manfred_outfmt:?fmt=jsonp)
`
if got != expected {
t.Fatalf("expected %q, got %q.", expected, got)
Expand Down
3 changes: 3 additions & 0 deletions examples/gno.land/r/x/manfred_upgrade_patterns/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Draft

module gno.land/r/x/manfred_upgrade_patterns
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package upgradea

import (
v1 "gno.land/r/demo/x/upgrade/upgrade-a/v1"
v2 "gno.land/r/demo/x/upgrade/upgrade-a/v2"
v1 "gno.land/r/x/manfred_upgrade_patterns/upgrade-a/v1"
v2 "gno.land/r/x/manfred_upgrade_patterns/upgrade-a/v2"
)

func main() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package upgradea
import (
"strconv"

v1 "gno.land/r/demo/x/upgrade/upgrade-a/v1"
v1 "gno.land/r/x/manfred_upgrade_patterns/upgrade-a/v1"
)

var counter int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package upgradeb
import (
"std"

v1 "gno.land/r/demo/x/upgrade/upgrade-b/v1"
v1 "gno.land/r/x/manfred_upgrade_patterns/upgrade-b/v1"
)

const admin = "blahblah"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package root

var (
counter int
currentImplementation = "gno.land/r/demo/x/upgrade/upgrade-c/v1"
currentImplementation = "gno.land/r/x/manfred_upgrade_patterns/upgrade-c/v1"
)

func Inc() int {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package v1

import "gno.land/r/demo/x/upgrade/upgrade-c/root"
import "gno.land/r/x/manfred_upgrade_patterns/upgrade-c/root"

func Inc() {
root.Inc()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package v1

import "gno.land/r/demo/x/upgrade/upgrade-c/root"
import "gno.land/r/x/manfred_upgrade_patterns/upgrade-c/root"

func Inc() {
root.Inc()
Expand Down
112 changes: 112 additions & 0 deletions gnovm/cmd/gno/lint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package main

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

"github.com/gnolang/gno/tm2/pkg/commands"
osm "github.com/gnolang/gno/tm2/pkg/os"
)

type lintCfg struct {
verbose bool
rootDir string
setExitStatus int
// min_confidence: minimum confidence of a problem to print it (default 0.8)
// auto-fix: apply suggested fixes automatically.
}

func newLintCmd(io *commands.IO) *commands.Command {
cfg := &lintCfg{}

return commands.NewCommand(
commands.Metadata{
Name: "lint",
ShortUsage: "lint [flags] <package> [<package>...]",
ShortHelp: "Runs the linter for the specified packages",
},
cfg,
func(_ context.Context, args []string) error {
return execLint(cfg, args, io)
},
)
}

func (c *lintCfg) RegisterFlags(fs *flag.FlagSet) {
fs.BoolVar(&c.verbose, "verbose", false, "verbose output when lintning")
fs.StringVar(&c.rootDir, "root-dir", "", "clone location of github.com/gnolang/gno (gnodev tries to guess it)")
fs.IntVar(&c.setExitStatus, "set_exit_status", 1, "set exit status to 1 if any issues are found")
}

func execLint(cfg *lintCfg, args []string, io *commands.IO) error {
if len(args) < 1 {
return flag.ErrHelp
}

var (
verbose = cfg.verbose
rootDir = cfg.rootDir
)
if rootDir == "" {
rootDir = guessRootDir()
}

pkgPaths, err := gnoPackagesFromArgs(args)
if err != nil {
return fmt.Errorf("list packages from args: %w", err)
}

hasError := false
addIssue := func(issue lintIssue) {
hasError = true
fmt.Fprint(io.Err, issue.String()+"\n")
}

for _, pkgPath := range pkgPaths {
if verbose {
fmt.Fprintf(io.Err, "Linting %q...\n", pkgPath)
}

// 'gno.mod' exists?
gnoModPath := filepath.Join(pkgPath, "gno.mod")
if !osm.FileExists(gnoModPath) {
addIssue(lintIssue{
Code: lintNoGnoMod,
Confidence: 1,
Location: pkgPath,
Msg: "missing 'gno.mod' file",
})
}

// TODO: add more checkers
}

if hasError && cfg.setExitStatus != 0 {
os.Exit(cfg.setExitStatus)
}
return nil
}

type lintCode int

const (
lintUnknown lintCode = 0
lintNoGnoMod lintCode = iota
// TODO: add new linter codes here.
)

type lintIssue struct {
Code lintCode
Msg string
Confidence float64 // 1 is 100%
Location string // file:line, or equivalent
// TODO: consider writing fix suggestions
}

func (i lintIssue) String() string {
// TODO: consider crafting a doc URL based on Code.
return fmt.Sprintf("%s: %s (code=%d).", i.Location, i.Msg, i.Code)
}
31 changes: 31 additions & 0 deletions gnovm/cmd/gno/lint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package main

import "testing"

func TestLintApp(t *testing.T) {
tc := []testMainCase{
{
args: []string{"lint"},
errShouldBe: "flag: help requested",
}, {
args: []string{"lint", "--set_exit_status=0", "../../tests/integ/run-main/"},
stderrShouldContain: "./../../tests/integ/run-main: missing 'gno.mod' file (code=1).",
}, {
args: []string{"lint", "--set_exit_status=0", "../../tests/integ/run-main/"},
stderrShouldContain: "./../../tests/integ/run-main: missing 'gno.mod' file (code=1).",
}, {
args: []string{"lint", "--set_exit_status=0", "../../tests/integ/minimalist-gnomod/"},
// TODO: raise an error because there is a gno.mod, but no .gno files
}, {
args: []string{"lint", "--set_exit_status=0", "../../tests/integ/invalid-module-name/"},
// TODO: raise an error because gno.mod is invalid
},
// TODO: 'gno mod' is valid?
// TODO: is gno source valid?
// TODO: are dependencies valid?
// TODO: is gno source using unsafe/discouraged features?
// TODO: consider making `gno precompile; go lint *gen.go`
// TODO: check for imports of native libs from non _test.gno files
}
testMainCaseRun(t, tc)
}
5 changes: 3 additions & 2 deletions gnovm/cmd/gno/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ func newGnocliCmd(io *commands.IO) *commands.Command {
)

cmd.AddSubCommands(
newModCmd(io),
newTestCmd(io),
newLintCmd(io),
newRunCmd(io),
newBuildCmd(io),
newPrecompileCmd(io),
newTestCmd(io),
newModCmd(io),
newCleanCmd(io),
newReplCmd(),
newDocCmd(io),
Expand Down
1 change: 1 addition & 0 deletions gnovm/docs/go-gno-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,4 @@ Additional native types:
| go tool | | |
| go version | | |
| go vet | | |
| golint | gno lint | same intention |
2 changes: 1 addition & 1 deletion misc/docker-integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func runSuite(t *testing.T, tempdir string) {
var acc gnoland.GnoAccount
dockerExec_gnokeyQuery(t, "auth/accounts/"+test1Addr, &acc)
require.Equal(t, test1Addr, acc.Address.String(), "test1 account not found")
minCoins := std.MustParseCoins("9999900000000ugnot")
minCoins := std.MustParseCoins("9990000000000ugnot") // This value is chosen arbitrarily and may not be optimal. Feel free to update it to a more suitable amount
require.True(t, acc.Coins.IsAllGTE(minCoins),
"test1 account coins expected at least %s, got %s", minCoins, acc.Coins)

Expand Down

0 comments on commit 3ade982

Please sign in to comment.