diff --git a/gnovm/cmd/gno/lint_test.go b/gnovm/cmd/gno/lint_test.go index 4589fc55f92..61aa1f3c8fb 100644 --- a/gnovm/cmd/gno/lint_test.go +++ b/gnovm/cmd/gno/lint_test.go @@ -10,18 +10,36 @@ func TestLintApp(t *testing.T) { { args: []string{"lint"}, errShouldBe: "flag: help requested", - }, { + }, + { args: []string{"lint", "../../tests/integ/run_main/"}, stderrShouldContain: "./../../tests/integ/run_main: gno.mod file not found in current or any parent directory (code=1)", errShouldBe: "exit code: 1", - }, { + }, + { args: []string{"lint", "../../tests/integ/undefined_variable_test/undefined_variables_test.gno"}, stderrShouldContain: "undefined_variables_test.gno:6:28: name toto not declared (code=2)", errShouldBe: "exit code: 1", - }, { + }, + { args: []string{"lint", "../../tests/integ/package_not_declared/main.gno"}, stderrShouldContain: "main.gno:4:2: name fmt not declared (code=2)", errShouldBe: "exit code: 1", + }, + { + args: []string{"lint", "../../tests/integ/several_lint_errors/main.gno"}, + stderrShouldContain: "../../tests/integ/several_lint_errors/main.gno:5:5: expected ';', found example (code=2).\n../../tests/integ/several_lint_errors/main.gno:6:2: expected '}', found 'EOF' (code=2).\n", + errShouldBe: "exit code: 1", + }, + { + args: []string{"lint", "../../tests/integ/several_files_multiple_errors/main.gno"}, + stderrShouldContain: "../../tests/integ/several_files_multiple_errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2).\n../../tests/integ/several_files_multiple_errors/file2.gno:5:1: expected type, found '}' (code=2).\n../../tests/integ/several_files_multiple_errors/main.gno:5:5: expected ';', found example (code=2).\n../../tests/integ/several_files_multiple_errors/main.gno:6:2: expected '}', found 'EOF' (code=2).\n", + errShouldBe: "exit code: 1", + }, + { + args: []string{"lint", "../../tests/integ/run_main/"}, + stderrShouldContain: "./../../tests/integ/run_main: missing 'gno.mod' file (code=1).", + errShouldBe: "exit code: 1", }, { args: []string{"lint", "../../tests/integ/several-lint-errors/main.gno"}, stderrShouldContain: "../../tests/integ/several-lint-errors/main.gno:5:5: expected ';', found example (code=2)\n../../tests/integ/several-lint-errors/main.gno:6", @@ -41,7 +59,8 @@ func TestLintApp(t *testing.T) { }, { args: []string{"lint", "../../tests/integ/minimalist_gnomod/"}, // TODO: raise an error because there is a gno.mod, but no .gno files - }, { + }, + { args: []string{"lint", "../../tests/integ/invalid_module_name/"}, // TODO: raise an error because gno.mod is invalid }, { diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index 0496d37ed72..c4e9b33ad32 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -2169,7 +2169,40 @@ func (x *BasicLitExpr) GetInt() int { return i } -var rePkgName = regexp.MustCompile(`^[a-z][a-z0-9_]+$`) +var invalidPkgNames = map[string]struct{}{ + "internal": {}, + "crypto": {}, +} + +func validatePkgPath(pkgPath, pkgName string) { + parts := strings.Split(pkgPath, "/") + lastPart := parts[len(parts)-1] + + // testing env + if lastPart == "." { + return + } + + if _, ok := invalidPkgNames[lastPart]; ok { + panic(fmt.Sprintf("cannot create package with invalid name %q", pkgName)) + } + + if strings.HasSuffix(lastPart, "_test") { + return + } + + validatePkgName(lastPart) + + if len(parts) > 1 { + return + } + + if !IsStdlib(pkgPath) { + panic(fmt.Sprintf("cannot create package with invalid path %q", pkgPath)) + } +} + +var rePkgName = regexp.MustCompile(`^[a-z0-9][a-z0-9_]+$`) // TODO: consider length restrictions. // If this function is changed, ReadMemPackage's documentation should be updated accordingly. diff --git a/gnovm/pkg/gnolang/nodes_test.go b/gnovm/pkg/gnolang/nodes_test.go index 2c3a03d8c09..9523d4894fb 100644 --- a/gnovm/pkg/gnolang/nodes_test.go +++ b/gnovm/pkg/gnolang/nodes_test.go @@ -2,6 +2,9 @@ package gnolang_test import ( "math" + "os" + "path/filepath" + "strings" "testing" "github.com/gnolang/gno/gnovm/pkg/gnolang" @@ -42,3 +45,285 @@ func TestStaticBlock_Define2_MaxNames(t *testing.T) { // This one should panic because the maximum number of names has been reached. staticBlock.Define2(false, gnolang.Name("a"), gnolang.BoolType, gnolang.TypedValue{T: gnolang.BoolType}) } + +func TestReadMemPackage(t *testing.T) { + t.Parallel() + tests := []struct { + name string + files map[string]string // map[filename]content + pkgPath string + shouldPanic bool + wantPkgName string + }{ + { + name: "valid package - math", + files: map[string]string{ + "math.gno": `package math + func Add(a, b int) int { return a + b }`, + }, + pkgPath: "std/math", + shouldPanic: false, + wantPkgName: "math", + }, + { + name: "valid package - bytealg", + files: map[string]string{ + "bytealg.gno": `package bytealg + func Compare(a, b []byte) int { return 0 }`, + }, + pkgPath: "std/bytealg", + shouldPanic: false, + wantPkgName: "bytealg", + }, + { + name: "valid package - sha256", + files: map[string]string{ + "sha256.gno": `package sha256 + func Sum256(data []byte) [32]byte { var sum [32]byte; return sum }`, + }, + pkgPath: "crypto/sha256", + shouldPanic: false, + wantPkgName: "sha256", + }, + { + name: "nested package - foo/bar", + files: map[string]string{ + "bar.gno": `package bar + func DoSomething() {}`, + }, + pkgPath: "gno.land/foo/bar", + shouldPanic: false, + wantPkgName: "bar", + }, + { + name: "package with README and LICENSE", + files: map[string]string{ + "math.gno": `package math + func Add(a, b int) int { return a + b }`, + "README.md": "# Math Package", + "LICENSE": "MIT License", + }, + pkgPath: "std/math", + shouldPanic: false, + wantPkgName: "math", + }, + { + name: "stdlib with .go files", + files: map[string]string{ + "math.gno": `package math + func Add(a, b int) int { return a + b }`, + "native.go": `package math + func NativeAdd(a, b int) int { return a + b }`, + }, + pkgPath: "std/math", + shouldPanic: false, + wantPkgName: "math", + }, + { + name: "stdlib with rejected .gen.go files", + files: map[string]string{ + "math.gno": `package math + func Add(a, b int) int { return a + b }`, + "generated.gen.go": `package math + func GeneratedFunc() {}`, + }, + pkgPath: "std/math", + shouldPanic: false, + wantPkgName: "math", + }, + { + name: "valid nested package - gnoland/xxx/foo/bar", + files: map[string]string{ + "bar.gno": `package bar + func DoSomething() string { return "hello" }`, + }, + pkgPath: "gno.land/xxx/foo/bar", + shouldPanic: false, + wantPkgName: "bar", + }, + { + name: "valid package - gno.land/p/demo/tests", + files: map[string]string{ + "tests.gno": `package tests + const World = "world"`, + }, + pkgPath: "gno.land/p/demo/tests", + shouldPanic: false, + wantPkgName: "tests", + }, + { + name: "foo/bar with empty foo directory", + files: map[string]string{ + "bar.gno": `package bar + func DoSomething() string { return "hello" }`, + }, + pkgPath: "gno.land/r/foo/bar", + shouldPanic: false, + wantPkgName: "bar", + }, + { + name: "invalid package - internal", + files: map[string]string{ + "internal.gno": `package internal + func someFunc() {}`, + }, + pkgPath: "std/internal", + shouldPanic: true, + }, + { + name: "invalid package - crypto", + files: map[string]string{ + "crypto.gno": `package crypto + func Hash(data []byte) []byte { return nil }`, + }, + pkgPath: "std/crypto", + shouldPanic: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + tmpDir, err := os.MkdirTemp("", "test-*") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + for fname, content := range tt.files { + fpath := filepath.Join(tmpDir, fname) + dir := filepath.Dir(fpath) + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(fpath, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + } + + if tt.shouldPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("expected panic for package %s", tt.pkgPath) + } + }() + } + + memPkg := gnolang.ReadMemPackage(tmpDir, tt.pkgPath) + + if !tt.shouldPanic { + if memPkg == nil { + t.Fatal("expected non-nil MemPackage") + } + + if memPkg.Name != tt.wantPkgName { + t.Errorf("got package name %q, want %q", memPkg.Name, tt.wantPkgName) + } + + if memPkg.Path != tt.pkgPath { + t.Errorf("got package path %q, want %q", memPkg.Path, tt.pkgPath) + } + + expectedFiles := 0 + for fname := range tt.files { + if strings.HasSuffix(fname, ".gen.go") { + continue + } + if strings.HasSuffix(fname, ".gno") || + fname == "README.md" || + fname == "LICENSE" || + (gnolang.IsStdlib(tt.pkgPath) && strings.HasSuffix(fname, ".go")) { + expectedFiles++ + } + } + if len(memPkg.Files) != expectedFiles { + t.Errorf("got %d files, want %d", len(memPkg.Files), expectedFiles) + } + } + }) + } +} + +func TestInjectNativeMethod(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + files map[string]string + pkgPath string + shouldPanic bool + }{ + { + name: "inject to valid package", + files: map[string]string{ + "foo.gno": `package foo + func RegularFunc() string { return "hello" }`, + }, + pkgPath: "gno.land/r/test/foo", + shouldPanic: false, + }, + { + name: "inject to invalid package name - crypto", + files: map[string]string{ + "crypto.gno": `package crypto + func Hash(data []byte) []byte { return nil }`, + }, + pkgPath: "std/crypto", + shouldPanic: true, + }, + { + name: "inject to invalid package name - internal", + files: map[string]string{ + "internal.gno": `package internal + func someFunc() {}`, + }, + pkgPath: "std/internal", + shouldPanic: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + tmpDir, err := os.MkdirTemp("", "test-*") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + for fname, content := range tt.files { + fpath := filepath.Join(tmpDir, fname) + dir := filepath.Dir(fpath) + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(fpath, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + } + + defer func() { + if r := recover(); r != nil { + if !tt.shouldPanic { + t.Errorf("unexpected panic: %v", r) + } + } else if tt.shouldPanic { + t.Error("expected panic, but got none") + } + }() + + memPkg := gnolang.ReadMemPackage(tmpDir, tt.pkgPath) + fset := gnolang.ParseMemPackage(memPkg) + pkgNode := gnolang.NewPackageNode(gnolang.Name(memPkg.Name), tt.pkgPath, fset) + + pkgNode.DefineNative("NativeMethod", + gnolang.FieldTypeExprs{}, + gnolang.FieldTypeExprs{}, + func(m *gnolang.Machine) {}, + ) + }) + } +} diff --git a/gnovm/tests/integ/several-files-multiple-errors/file2.gno b/gnovm/tests/integ/several_files_multiple_errors/file2.gno similarity index 100% rename from gnovm/tests/integ/several-files-multiple-errors/file2.gno rename to gnovm/tests/integ/several_files_multiple_errors/file2.gno diff --git a/gnovm/tests/integ/several-files-multiple-errors/gno.mod b/gnovm/tests/integ/several_files_multiple_errors/gno.mod similarity index 100% rename from gnovm/tests/integ/several-files-multiple-errors/gno.mod rename to gnovm/tests/integ/several_files_multiple_errors/gno.mod diff --git a/gnovm/tests/integ/several-files-multiple-errors/main.gno b/gnovm/tests/integ/several_files_multiple_errors/main.gno similarity index 100% rename from gnovm/tests/integ/several-files-multiple-errors/main.gno rename to gnovm/tests/integ/several_files_multiple_errors/main.gno diff --git a/gnovm/tests/integ/several-lint-errors/gno.mod b/gnovm/tests/integ/several_lint_errors/gno.mod similarity index 100% rename from gnovm/tests/integ/several-lint-errors/gno.mod rename to gnovm/tests/integ/several_lint_errors/gno.mod diff --git a/gnovm/tests/integ/several-lint-errors/main.gno b/gnovm/tests/integ/several_lint_errors/main.gno similarity index 100% rename from gnovm/tests/integ/several-lint-errors/main.gno rename to gnovm/tests/integ/several_lint_errors/main.gno