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 @@

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 @@
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 @@
})
}

// 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 120 in gnovm/cmd/gno/lint.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/lint.go#L120

Added line #L120 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
}

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)

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

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/lint.go#L159-L162

Added lines #L159 - L162 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 {
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()
}

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

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/lint.go#L177-L179

Added lines #L177 - L179 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).
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 @@
"fmt"
"net/http"
"os"
"strings"
"time"

// Ignore pprof import, as the server does not
Expand Down Expand Up @@ -76,6 +77,40 @@
}
}

// 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()

Check warning on line 100 in gnovm/pkg/gnolang/debug.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/debug.go#L94-L100

Added lines #L94 - L100 were not covered by tests
}

// 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()

Check warning on line 111 in gnovm/pkg/gnolang/debug.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/debug.go#L105-L111

Added lines #L105 - L111 were not covered by tests
}

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

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())

Check warning on line 167 in gnovm/pkg/gnolang/preprocess.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/preprocess.go#L167

Added line #L167 was not covered by tests
} 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
Loading