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 29 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
99 changes: 97 additions & 2 deletions gnovm/cmd/gno/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ package main

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 @@ -71,7 +76,7 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error {
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 +87,110 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error {
})
}

// TODO: add more checkers
// 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
}

// 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
}

var reParseRecover = regexp.MustCompile(`^(.+):(\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)
}

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 {
issue.Location = fmt.Sprintf("%s:%s", matches[1], matches[2])
issue.Msg = strings.TrimSpace(matches[3])
} else {
issue.Location = fmt.Sprintf("%s:0", parsedError)
issue.Msg = err.Error()
}

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).",
}, {
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).
35 changes: 35 additions & 0 deletions gnovm/pkg/gnolang/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"os"
"strings"
"time"

// Ignore pprof import, as the server does not
Expand Down Expand Up @@ -76,6 +77,40 @@ func (d debugging) Errorf(format string, args ...interface{}) {
}
}

// PreprocessError wraps a processing error along with its associated
// preprocessing stack for enhanced error reporting.
type PreprocessError struct {
err error
stack []BlockNode
}

// Unwrap returns the encapsulated error message.
func (p *PreprocessError) Unwrap() error {
return p.err
}

// Stack produces a string representation of the preprocessing stack
// trace that was associated with the error occurrence.
func (p *PreprocessError) Stack() string {
var stacktrace strings.Builder
for i := len(p.stack) - 1; i >= 0; i-- {
sbn := p.stack[i]
fmt.Fprintf(&stacktrace, "stack %d: %s\n", i, sbn.String())
}
return stacktrace.String()
}

// Error consolidates and returns the full error message, including
// the actual error followed by its associated preprocessing stack.
func (p *PreprocessError) Error() string {
var err strings.Builder
fmt.Fprintf(&err, "%s:\n", p.Unwrap())
fmt.Fprintln(&err, "--- preprocess stack ---")
fmt.Fprint(&err, p.Stack())
fmt.Fprintf(&err, "------------------------")
return err.String()
}

// ----------------------------------------
// Exposed errors accessors
// File tests may access debug errors.
Expand Down
21 changes: 12 additions & 9 deletions gnovm/pkg/gnolang/preprocess.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,24 +154,27 @@ func Preprocess(store Store, ctx BlockNode, n Node) Node {

defer func() {
if r := recover(); r != nil {
fmt.Println("--- preprocess stack ---")
for i := len(stack) - 1; i >= 0; i-- {
sbn := stack[i]
fmt.Printf("stack %d: %s\n", i, sbn.String())
}
fmt.Println("------------------------")
// before re-throwing the error, append location information to message.
loc := last.GetLocation()
if nline := n.GetLine(); nline > 0 {
loc.Line = nline
}
if rerr, ok := r.(error); ok {

var err error
rerr, ok := r.(error)
if ok {
// NOTE: gotuna/gorilla expects error exceptions.
panic(errors.Wrap(rerr, loc.String()))
err = errors.Wrap(rerr, loc.String())
} else {
// NOTE: gotuna/gorilla expects error exceptions.
panic(errors.New(fmt.Sprintf("%s: %v", loc.String(), r)))
err = errors.New(fmt.Sprintf("%s: %v", loc.String(), r))
}

// Re-throw the error after wrapping it with the preprocessing stack information.
panic(&PreprocessError{
err: err,
stack: stack,
})
}
}()
if debug {
Expand Down
Loading