From ba339124a837f3ec941f8e0b5edaa9ca8dcf4658 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 6 Oct 2023 21:44:20 +0200 Subject: [PATCH 01/26] feat: improve linter Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/lint.go | 106 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 97 insertions(+), 9 deletions(-) diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index 158b9d8db5d..b95ac8ac9db 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -6,11 +6,17 @@ import ( "fmt" "os" "path/filepath" + "regexp" + "strings" + 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" ) +var reParseRecover = regexp.MustCompile(`^(.+):(\d+): ?(.*)$`) + type lintCfg struct { verbose bool rootDir string @@ -71,22 +77,102 @@ func execLint(cfg *lintCfg, args []string, io *commands.IO) error { } // '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", - }) + { + gnoModPath := filepath.Join(pkgPath, "gno.mod") + if !osm.FileExists(gnoModPath) { + addIssue(lintIssue{ + Code: lintNoGnoMod, + Confidence: 1, + Location: pkgPath, + Msg: "missing 'gno.mod' file", + }) + } + } + + // run gno machine to detect basic package errors + { + var ( + stdout = io.Out + stdin = io.In + stderr = io.Err + + testStore = tests.TestStore( + rootDir, "", + stdin, stdout, stderr, + tests.ImportModeStdlibsOnly, + ) + ) + + catchError := func() { + r := recover() + if r == nil { + return + } + + if err, ok := r.(error); ok { + loc := strings.TrimSpace(err.Error()) + // XXX: this should not happen, loc should not contain package path + loc = strings.TrimPrefix(loc, pkgPath+"/") + subm := reParseRecover.FindStringSubmatch(loc) + if len(subm) > 0 { + // subm[1]: file + // subm[2]: line + // subm[3]: error + addIssue(lintIssue{ + Code: lintGnoError, + Confidence: 1, + Location: fmt.Sprintf("%s:%s", subm[1], subm[2]), + Msg: strings.TrimSpace(subm[3]), + }) + } + } + } + + // run the machine on the target package + func() { + defer catchError() + + // reuse test run files + memPkg := gno.ReadMemPackage(filepath.Dir(pkgPath), pkgPath) + m := tests.TestMachine(testStore, stdout, memPkg.Name) + + // check package + m.RunMemPackage(memPkg, true) + + // check test files + { + testfiles := &gno.FileSet{} + for _, mfile := range memPkg.Files { + if !strings.HasSuffix(mfile.Name, ".gno") { + continue // skip this file. + } + + n, _ := gno.ParseFile(mfile.Name, mfile.Body) + if n == nil { + continue + } + + if strings.HasSuffix(mfile.Name, "_test.gno") { + testfiles.AddFiles(n) + } + } + + m.RunFiles(testfiles.Files...) + } + }() } - // TODO: add more checkers + // XXX: add more checkers } if hasError && cfg.setExitStatus != 0 { os.Exit(cfg.setExitStatus) } + + if verbose { + fmt.Println("no lint errors") + } + return nil } @@ -95,6 +181,8 @@ type lintCode int const ( lintUnknown lintCode = 0 lintNoGnoMod lintCode = iota + lintGnoError + // TODO: add new linter codes here. ) From 0effbbf01dbc392857900d165c8422ff8a602201 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 6 Oct 2023 21:45:03 +0200 Subject: [PATCH 02/26] chore: cleanup gnoland preprocess output Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/pkg/gnolang/preprocess.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index e02a158fcf1..21bde8e06a0 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -154,12 +154,6 @@ 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 { From 5bfdf288caa571589c493f2e2229f574543c040a Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 6 Oct 2023 21:45:52 +0200 Subject: [PATCH 03/26] feat: add `UPDATE_SCRIPTS` environement variable to gnovm from `testscripts` doc.go: UpdateScripts specifies that if a `cmp` command fails and its second argument refers to a file inside the testscript file, the command will succeed and the testscript file will be updated to reflect the actual content (which could be stdout, stderr or a real file). The content will be quoted with txtar.Quote if needed; a manual change will be needed if it is not unquoted in the script. Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/main_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gnovm/cmd/gno/main_test.go b/gnovm/cmd/gno/main_test.go index 8d19a50e814..82fd5f69491 100644 --- a/gnovm/cmd/gno/main_test.go +++ b/gnovm/cmd/gno/main_test.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" "testing" @@ -153,10 +154,13 @@ func setupTestScript(t *testing.T, txtarDir string) testscript.Params { rootDir := filepath.Dir(string(goModPath)) // Build a fresh gno binary in a temp directory gnoBin := filepath.Join(t.TempDir(), "gno") + err = exec.Command("go", "build", "-o", gnoBin, filepath.Join(rootDir, "gnovm", "cmd", "gno")).Run() require.NoError(t, err) // Define script params + updateSripts, _ := strconv.ParseBool(os.Getenv("UPDATE_SCRIPTS")) return testscript.Params{ + UpdateScripts: updateSripts, Setup: func(env *testscript.Env) error { env.Vars = append(env.Vars, "GNOROOT="+rootDir, // thx PR 1014 :) From fd891a2f28e9d2ca092c1350c97c026a899d69dd Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 6 Oct 2023 21:49:30 +0200 Subject: [PATCH 04/26] feat: add some linter testscripts tests Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- .../testdata/gno_test/lint_bad_import.txtar | 19 ++++++++++++++++++ .../testdata/gno_test/lint_file_error_txtar | 20 +++++++++++++++++++ .../gno/testdata/gno_test/lint_no_error.txtar | 18 +++++++++++++++++ .../testdata/gno_test/lint_no_gnomod.txtar | 19 ++++++++++++++++++ .../testdata/gno_test/lint_not_declared.txtar | 20 +++++++++++++++++++ 5 files changed, 96 insertions(+) create mode 100644 gnovm/cmd/gno/testdata/gno_test/lint_bad_import.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_test/lint_file_error_txtar create mode 100644 gnovm/cmd/gno/testdata/gno_test/lint_no_error.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_test/lint_no_gnomod.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_test/lint_not_declared.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_bad_import.txtar b/gnovm/cmd/gno/testdata/gno_test/lint_bad_import.txtar new file mode 100644 index 00000000000..946e1bcba35 --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_test/lint_bad_import.txtar @@ -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). diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_file_error_txtar b/gnovm/cmd/gno/testdata/gno_test/lint_file_error_txtar new file mode 100644 index 00000000000..9482eeb1f4f --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_test/lint_file_error_txtar @@ -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). diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_no_error.txtar b/gnovm/cmd/gno/testdata/gno_test/lint_no_error.txtar new file mode 100644 index 00000000000..95356b1ba2b --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_test/lint_no_error.txtar @@ -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 -- diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_no_gnomod.txtar b/gnovm/cmd/gno/testdata/gno_test/lint_no_gnomod.txtar new file mode 100644 index 00000000000..52daa6f0e9b --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_test/lint_no_gnomod.txtar @@ -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). diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_not_declared.txtar b/gnovm/cmd/gno/testdata/gno_test/lint_not_declared.txtar new file mode 100644 index 00000000000..7bd74a34855 --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_test/lint_not_declared.txtar @@ -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). From e53f0172d46a18d644a327d4e5acbeb1f71a4843 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 6 Oct 2023 22:26:22 +0200 Subject: [PATCH 05/26] chore: lint files Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/lint.go | 136 +++++++++++++++++++----------------------- 1 file changed, 62 insertions(+), 74 deletions(-) diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index b95ac8ac9db..db0e12e388c 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -15,8 +15,6 @@ import ( osm "github.com/gnolang/gno/tm2/pkg/os" ) -var reParseRecover = regexp.MustCompile(`^(.+):(\d+): ?(.*)$`) - type lintCfg struct { verbose bool rootDir string @@ -76,93 +74,83 @@ func execLint(cfg *lintCfg, args []string, io *commands.IO) error { 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", - }) - } + // Check if '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", + }) } - // run gno machine to detect basic package errors - { - var ( - stdout = io.Out - stdin = io.In - stderr = io.Err - - testStore = tests.TestStore( - rootDir, "", - stdin, stdout, stderr, - tests.ImportModeStdlibsOnly, - ) - ) + // Use `RunMemPackage` to detect basic package errors + var ( + stdout = io.Out + stdin = io.In + stderr = io.Err - catchError := func() { - r := recover() - if r == nil { - return - } + testStore = tests.TestStore( + rootDir, "", + stdin, stdout, stderr, + tests.ImportModeStdlibsOnly, + ) - if err, ok := r.(error); ok { - loc := strings.TrimSpace(err.Error()) - // XXX: this should not happen, loc should not contain package path - loc = strings.TrimPrefix(loc, pkgPath+"/") - subm := reParseRecover.FindStringSubmatch(loc) - if len(subm) > 0 { - // subm[1]: file - // subm[2]: line - // subm[3]: error + reParseRecover = regexp.MustCompile(`^(.+):(\d+): ?(.*)$`) + ) + + handleError := func() { + // Errors here mostly come from: gnovm/pkg/gnolang/preprocess.go + if r := recover(); r != nil { + if recErr, ok := r.(error); ok { + parsedError := strings.TrimSpace(recErr.Error()) + parsedError = strings.TrimPrefix(parsedError, pkgPath+"/") + matches := reParseRecover.FindStringSubmatch(parsedError) + if len(matches) > 0 { addIssue(lintIssue{ Code: lintGnoError, Confidence: 1, - Location: fmt.Sprintf("%s:%s", subm[1], subm[2]), - Msg: strings.TrimSpace(subm[3]), + Location: fmt.Sprintf("%s:%s", matches[1], matches[2]), + Msg: strings.TrimSpace(matches[3]), }) } } } + } - // run the machine on the target package - func() { - defer catchError() - - // reuse test run files - memPkg := gno.ReadMemPackage(filepath.Dir(pkgPath), pkgPath) - m := tests.TestMachine(testStore, stdout, memPkg.Name) - - // check package - m.RunMemPackage(memPkg, true) - - // check test files - { - testfiles := &gno.FileSet{} - for _, mfile := range memPkg.Files { - if !strings.HasSuffix(mfile.Name, ".gno") { - continue // skip this file. - } - - n, _ := gno.ParseFile(mfile.Name, mfile.Body) - if n == nil { - continue - } - - if strings.HasSuffix(mfile.Name, "_test.gno") { - testfiles.AddFiles(n) - } - } + // Run the machine on the target package + func() { + defer handleError() + + memPkg := gno.ReadMemPackage(filepath.Dir(pkgPath), pkgPath) + tm := tests.TestMachine(testStore, stdout, memPkg.Name) - m.RunFiles(testfiles.Files...) + // 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 } - }() - } - // XXX: add more checkers + n, _ := gno.ParseFile(mfile.Name, mfile.Body) + if n == nil { + continue // Skip empty files + } + + if strings.HasSuffix(mfile.Name, "_test.gno") { + // Keep only test files + testfiles.AddFiles(n) + } + } + + tm.RunFiles(testfiles.Files...) + }() + + // TODO: Add more checkers here } if hasError && cfg.setExitStatus != 0 { From 3ff3b437eec2bdf322149778e5d47117c2fab722 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Sat, 7 Oct 2023 11:29:07 +0200 Subject: [PATCH 06/26] fix: add standard test Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/lint_test.go | 4 ++++ gnovm/tests/integ/package-not-declared/gno.mod | 1 + gnovm/tests/integ/package-not-declared/main.gno | 5 +++++ 3 files changed, 10 insertions(+) create mode 100644 gnovm/tests/integ/package-not-declared/gno.mod create mode 100644 gnovm/tests/integ/package-not-declared/main.gno diff --git a/gnovm/cmd/gno/lint_test.go b/gnovm/cmd/gno/lint_test.go index ce200a1fedd..77b7e3a15d4 100644 --- a/gnovm/cmd/gno/lint_test.go +++ b/gnovm/cmd/gno/lint_test.go @@ -13,6 +13,9 @@ 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/package-not-declared/main.gno"}, + stderrShouldContain: "main.gno:4: name fmt not declared (code=2).", }, { 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 @@ -20,6 +23,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? diff --git a/gnovm/tests/integ/package-not-declared/gno.mod b/gnovm/tests/integ/package-not-declared/gno.mod new file mode 100644 index 00000000000..8b4e0375297 --- /dev/null +++ b/gnovm/tests/integ/package-not-declared/gno.mod @@ -0,0 +1 @@ +module gno.land/tests/nodeclared \ No newline at end of file diff --git a/gnovm/tests/integ/package-not-declared/main.gno b/gnovm/tests/integ/package-not-declared/main.gno new file mode 100644 index 00000000000..bdbb2e7cfcb --- /dev/null +++ b/gnovm/tests/integ/package-not-declared/main.gno @@ -0,0 +1,5 @@ +package main + +func Main() { + fmt.Println("hello world") +} From 5eb43c4e8611bced423cbde611aca2da0695d823 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Sat, 7 Oct 2023 14:42:30 +0200 Subject: [PATCH 07/26] fix: add lint test for _test files Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/lint_test.go | 4 ++-- gnovm/tests/integ/undefined-variable-test/gno.mod | 1 + .../undefined-variable-test/undefined_variables_test.gno | 7 +++++++ 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 gnovm/tests/integ/undefined-variable-test/gno.mod create mode 100644 gnovm/tests/integ/undefined-variable-test/undefined_variables_test.gno diff --git a/gnovm/cmd/gno/lint_test.go b/gnovm/cmd/gno/lint_test.go index 77b7e3a15d4..319415d34ac 100644 --- a/gnovm/cmd/gno/lint_test.go +++ b/gnovm/cmd/gno/lint_test.go @@ -11,8 +11,8 @@ 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/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).", diff --git a/gnovm/tests/integ/undefined-variable-test/gno.mod b/gnovm/tests/integ/undefined-variable-test/gno.mod new file mode 100644 index 00000000000..0a75f00e83f --- /dev/null +++ b/gnovm/tests/integ/undefined-variable-test/gno.mod @@ -0,0 +1 @@ +module gno.land/tests/undefined-test \ No newline at end of file diff --git a/gnovm/tests/integ/undefined-variable-test/undefined_variables_test.gno b/gnovm/tests/integ/undefined-variable-test/undefined_variables_test.gno new file mode 100644 index 00000000000..0afdec36ef3 --- /dev/null +++ b/gnovm/tests/integ/undefined-variable-test/undefined_variables_test.gno @@ -0,0 +1,7 @@ +package main + +import "testing" + +func TestUndefinedVariables(t *testing.T) { + println("hello world: " + toto) +} From ec5373207a92909e71f4eed4bbb0b7b9f821d835 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 9 Oct 2023 11:04:09 +0200 Subject: [PATCH 08/26] chore: lint Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/lint.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index db0e12e388c..db9ea13d453 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -150,7 +150,7 @@ func execLint(cfg *lintCfg, args []string, io *commands.IO) error { tm.RunFiles(testfiles.Files...) }() - // TODO: Add more checkers here + // TODO: Add more checkers } if hasError && cfg.setExitStatus != 0 { From 145c78f1b7c22159b19645a68e7d8a09219f4e3f Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 20 Oct 2023 12:25:38 -0400 Subject: [PATCH 09/26] feat: add preprocess stack error Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/lint.go | 33 ++++++++++++++++--------- gnovm/pkg/gnolang/debug.go | 44 +++++++++++++++++++++++++++++++++ gnovm/pkg/gnolang/preprocess.go | 15 ++++++++--- 3 files changed, 77 insertions(+), 15 deletions(-) diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index db9ea13d453..9b6fbc96c85 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -103,19 +103,28 @@ func execLint(cfg *lintCfg, args []string, io *commands.IO) error { handleError := func() { // Errors here mostly come from: gnovm/pkg/gnolang/preprocess.go if r := recover(); r != nil { - if recErr, ok := r.(error); ok { - parsedError := strings.TrimSpace(recErr.Error()) - parsedError = strings.TrimPrefix(parsedError, pkgPath+"/") - matches := reParseRecover.FindStringSubmatch(parsedError) - if len(matches) > 0 { - addIssue(lintIssue{ - Code: lintGnoError, - Confidence: 1, - Location: fmt.Sprintf("%s:%s", matches[1], matches[2]), - Msg: strings.TrimSpace(matches[3]), - }) - } + var err error + switch v := r.(type) { + case *gno.PreprocessStackError: + err = v.Err + case error: + err = v + default: + panic(r) } + + parsedError := strings.TrimSpace(err.Error()) + parsedError = strings.TrimPrefix(parsedError, pkgPath+"/") + matches := reParseRecover.FindStringSubmatch(parsedError) + if len(matches) > 0 { + addIssue(lintIssue{ + Code: lintGnoError, + Confidence: 1, + Location: fmt.Sprintf("%s:%s", matches[1], matches[2]), + Msg: strings.TrimSpace(matches[3]), + }) + } + } } diff --git a/gnovm/pkg/gnolang/debug.go b/gnovm/pkg/gnolang/debug.go index cb21da12ef2..901cfb23ea9 100644 --- a/gnovm/pkg/gnolang/debug.go +++ b/gnovm/pkg/gnolang/debug.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "os" + "strings" "time" // Ignore pprof import, as the server does not @@ -76,6 +77,40 @@ func (d debugging) Errorf(format string, args ...interface{}) { } } +// PreprocessStackError wraps a processing error along with its associated +// preprocessing stack for enhanced error reporting. +type PreprocessStackError struct { + Err error + Stack []BlockNode +} + +// PreprocessError returns the encapsulated error message. +func (p *PreprocessStackError) PreprocessError() string { + return p.Err.Error() +} + +// PreprocessStack produces a string representation of the preprocessing stack +// trace that was associated with the error occurrence. +func (p *PreprocessStackError) PreprocessStack() 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 *PreprocessStackError) Error() string { + var err strings.Builder + fmt.Fprintln(&err, p.PreprocessError()+":") + fmt.Fprintln(&err, "--- preprocess stack ---") + fmt.Fprint(&err, p.PreprocessStack()) + fmt.Fprintf(&err, "------------------------") + return err.String() +} + // ---------------------------------------- // Exposed errors accessors // File tests may access debug errors. @@ -107,3 +142,12 @@ func DisableDebug() { func EnableDebug() { enabled = true } + +type DebugError struct { + error + Metas interface{} +} + +func (d *DebugError) Error() string { + return d.Error() +} diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 21bde8e06a0..3bebb15f9e2 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -159,13 +159,22 @@ func Preprocess(store Store, ctx BlockNode, n Node) Node { 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(&PreprocessStackError{ + Err: err, + Stack: stack, + }) } }() if debug { From 89befe1bc8c1504dc2928f0fdaf59837fdc20060 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 20 Oct 2023 12:26:20 -0400 Subject: [PATCH 10/26] fix: add lint file error testscripts Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- .../testdata/gno_test/lint_file_error.txtar | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 gnovm/cmd/gno/testdata/gno_test/lint_file_error.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_file_error.txtar b/gnovm/cmd/gno/testdata/gno_test/lint_file_error.txtar new file mode 100644 index 00000000000..9482eeb1f4f --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_test/lint_file_error.txtar @@ -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). From f461b2d7f7318626cbafd21d0db314804d51f084 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 20 Oct 2023 12:57:40 -0400 Subject: [PATCH 11/26] chore: lint Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/lint.go | 1 - 1 file changed, 1 deletion(-) diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index 9b6fbc96c85..0206fffddb4 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -124,7 +124,6 @@ func execLint(cfg *lintCfg, args []string, io *commands.IO) error { Msg: strings.TrimSpace(matches[3]), }) } - } } From a628c940860f9b60b35fd617212998c9519ba9e0 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 23 Oct 2023 11:52:56 -0400 Subject: [PATCH 12/26] fix: repl test Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/pkg/gnolang/debug.go | 2 +- gnovm/pkg/repl/repl_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gnovm/pkg/gnolang/debug.go b/gnovm/pkg/gnolang/debug.go index 901cfb23ea9..efd1e12dee1 100644 --- a/gnovm/pkg/gnolang/debug.go +++ b/gnovm/pkg/gnolang/debug.go @@ -104,7 +104,7 @@ func (p *PreprocessStackError) PreprocessStack() string { // the actual error followed by its associated preprocessing stack. func (p *PreprocessStackError) Error() string { var err strings.Builder - fmt.Fprintln(&err, p.PreprocessError()+":") + fmt.Fprintf(&err, "%s:", p.PreprocessError()) fmt.Fprintln(&err, "--- preprocess stack ---") fmt.Fprint(&err, p.PreprocessStack()) fmt.Fprintf(&err, "------------------------") diff --git a/gnovm/pkg/repl/repl_test.go b/gnovm/pkg/repl/repl_test.go index 3c4d1f7c6c6..fa644a663b0 100644 --- a/gnovm/pkg/repl/repl_test.go +++ b/gnovm/pkg/repl/repl_test.go @@ -69,7 +69,7 @@ var fixtures = []struct { CodeSteps: []step{ { Line: "importasdasd", - Error: "recovered from panic: test/test1.gno:7: name importasdasd not declared", + Error: "test/test1.gno:7: name importasdasd not declared", }, { Line: "var a := 1", From 984841ef8d259e4be0f7b89341a61ddf4151f0aa Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 23 Oct 2023 15:00:56 -0400 Subject: [PATCH 13/26] fix: handle preprocess error on test Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/lint.go | 50 ++++++++++++++++----------------- gnovm/pkg/gnolang/debug.go | 28 +++++++++--------- gnovm/pkg/gnolang/preprocess.go | 6 ++-- gnovm/tests/file.go | 11 ++++++-- 4 files changed, 50 insertions(+), 45 deletions(-) diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index 0206fffddb4..a2c1f65be78 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -102,29 +102,33 @@ func execLint(cfg *lintCfg, args []string, io *commands.IO) error { handleError := func() { // Errors here mostly come from: gnovm/pkg/gnolang/preprocess.go - if r := recover(); r != nil { - var err error - switch v := r.(type) { - case *gno.PreprocessStackError: - err = v.Err - case error: - err = v - default: - panic(r) - } + r := recover() + if r == nil { + return + } - parsedError := strings.TrimSpace(err.Error()) - parsedError = strings.TrimPrefix(parsedError, pkgPath+"/") - matches := reParseRecover.FindStringSubmatch(parsedError) - if len(matches) > 0 { - addIssue(lintIssue{ - Code: lintGnoError, - Confidence: 1, - Location: fmt.Sprintf("%s:%s", matches[1], matches[2]), - Msg: strings.TrimSpace(matches[3]), - }) - } + var err error + switch verr := r.(type) { + case *gno.PreprocessError: + err = verr.Unwrap() + case error: + err = verr + default: + panic(r) + } + + parsedError := strings.TrimSpace(err.Error()) + parsedError = strings.TrimPrefix(parsedError, pkgPath+"/") + matches := reParseRecover.FindStringSubmatch(parsedError) + if len(matches) > 0 { + addIssue(lintIssue{ + Code: lintGnoError, + Confidence: 1, + Location: fmt.Sprintf("%s:%s", matches[1], matches[2]), + Msg: strings.TrimSpace(matches[3]), + }) } + } // Run the machine on the target package @@ -165,10 +169,6 @@ func execLint(cfg *lintCfg, args []string, io *commands.IO) error { os.Exit(cfg.setExitStatus) } - if verbose { - fmt.Println("no lint errors") - } - return nil } diff --git a/gnovm/pkg/gnolang/debug.go b/gnovm/pkg/gnolang/debug.go index efd1e12dee1..2adbae89ef6 100644 --- a/gnovm/pkg/gnolang/debug.go +++ b/gnovm/pkg/gnolang/debug.go @@ -77,24 +77,24 @@ func (d debugging) Errorf(format string, args ...interface{}) { } } -// PreprocessStackError wraps a processing error along with its associated +// PreprocessError wraps a processing error along with its associated // preprocessing stack for enhanced error reporting. -type PreprocessStackError struct { - Err error - Stack []BlockNode +type PreprocessError struct { + err error + stack []BlockNode } -// PreprocessError returns the encapsulated error message. -func (p *PreprocessStackError) PreprocessError() string { - return p.Err.Error() +// Unwrap returns the encapsulated error message. +func (p *PreprocessError) Unwrap() error { + return p.err } -// PreprocessStack produces a string representation of the preprocessing stack +// Stack produces a string representation of the preprocessing stack // trace that was associated with the error occurrence. -func (p *PreprocessStackError) PreprocessStack() string { +func (p *PreprocessError) Stack() string { var stacktrace strings.Builder - for i := len(p.Stack) - 1; i >= 0; i-- { - sbn := p.Stack[i] + 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() @@ -102,11 +102,11 @@ func (p *PreprocessStackError) PreprocessStack() string { // Error consolidates and returns the full error message, including // the actual error followed by its associated preprocessing stack. -func (p *PreprocessStackError) Error() string { +func (p *PreprocessError) Error() string { var err strings.Builder - fmt.Fprintf(&err, "%s:", p.PreprocessError()) + fmt.Fprintf(&err, "%s:\n", p.Unwrap()) fmt.Fprintln(&err, "--- preprocess stack ---") - fmt.Fprint(&err, p.PreprocessStack()) + fmt.Fprint(&err, p.Stack()) fmt.Fprintf(&err, "------------------------") return err.String() } diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 3bebb15f9e2..067151cdb60 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -171,9 +171,9 @@ func Preprocess(store Store, ctx BlockNode, n Node) Node { } // Re-throw the error after wrapping it with the preprocessing stack information. - panic(&PreprocessStackError{ - Err: err, - Stack: stack, + panic(&PreprocessError{ + err: err, + stack: stack, }) } }() diff --git a/gnovm/tests/file.go b/gnovm/tests/file.go index 451bf0677dc..332942000c1 100644 --- a/gnovm/tests/file.go +++ b/gnovm/tests/file.go @@ -240,15 +240,20 @@ func RunFileTest(rootDir string, path string, opts ...RunFileTestOption) error { if pnc == nil { panic(fmt.Sprintf("fail on %s: got nil error, want: %q", path, errWanted)) } + errstr := "" - if tv, ok := pnc.(*gno.TypedValue); ok { - errstr = tv.Sprint(m) - } else { + switch v := pnc.(type) { + case *gno.TypedValue: + errstr = v.Sprint(m) + case *gno.PreprocessError: + errstr = v.Unwrap().Error() + default: errstr = strings.TrimSpace(fmt.Sprintf("%v", pnc)) } if errstr != errWanted { panic(fmt.Sprintf("fail on %s: got %q, want: %q", path, errstr, errWanted)) } + // NOTE: ignores any gno.GetDebugErrors(). gno.ClearDebugErrors() return nil // nothing more to do. From d40cf0f7d036a4ae5c0edb6e29aed43797cfbbf2 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Wed, 25 Oct 2023 09:51:27 -0400 Subject: [PATCH 14/26] chore: lint Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/lint.go | 1 - 1 file changed, 1 deletion(-) diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index a2c1f65be78..0f62ce2315e 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -128,7 +128,6 @@ func execLint(cfg *lintCfg, args []string, io *commands.IO) error { Msg: strings.TrimSpace(matches[3]), }) } - } // Run the machine on the target package From c2c96565424592f9789ff60d577059beff443bee Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 4 Dec 2023 12:46:31 +0100 Subject: [PATCH 15/26] chore: lint Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/lint.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index 050e0d48a81..61a28575fbf 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -88,9 +88,9 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { // Use `RunMemPackage` to detect basic package errors var ( - stdout = io.Out - stdin = io.In - stderr = io.Err + stdout = io.Out() + stdin = io.In() + stderr = io.Err() testStore = tests.TestStore( rootDir, "", From 14251e6134d6e006ef6abfeba757ed164f8196ea Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:03:54 +0100 Subject: [PATCH 16/26] fix: gno run test for preprocess stack Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/run_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gnovm/cmd/gno/run_test.go b/gnovm/cmd/gno/run_test.go index e439a6bad8d..575798a78dc 100644 --- a/gnovm/cmd/gno/run_test.go +++ b/gnovm/cmd/gno/run_test.go @@ -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 From 89ae08fb8e64da1a4401af2081b11a8e04340ccf Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 7 Dec 2023 12:49:43 +0100 Subject: [PATCH 17/26] chore: encapsulate error catch for better readability Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/lint.go | 89 +++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index 61a28575fbf..c5038564d63 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -86,55 +86,15 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { }) } - // Use `RunMemPackage` to detect basic package errors - var ( - stdout = io.Out() - stdin = io.In() - stderr = io.Err() - - testStore = tests.TestStore( + // 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, ) - reParseRecover = regexp.MustCompile(`^(.+):(\d+): ?(.*)$`) - ) - - handleError := func() { - // Errors 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 - default: - panic(r) - } - - parsedError := strings.TrimSpace(err.Error()) - parsedError = strings.TrimPrefix(parsedError, pkgPath+"/") - matches := reParseRecover.FindStringSubmatch(parsedError) - if len(matches) > 0 { - addIssue(lintIssue{ - Code: lintGnoError, - Confidence: 1, - Location: fmt.Sprintf("%s:%s", matches[1], matches[2]), - Msg: strings.TrimSpace(matches[3]), - }) - } - } - - // Run the machine on the target package - func() { - defer handleError() - memPkg := gno.ReadMemPackage(filepath.Dir(pkgPath), pkgPath) tm := tests.TestMachine(testStore, stdout, memPkg.Name) @@ -160,7 +120,7 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { } tm.RunFiles(testfiles.Files...) - }() + }) // TODO: Add more checkers } @@ -172,6 +132,45 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { 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 + default: + panic(r) + } + + parsedError := strings.TrimSpace(err.Error()) + parsedError = strings.TrimPrefix(parsedError, pkgPath+"/") + reParseRecover := regexp.MustCompile(`^(.+):(\d+): ?(.*)$`) + matches := reParseRecover.FindStringSubmatch(parsedError) + if len(matches) == 0 { + return + } + + addIssue(lintIssue{ + Code: lintGnoError, + Confidence: 1, + Location: fmt.Sprintf("%s:%s", matches[1], matches[2]), + Msg: strings.TrimSpace(matches[3]), + }) + }() + + action() +} + type lintCode int const ( From 11f101a4bc51c3a95d0e73989d4775905c9850e8 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 7 Dec 2023 13:17:42 +0100 Subject: [PATCH 18/26] fix: lint Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/lint.go | 1 - gnovm/pkg/gnolang/debug.go | 9 --------- 2 files changed, 10 deletions(-) diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index c5038564d63..e23fb241e90 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -154,7 +154,6 @@ func catchRuntimeError(pkgPath string, addIssue func(issue lintIssue), action fu parsedError := strings.TrimSpace(err.Error()) parsedError = strings.TrimPrefix(parsedError, pkgPath+"/") - reParseRecover := regexp.MustCompile(`^(.+):(\d+): ?(.*)$`) matches := reParseRecover.FindStringSubmatch(parsedError) if len(matches) == 0 { return diff --git a/gnovm/pkg/gnolang/debug.go b/gnovm/pkg/gnolang/debug.go index 2adbae89ef6..c6e39dfaa5a 100644 --- a/gnovm/pkg/gnolang/debug.go +++ b/gnovm/pkg/gnolang/debug.go @@ -142,12 +142,3 @@ func DisableDebug() { func EnableDebug() { enabled = true } - -type DebugError struct { - error - Metas interface{} -} - -func (d *DebugError) Error() string { - return d.Error() -} From d0f72458e5ce206d7c3861cc9d6d02814c5ab402 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 7 Dec 2023 14:07:54 +0100 Subject: [PATCH 19/26] fix: bad rebase Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/lint_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gnovm/cmd/gno/lint_test.go b/gnovm/cmd/gno/lint_test.go index 4ff9a608e87..d700467965d 100644 --- a/gnovm/cmd/gno/lint_test.go +++ b/gnovm/cmd/gno/lint_test.go @@ -11,10 +11,10 @@ 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"}, + 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"}, + 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/"}, From 8b828b0e4f24580df9bf13abb249739ce70e81aa Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:51:29 +0100 Subject: [PATCH 20/26] fix: global lint Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/lint.go | 34 +++++++++++++++++++++++---------- gnovm/cmd/gno/util.go | 44 +++++++++++++------------------------------ 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index e23fb241e90..11d78797bbc 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "flag" "fmt" "os" @@ -95,7 +96,13 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { tests.ImportModeStdlibsOnly, ) - memPkg := gno.ReadMemPackage(filepath.Dir(pkgPath), pkgPath) + 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 @@ -113,7 +120,8 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { continue // Skip empty files } - if strings.HasSuffix(mfile.Name, "_test.gno") { + // 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) } @@ -148,23 +156,29 @@ func catchRuntimeError(pkgPath string, addIssue func(issue lintIssue), action fu 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) == 0 { - return + 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(lintIssue{ - Code: lintGnoError, - Confidence: 1, - Location: fmt.Sprintf("%s:%s", matches[1], matches[2]), - Msg: strings.TrimSpace(matches[3]), - }) + addIssue(issue) }() action() diff --git a/gnovm/cmd/gno/util.go b/gnovm/cmd/gno/util.go index a8e835d4759..e699ab81888 100644 --- a/gnovm/cmd/gno/util.go +++ b/gnovm/cmd/gno/util.go @@ -13,6 +13,7 @@ import ( "github.com/gnolang/gno/gnovm/pkg/gnoenv" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/pkg/gnomod" ) func isGnoFile(f fs.DirEntry) bool { @@ -65,40 +66,21 @@ func gnoPackagesFromArgs(args []string) ([]string, error) { if !info.IsDir() { paths = append(paths, arg) } else { - // if the passed arg is a dir, then we'll recursively walk the dir - // and look for directories containing at least one .gno file. + absdir, err := filepath.Abs(arg) + if err != nil { + return nil, fmt.Errorf("unable to get absolute path of %q: %w", arg, err) + } - visited := map[string]bool{} // used to run the builder only once per folder. - err = filepath.WalkDir(arg, func(curpath string, f fs.DirEntry, err error) error { - if err != nil { - return fmt.Errorf("%s: walk dir: %w", arg, err) - } - if f.IsDir() { - return nil // skip - } - if !isGnoFile(f) { - return nil // skip - } + pkglist, err := gnomod.ListPkgs(absdir) + if err != nil { + return nil, fmt.Errorf("unable to list packages of %q: %w", absdir, err) + } - parentDir := filepath.Dir(curpath) - if _, found := visited[parentDir]; found { - return nil + for _, pkg := range pkglist { + // Skip empty and draft pkgs + if !pkg.Draft && pkg.Name != "" { + paths = append(paths, pkg.Dir) } - visited[parentDir] = true - - pkg := parentDir - if !filepath.IsAbs(parentDir) { - // cannot use path.Join or filepath.Join, because we need - // to ensure that ./ is the prefix to pass to go build. - // if not absolute. - pkg = "./" + parentDir - } - - paths = append(paths, pkg) - return nil - }) - if err != nil { - return nil, err } } } From 4b69afee8754a5bbcf8646960e17d303c5e90714 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 7 Dec 2023 16:41:48 +0100 Subject: [PATCH 21/26] fix: linter Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/lint.go | 2 +- gnovm/cmd/gno/util.go | 44 ++++++++++++++++++++++++++++++------------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index 11d78797bbc..26d4b7994b1 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -98,7 +98,7 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { targetPath := pkgPath info, err := os.Stat(pkgPath) - if err != nil && !info.IsDir() { + if err == nil && !info.IsDir() { targetPath = filepath.Dir(pkgPath) } diff --git a/gnovm/cmd/gno/util.go b/gnovm/cmd/gno/util.go index e699ab81888..a8e835d4759 100644 --- a/gnovm/cmd/gno/util.go +++ b/gnovm/cmd/gno/util.go @@ -13,7 +13,6 @@ import ( "github.com/gnolang/gno/gnovm/pkg/gnoenv" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" - "github.com/gnolang/gno/gnovm/pkg/gnomod" ) func isGnoFile(f fs.DirEntry) bool { @@ -66,21 +65,40 @@ func gnoPackagesFromArgs(args []string) ([]string, error) { if !info.IsDir() { paths = append(paths, arg) } else { - absdir, err := filepath.Abs(arg) - if err != nil { - return nil, fmt.Errorf("unable to get absolute path of %q: %w", arg, err) - } + // if the passed arg is a dir, then we'll recursively walk the dir + // and look for directories containing at least one .gno file. - pkglist, err := gnomod.ListPkgs(absdir) - if err != nil { - return nil, fmt.Errorf("unable to list packages of %q: %w", absdir, err) - } + visited := map[string]bool{} // used to run the builder only once per folder. + err = filepath.WalkDir(arg, func(curpath string, f fs.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("%s: walk dir: %w", arg, err) + } + if f.IsDir() { + return nil // skip + } + if !isGnoFile(f) { + return nil // skip + } - for _, pkg := range pkglist { - // Skip empty and draft pkgs - if !pkg.Draft && pkg.Name != "" { - paths = append(paths, pkg.Dir) + parentDir := filepath.Dir(curpath) + if _, found := visited[parentDir]; found { + return nil } + visited[parentDir] = true + + pkg := parentDir + if !filepath.IsAbs(parentDir) { + // cannot use path.Join or filepath.Join, because we need + // to ensure that ./ is the prefix to pass to go build. + // if not absolute. + pkg = "./" + parentDir + } + + paths = append(paths, pkg) + return nil + }) + if err != nil { + return nil, err } } } From af22e2348878ef7bf6b0dfaa745bdcbd6b626f2d Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 7 Dec 2023 17:02:10 +0100 Subject: [PATCH 22/26] feat: add update scripts Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/test_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gnovm/cmd/gno/test_test.go b/gnovm/cmd/gno/test_test.go index b1dcfb21d29..98320f41cc9 100644 --- a/gnovm/cmd/gno/test_test.go +++ b/gnovm/cmd/gno/test_test.go @@ -1,6 +1,8 @@ package main import ( + "os" + "strconv" "testing" "github.com/gnolang/gno/gnovm/pkg/integration" @@ -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 { From 0540dc3d8c4345ef45abff3cae83556b0bce7178 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 7 Dec 2023 17:09:31 +0100 Subject: [PATCH 23/26] chore: update golden file Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/testdata/gno_test/lint_bad_import.txtar | 2 +- gnovm/cmd/gno/testdata/gno_test/lint_file_error.txtar | 2 +- gnovm/cmd/gno/testdata/gno_test/lint_not_declared.txtar | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_bad_import.txtar b/gnovm/cmd/gno/testdata/gno_test/lint_bad_import.txtar index 946e1bcba35..d27a0bd0c34 100644 --- a/gnovm/cmd/gno/testdata/gno_test/lint_bad_import.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/lint_bad_import.txtar @@ -16,4 +16,4 @@ func main() { -- stdout.golden -- -- stderr.golden -- -bad_file.gno:1: unknown import path python (code=2). +./bad_file.gno:1: unknown import path python (code=2). diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_file_error.txtar b/gnovm/cmd/gno/testdata/gno_test/lint_file_error.txtar index 9482eeb1f4f..d4975987e5e 100644 --- a/gnovm/cmd/gno/testdata/gno_test/lint_file_error.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/lint_file_error.txtar @@ -17,4 +17,4 @@ func TestIHaveSomeError() { -- stdout.golden -- -- stderr.golden -- -i_have_error_test.gno:6: name undefined_variable not declared (code=2). +./i_have_error_test.gno:6: name undefined_variable not declared (code=2). diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_not_declared.txtar b/gnovm/cmd/gno/testdata/gno_test/lint_not_declared.txtar index 7bd74a34855..035c638cf21 100644 --- a/gnovm/cmd/gno/testdata/gno_test/lint_not_declared.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/lint_not_declared.txtar @@ -17,4 +17,4 @@ func main() { -- stdout.golden -- -- stderr.golden -- -bad_file.gno:6: name hello not declared (code=2). +./bad_file.gno:6: name hello not declared (code=2). From 3fb3b71c745e905f8203b160bb470c11c7b53830 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 8 Dec 2023 16:38:19 +0100 Subject: [PATCH 24/26] fix: normalize lint error Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/lint.go | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index 26d4b7994b1..68a5808b309 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -42,8 +42,10 @@ func newLintCmd(io commands.IO) *commands.Command { } 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") } @@ -140,7 +142,27 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { return nil } -var reParseRecover = regexp.MustCompile(`^(.+):(\d+): ?(.*)$`) +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) +} + +// 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() { @@ -171,10 +193,11 @@ func catchRuntimeError(pkgPath string, addIssue func(issue lintIssue), action fu matches := reParseRecover.FindStringSubmatch(parsedError) if len(matches) == 4 { - issue.Location = fmt.Sprintf("%s:%s", matches[1], matches[2]) + 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", parsedError) + issue.Location = fmt.Sprintf("%s:0", filepath.Clean(pkgPath)) issue.Msg = err.Error() } From 3a444e05c0ec5feeade5da18367da58b8465f989 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 8 Dec 2023 16:38:45 +0100 Subject: [PATCH 25/26] chore: update contributing md Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- CONTRIBUTING.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6456c9b9a2f..3405614386a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -104,6 +104,18 @@ To use *gofumpt* instead of *gofmt*, as hinted in the comment, you may either ha cexpr system('go run -modfile /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 :make +``` + ### ViM Support (with LSP) There is an experimental and unofficial [Gno Language Server](https://github.com/jdkato/gnols) @@ -172,7 +184,31 @@ Additionaly, 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 From fdc0c10fa1af6ef5fdafa6ef664a8583f2edcf2b Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:22:59 +0100 Subject: [PATCH 26/26] fix: update golden files Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gnovm/cmd/gno/testdata/gno_test/lint_bad_import.txtar | 2 +- gnovm/cmd/gno/testdata/gno_test/lint_file_error.txtar | 2 +- gnovm/cmd/gno/testdata/gno_test/lint_not_declared.txtar | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_bad_import.txtar b/gnovm/cmd/gno/testdata/gno_test/lint_bad_import.txtar index d27a0bd0c34..946e1bcba35 100644 --- a/gnovm/cmd/gno/testdata/gno_test/lint_bad_import.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/lint_bad_import.txtar @@ -16,4 +16,4 @@ func main() { -- stdout.golden -- -- stderr.golden -- -./bad_file.gno:1: unknown import path python (code=2). +bad_file.gno:1: unknown import path python (code=2). diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_file_error.txtar b/gnovm/cmd/gno/testdata/gno_test/lint_file_error.txtar index d4975987e5e..9482eeb1f4f 100644 --- a/gnovm/cmd/gno/testdata/gno_test/lint_file_error.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/lint_file_error.txtar @@ -17,4 +17,4 @@ func TestIHaveSomeError() { -- stdout.golden -- -- stderr.golden -- -./i_have_error_test.gno:6: name undefined_variable not declared (code=2). +i_have_error_test.gno:6: name undefined_variable not declared (code=2). diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_not_declared.txtar b/gnovm/cmd/gno/testdata/gno_test/lint_not_declared.txtar index 035c638cf21..7bd74a34855 100644 --- a/gnovm/cmd/gno/testdata/gno_test/lint_not_declared.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/lint_not_declared.txtar @@ -17,4 +17,4 @@ func main() { -- stdout.golden -- -- stderr.golden -- -./bad_file.gno:6: name hello not declared (code=2). +bad_file.gno:6: name hello not declared (code=2).