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 15 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
88 changes: 86 additions & 2 deletions gnovm/cmd/gno/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ 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"
)
Expand Down Expand Up @@ -70,7 +74,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 @@ -81,12 +85,90 @@ func execLint(cfg *lintCfg, args []string, io *commands.IO) error {
})
}

// TODO: add more checkers
gfanton marked this conversation as resolved.
Show resolved Hide resolved
// Use `RunMemPackage` to detect basic package errors
var (
stdout = io.Out
stdin = io.In
stderr = 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
if r := recover(); r != nil {
gfanton marked this conversation as resolved.
Show resolved Hide resolved
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]),
})
}
}
}

// Run the machine on the target package
func() {
thehowl marked this conversation as resolved.
Show resolved Hide resolved
defer handleError()

memPkg := gno.ReadMemPackage(filepath.Dir(pkgPath), pkgPath)
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
}

if strings.HasSuffix(mfile.Name, "_test.gno") {
// Keep only test files
testfiles.AddFiles(n)
}
}

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

// TODO: Add more checkers
}

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

if verbose {
fmt.Println("no lint errors")
}

gfanton marked this conversation as resolved.
Show resolved Hide resolved
return nil
}

Expand All @@ -95,6 +177,8 @@ type lintCode int
const (
lintUnknown lintCode = 0
lintNoGnoMod lintCode = iota
lintGnoError

// TODO: add new linter codes here.
)

Expand Down
8 changes: 6 additions & 2 deletions gnovm/cmd/gno/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,19 @@ 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).",
thehowl marked this conversation as resolved.
Show resolved Hide resolved
}, {
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?
Expand Down
4 changes: 4 additions & 0 deletions gnovm/cmd/gno/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"

Expand Down Expand Up @@ -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 :)
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).
44 changes: 44 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{}) {
}
}

// PreprocessStackError wraps a processing error along with its associated
// preprocessing stack for enhanced error reporting.
type PreprocessStackError struct {
Err error
Stack []BlockNode
}
gfanton marked this conversation as resolved.
Show resolved Hide resolved

// 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.Fprintf(&err, "%s:", 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.
Expand Down Expand Up @@ -107,3 +142,12 @@ func DisableDebug() {
func EnableDebug() {
enabled = true
}

type DebugError struct {
error
Metas interface{}
}

func (d *DebugError) Error() string {
return d.Error()
}
gfanton marked this conversation as resolved.
Show resolved Hide resolved
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 ---")
thehowl marked this conversation as resolved.
Show resolved Hide resolved
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(&PreprocessStackError{
Err: err,
Stack: stack,
})
}
}()
if debug {
Expand Down
2 changes: 1 addition & 1 deletion gnovm/pkg/repl/repl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions gnovm/tests/integ/package-not-declared/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/tests/nodeclared
Loading
Loading