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: improve gno linter with basic errors support #1202

Merged
merged 34 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ba33912
feat: improve linter
gfanton Oct 6, 2023
0effbbf
chore: cleanup gnoland preprocess output
gfanton Oct 6, 2023
5bfdf28
feat: add `UPDATE_SCRIPTS` environement variable to gnovm
gfanton Oct 6, 2023
fd891a2
feat: add some linter testscripts tests
gfanton Oct 6, 2023
e53f017
chore: lint files
gfanton Oct 6, 2023
708092b
Merge branch 'master' into feat/gnolint-error
gfanton Oct 6, 2023
3ff3b43
fix: add standard test
gfanton Oct 7, 2023
5eb43c4
fix: add lint test for _test files
gfanton Oct 7, 2023
ec53732
chore: lint
gfanton Oct 9, 2023
1e556c7
Merge branch 'master' into feat/gnolint-error
gfanton Oct 12, 2023
145c78f
feat: add preprocess stack error
gfanton Oct 20, 2023
89befe1
fix: add lint file error testscripts
gfanton Oct 20, 2023
7ae0476
Merge remote-tracking branch 'origin/master' into feat/gnolint-error
gfanton Oct 20, 2023
f461b2d
chore: lint
gfanton Oct 20, 2023
a628c94
fix: repl test
gfanton Oct 23, 2023
984841e
fix: handle preprocess error on test
gfanton Oct 23, 2023
d40cf0f
chore: lint
gfanton Oct 25, 2023
0f5c9de
Merge remote-tracking branch 'origin/master' into feat/gnolint-error
gfanton Dec 4, 2023
c2c9656
chore: lint
gfanton Dec 4, 2023
caf6442
Merge remote-tracking branch 'origin/master' into feat/gnolint-error
gfanton Dec 6, 2023
14251e6
fix: gno run test for preprocess stack
gfanton Dec 6, 2023
89ae08f
chore: encapsulate error catch for better readability
gfanton Dec 7, 2023
11f101a
fix: lint
gfanton Dec 7, 2023
d0f7245
fix: bad rebase
gfanton Dec 7, 2023
8b828b0
fix: global lint
gfanton Dec 7, 2023
4b69afe
fix: linter
gfanton Dec 7, 2023
af22e23
feat: add update scripts
gfanton Dec 7, 2023
0540dc3
chore: update golden file
gfanton Dec 7, 2023
2f160cf
Merge branch 'master' into feat/gnolint-error
gfanton Dec 7, 2023
3fb3b71
fix: normalize lint error
gfanton Dec 8, 2023
3a444e0
chore: update contributing md
gfanton Dec 8, 2023
fdc0c10
fix: update golden files
gfanton Dec 8, 2023
845ab7c
Merge branch 'master' into feat/gnolint-error
gfanton Jan 15, 2024
0e4dad1
Merge branch 'master' into feat/gnolint-error
gfanton Jan 22, 2024
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
38 changes: 37 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ To use *gofumpt* instead of *gofmt*, as hinted in the comment, you may either ha
cexpr system('go run -modfile </path/to/gno>/misc/devdeps/go.mod mvdan.cc/gofumpt -w ' . expand('%'))
```

##### ViM Linting Support

To integrate GNO linting in Vim, you can use Vim's `:make` command with a custom `makeprg` and `errorformat` to run the GNO linter and parse its output. Add the following configuration to your `.vimrc` file:

```vim
autocmd FileType gno setlocal makeprg=gno\ lint\ %
autocmd FileType gno setlocal errorformat=%f:%l:\ %m

" Optional: Key binding to run :make on the current file
autocmd FileType gno nnoremap <buffer> <F5> :make<CR>
```

### ViM Support (with LSP)

There is an experimental and unofficial [Gno Language Server](https://github.com/jdkato/gnols)
Expand Down Expand Up @@ -172,7 +184,31 @@ Additionally, it's not possible to use `gofumpt` for code formatting with
2. Add to your emacs configuration file:

```lisp
(add-to-list 'auto-mode-alist '("\\.gno\\'" . go-mode))
(define-derived-mode gno-mode go-mode "GNO"
"Major mode for GNO files, an alias for go-mode."
(setq-local tab-width 8))
(define-derived-mode gno-dot-mod-mode go-dot-mod-mode "GNO Mod"
"Major mode for GNO mod files, an alias for go-dot-mod-mode."
)
```

3. To integrate GNO linting with Flycheck, add the following to your Emacs configuration:
```lisp
(require 'flycheck)

(flycheck-define-checker gno-lint
"A GNO syntax checker using the gno lint tool."
:command ("gno" "lint" source-original)
:error-patterns (;; ./file.gno:32: error message (code=1)
(error line-start (file-name) ":" line ": " (message) " (code=" (id (one-or-more digit)) ")." line-end))
;; Ensure the file is saved, to work around
;; https://github.com/python/mypy/issues/4746.
:predicate (lambda ()
(and (not (bound-and-true-p polymode-mode))
(flycheck-buffer-saved-p)))
:modes gno-mode)

(add-to-list 'flycheck-checkers 'gno-lint)
```

#### Sublime Text
Expand Down
124 changes: 121 additions & 3 deletions gnovm/cmd/gno/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

import (
"context"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/gnolang/gno/gnovm/pkg/gnoenv"
gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
"github.com/gnolang/gno/gnovm/tests"
"github.com/gnolang/gno/tm2/pkg/commands"
osm "github.com/gnolang/gno/tm2/pkg/os"
)
Expand Down Expand Up @@ -37,8 +42,10 @@
}

func (c *lintCfg) RegisterFlags(fs *flag.FlagSet) {
rootdir := gnoenv.RootDir()

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

Expand Down Expand Up @@ -71,7 +78,7 @@
fmt.Fprintf(io.Err(), "Linting %q...\n", pkgPath)
}

// 'gno.mod' exists?
// Check if 'gno.mod' exists
gnoModPath := filepath.Join(pkgPath, "gno.mod")
if !osm.FileExists(gnoModPath) {
addIssue(lintIssue{
Expand All @@ -82,20 +89,131 @@
})
}

// TODO: add more checkers
gfanton marked this conversation as resolved.
Show resolved Hide resolved
// Handle runtime errors
catchRuntimeError(pkgPath, addIssue, func() {
stdout, stdin, stderr := io.Out(), io.In(), io.Err()
testStore := tests.TestStore(
rootDir, "",
stdin, stdout, stderr,
tests.ImportModeStdlibsOnly,
)

targetPath := pkgPath
info, err := os.Stat(pkgPath)
if err == nil && !info.IsDir() {
targetPath = filepath.Dir(pkgPath)
}

memPkg := gno.ReadMemPackage(targetPath, targetPath)
tm := tests.TestMachine(testStore, stdout, memPkg.Name)

// Check package
tm.RunMemPackage(memPkg, true)

// Check test files
testfiles := &gno.FileSet{}
for _, mfile := range memPkg.Files {
if !strings.HasSuffix(mfile.Name, ".gno") {
continue // Skip non-GNO files
}

n, _ := gno.ParseFile(mfile.Name, mfile.Body)
if n == nil {
continue // Skip empty files

Check warning on line 122 in gnovm/cmd/gno/lint.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/lint.go#L122

Added line #L122 was not covered by tests
}

// XXX: package ending with `_test` is not supported yet
if strings.HasSuffix(mfile.Name, "_test.gno") && !strings.HasSuffix(string(n.PkgName), "_test") {
// Keep only test files
testfiles.AddFiles(n)
}
}

tm.RunFiles(testfiles.Files...)
})

// TODO: Add more checkers
}

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

return nil
}

func guessSourcePath(pkg, source string) string {
if info, err := os.Stat(pkg); !os.IsNotExist(err) && !info.IsDir() {
pkg = filepath.Dir(pkg)
}

sourceJoin := filepath.Join(pkg, source)
if _, err := os.Stat(sourceJoin); !os.IsNotExist(err) {
return filepath.Clean(sourceJoin)
}

if _, err := os.Stat(source); !os.IsNotExist(err) {
return filepath.Clean(source)
}

return filepath.Clean(pkg)

Check warning on line 159 in gnovm/cmd/gno/lint.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/lint.go#L159

Added line #L159 was not covered by tests
}

// reParseRecover is a regex designed to parse error details from a string.
// It extracts the file location, line number, and error message from a formatted error string.
// XXX: Ideally, error handling should encapsulate location details within a dedicated error type.
var reParseRecover = regexp.MustCompile(`^([^:]+):(\d+)(?::\d+)?:? *(.*)$`)

func catchRuntimeError(pkgPath string, addIssue func(issue lintIssue), action func()) {
defer func() {
// Errors catched here mostly come from: gnovm/pkg/gnolang/preprocess.go
r := recover()
if r == nil {
return
}

var err error
switch verr := r.(type) {
case *gno.PreprocessError:
err = verr.Unwrap()
case error:
err = verr
case string:
err = errors.New(verr)
default:
panic(r)

Check warning on line 184 in gnovm/cmd/gno/lint.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/lint.go#L181-L184

Added lines #L181 - L184 were not covered by tests
}

var issue lintIssue
issue.Confidence = 1
issue.Code = lintGnoError

parsedError := strings.TrimSpace(err.Error())
parsedError = strings.TrimPrefix(parsedError, pkgPath+"/")

matches := reParseRecover.FindStringSubmatch(parsedError)
if len(matches) == 4 {
sourcepath := guessSourcePath(pkgPath, matches[1])
issue.Location = fmt.Sprintf("%s:%s", sourcepath, matches[2])
issue.Msg = strings.TrimSpace(matches[3])
} else {
issue.Location = fmt.Sprintf("%s:0", filepath.Clean(pkgPath))
issue.Msg = err.Error()
}

Check warning on line 202 in gnovm/cmd/gno/lint.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/lint.go#L200-L202

Added lines #L200 - L202 were not covered by tests

addIssue(issue)
}()

action()
}

type lintCode int

const (
lintUnknown lintCode = 0
lintNoGnoMod lintCode = iota
lintGnoError

// TODO: add new linter codes here.
)

Expand Down
7 changes: 7 additions & 0 deletions gnovm/cmd/gno/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ func TestLintApp(t *testing.T) {
}, {
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/undefined-variable-test/undefined_variables_test.gno"},
stderrShouldContain: "undefined_variables_test.gno:6: name toto not declared (code=2)",
}, {
args: []string{"lint", "--set-exit-status=0", "../../tests/integ/package-not-declared/main.gno"},
stderrShouldContain: "main.gno:4: name fmt not declared (code=2).",
thehowl marked this conversation as resolved.
Show resolved Hide resolved
}, {
args: []string{"lint", "--set-exit-status=0", "../../tests/integ/run-main/"},
stderrShouldContain: "./../../tests/integ/run-main: missing 'gno.mod' file (code=1).",
Expand All @@ -20,6 +26,7 @@ func TestLintApp(t *testing.T) {
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?
Expand Down
4 changes: 4 additions & 0 deletions gnovm/cmd/gno/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ func TestRunApp(t *testing.T) {
args: []string{"run", "-expr", "WithArg(-255)", "../../tests/integ/run-package"},
stdoutShouldContain: "out of range!",
},
{
args: []string{"run", "../../tests/integ/undefined-variable-test/undefined_variables_test.gno"},
recoverShouldContain: "--- preprocess stack ---", // should contain preprocess debug stack trace
},
// TODO: a test file
// TODO: args
// TODO: nativeLibs VS stdlibs
Expand Down
6 changes: 5 additions & 1 deletion gnovm/cmd/gno/test_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"os"
"strconv"
"testing"

"github.com/gnolang/gno/gnovm/pkg/integration"
Expand All @@ -9,8 +11,10 @@ import (
)

func Test_ScriptsTest(t *testing.T) {
updateScripts, _ := strconv.ParseBool(os.Getenv("UPDATE_SCRIPTS"))
p := testscript.Params{
Dir: "testdata/gno_test",
UpdateScripts: updateScripts,
Dir: "testdata/gno_test",
}

if coverdir, ok := integration.ResolveCoverageDir(); ok {
Expand Down
19 changes: 19 additions & 0 deletions gnovm/cmd/gno/testdata/gno_test/lint_bad_import.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# testing gno lint command: bad import error

! gno lint ./bad_file.gno

cmp stdout stdout.golden
cmp stderr stderr.golden

-- bad_file.gno --
package main

import "python"

func main() {
fmt.Println("Hello", 42)
}

-- stdout.golden --
-- stderr.golden --
bad_file.gno:1: unknown import path python (code=2).
20 changes: 20 additions & 0 deletions gnovm/cmd/gno/testdata/gno_test/lint_file_error.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# gno lint: test file error

! gno lint ./i_have_error_test.gno

cmp stdout stdout.golden
cmp stderr stderr.golden

-- i_have_error_test.gno --
package main

import "fmt"

func TestIHaveSomeError() {
i := undefined_variable
fmt.Println("Hello", 42)
}

-- stdout.golden --
-- stderr.golden --
i_have_error_test.gno:6: name undefined_variable not declared (code=2).
20 changes: 20 additions & 0 deletions gnovm/cmd/gno/testdata/gno_test/lint_file_error_txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# gno lint: test file error

! gno lint ./i_have_error_test.gno

cmp stdout stdout.golden
cmp stderr stderr.golden

-- i_have_error_test.gno --
package main

import "fmt"

func TestIHaveSomeError() {
i := undefined_variable
fmt.Println("Hello", 42)
}

-- stdout.golden --
-- stderr.golden --
i_have_error_test.gno:6: name undefined_variable not declared (code=2).
18 changes: 18 additions & 0 deletions gnovm/cmd/gno/testdata/gno_test/lint_no_error.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# testing simple gno lint command with any error

gno lint ./good_file.gno

cmp stdout stdout.golden
cmp stdout stderr.golden

-- good_file.gno --
package main

import "fmt"

func main() {
fmt.Println("Hello", 42)
}

-- stdout.golden --
-- stderr.golden --
19 changes: 19 additions & 0 deletions gnovm/cmd/gno/testdata/gno_test/lint_no_gnomod.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# gno lint: no gnomod

! gno lint .

cmp stdout stdout.golden
cmp stderr stderr.golden

-- good_file.gno --
package main

import "fmt"

func main() {
fmt.Println("Hello", 42)
}

-- stdout.golden --
-- stderr.golden --
./.: missing 'gno.mod' file (code=1).
20 changes: 20 additions & 0 deletions gnovm/cmd/gno/testdata/gno_test/lint_not_declared.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# testing gno lint command: not declared error

! gno lint ./bad_file.gno

cmp stdout stdout.golden
cmp stderr stderr.golden

-- bad_file.gno --
package main

import "fmt"

func main() {
hello.Foo()
fmt.Println("Hello", 42)
}

-- stdout.golden --
-- stderr.golden --
bad_file.gno:6: name hello not declared (code=2).
Loading
Loading