diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index 2a0a4cf1106..a7d1a6ea6f4 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -750,15 +750,15 @@ func (pl *pkgsLoader) LoadPackage(modroot string, path, name string) error { if err != nil { return fmt.Errorf("unable to read package at %q: %w", currentPkg.Dir, err) } - imports, err := packages.Imports(pkg) + imports, err := packages.Imports(pkg, nil) if err != nil { return fmt.Errorf("unable to load package imports in %q: %w", currentPkg.Dir, err) } for _, imp := range imports { - if imp == currentPkg.Name || gnolang.IsStdlib(imp) { + if imp.PkgPath == currentPkg.Name || gnolang.IsStdlib(imp.PkgPath) { continue } - currentPkg.Imports = append(currentPkg.Imports, imp) + currentPkg.Imports = append(currentPkg.Imports, imp.PkgPath) } } diff --git a/gnovm/cmd/gno/download_deps.go b/gnovm/cmd/gno/download_deps.go index d19de9dd338..5a8c50be20b 100644 --- a/gnovm/cmd/gno/download_deps.go +++ b/gnovm/cmd/gno/download_deps.go @@ -25,13 +25,13 @@ func downloadDeps(io commands.IO, pkgDir string, gnoMod *gnomod.File, fetcher pk if err != nil { return fmt.Errorf("read package at %q: %w", pkgDir, err) } - imports, err := packages.Imports(pkg) + imports, err := packages.Imports(pkg, nil) if err != nil { return fmt.Errorf("read imports at %q: %w", pkgDir, err) } for _, pkgPath := range imports { - resolved := gnoMod.Resolve(module.Version{Path: pkgPath}) + resolved := gnoMod.Resolve(module.Version{Path: pkgPath.PkgPath}) resolvedPkgPath := resolved.Path if !isRemotePkgPath(resolvedPkgPath) { diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index a3e7f5310e1..8a1256772df 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -7,7 +7,7 @@ import ( "fmt" "go/scanner" "go/types" - "io" + goio "io" "os" "path/filepath" "regexp" @@ -97,6 +97,11 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { hasError := false + bs, ts := test.Store( + rootDir, false, + nopReader{}, goio.Discard, goio.Discard, + ) + for _, pkgPath := range pkgPaths { if verbose { io.ErrPrintln(pkgPath) @@ -120,12 +125,6 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { hasError = true } - stdout, stdin, stderr := io.Out(), io.In(), io.Err() - _, testStore := test.Store( - rootDir, false, - stdin, stdout, stderr, - ) - memPkg, err := gno.ReadMemPackage(pkgPath, pkgPath) if err != nil { io.ErrPrintln(issueFromError(pkgPath, err).String()) @@ -133,22 +132,34 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { continue } - // Run type checking - if gmFile == nil || !gmFile.Draft { - foundErr, err := lintTypeCheck(io, memPkg, testStore) - if err != nil { - io.ErrPrintln(err) - hasError = true - } else if foundErr { - hasError = true - } - } else if verbose { - io.ErrPrintfln("%s: module is draft, skipping type check", pkgPath) + // Perform imports using the parent store. + if err := test.LoadImports(ts, memPkg); err != nil { + io.ErrPrintln(issueFromError(pkgPath, err).String()) + hasError = true + continue } // Handle runtime errors hasRuntimeErr := catchRuntimeError(pkgPath, io.Err(), func() { - tm := test.Machine(testStore, stdout, memPkg.Path) + // Wrap in cache wrap so execution of the linter doesn't impact + // other packages. + cw := bs.CacheWrap() + gs := ts.BeginTransaction(cw, cw) + + // Run type checking + if gmFile == nil || !gmFile.Draft { + foundErr, err := lintTypeCheck(io, memPkg, gs) + if err != nil { + io.ErrPrintln(err) + hasError = true + } else if foundErr { + hasError = true + } + } else if verbose { + io.ErrPrintfln("%s: module is draft, skipping type check", pkgPath) + } + + tm := test.Machine(gs, goio.Discard, memPkg.Path) defer tm.Release() // Check package @@ -253,7 +264,7 @@ func guessSourcePath(pkg, source string) string { // XXX: Ideally, error handling should encapsulate location details within a dedicated error type. var reParseRecover = regexp.MustCompile(`^([^:]+)((?::(?:\d+)){1,2}):? *(.*)$`) -func catchRuntimeError(pkgPath string, stderr io.WriteCloser, action func()) (hasError bool) { +func catchRuntimeError(pkgPath string, stderr goio.WriteCloser, action func()) (hasError bool) { defer func() { // Errors catched here mostly come from: gnovm/pkg/gnolang/preprocess.go r := recover() @@ -307,3 +318,7 @@ func issueFromError(pkgPath string, err error) lintIssue { } return issue } + +type nopReader struct{} + +func (nopReader) Read(p []byte) (int, error) { return 0, goio.EOF } diff --git a/gnovm/cmd/gno/mod.go b/gnovm/cmd/gno/mod.go index f762b070fe4..5479d934ce6 100644 --- a/gnovm/cmd/gno/mod.go +++ b/gnovm/cmd/gno/mod.go @@ -362,15 +362,12 @@ func getImportToFilesMap(pkgPath string) (map[string][]string, error) { if err != nil { return nil, err } - imports, _, err := packages.FileImports(filename, string(data)) + imports, err := packages.FileImports(filename, string(data), nil) if err != nil { return nil, err } for _, imp := range imports { - if imp.Error != nil { - return nil, err - } m[imp.PkgPath] = append(m[imp.PkgPath], filename) } } diff --git a/gnovm/cmd/gno/testdata/lint/bad_import.txtar b/gnovm/cmd/gno/testdata/lint/bad_import.txtar index b5edbdd0223..e2c0431443c 100644 --- a/gnovm/cmd/gno/testdata/lint/bad_import.txtar +++ b/gnovm/cmd/gno/testdata/lint/bad_import.txtar @@ -19,5 +19,4 @@ module gno.land/p/test -- stdout.golden -- -- stderr.golden -- -bad_file.gno:3:8: could not import python (import not found: python) (code=4) bad_file.gno:3:8: unknown import path python (code=2) diff --git a/gnovm/pkg/doc/dirs.go b/gnovm/pkg/doc/dirs.go index eadbec7d464..b287fd20708 100644 --- a/gnovm/pkg/doc/dirs.go +++ b/gnovm/pkg/doc/dirs.go @@ -105,10 +105,14 @@ func packageImportsRecursive(root string, pkgPath string) []string { pkg = &gnovm.MemPackage{} } - res, err := packages.Imports(pkg) + resRaw, err := packages.Imports(pkg, nil) if err != nil { // ignore packages with invalid imports - res = nil + resRaw = nil + } + res := make([]string, len(resRaw)) + for idx, imp := range resRaw { + res[idx] = imp.PkgPath } entries, err := os.ReadDir(root) @@ -127,7 +131,7 @@ func packageImportsRecursive(root string, pkgPath string) []string { for _, imp := range sub { if !slices.Contains(res, imp) { - res = append(res, imp) + res = append(res, imp) //nolint:makezero } } } diff --git a/gnovm/pkg/gnomod/pkg.go b/gnovm/pkg/gnomod/pkg.go index 35f52e3dded..a0831d494b0 100644 --- a/gnovm/pkg/gnomod/pkg.go +++ b/gnovm/pkg/gnomod/pkg.go @@ -5,7 +5,6 @@ import ( "io/fs" "os" "path/filepath" - "slices" "strings" "github.com/gnolang/gno/gnovm" @@ -122,16 +121,19 @@ func ListPkgs(root string) (PkgList, error) { pkg = &gnovm.MemPackage{} } - imports, err := packages.Imports(pkg) + importsRaw, err := packages.Imports(pkg, nil) if err != nil { // ignore imports on error - imports = []string{} + importsRaw = nil + } + imports := make([]string, 0, len(importsRaw)) + for _, imp := range importsRaw { + // remove self and standard libraries from imports + if imp.PkgPath != gnoMod.Module.Mod.Path && + !gnolang.IsStdlib(imp.PkgPath) { + imports = append(imports, imp.PkgPath) + } } - - // remove self and standard libraries from imports - imports = slices.DeleteFunc(imports, func(imp string) bool { - return imp == gnoMod.Module.Mod.Path || gnolang.IsStdlib(imp) - }) pkgs = append(pkgs, Pkg{ Dir: path, diff --git a/gnovm/pkg/packages/imports.go b/gnovm/pkg/packages/imports.go index e72f37276db..201965bc588 100644 --- a/gnovm/pkg/packages/imports.go +++ b/gnovm/pkg/packages/imports.go @@ -13,9 +13,10 @@ import ( ) // Imports returns the list of gno imports from a [gnovm.MemPackage]. -func Imports(pkg *gnovm.MemPackage) ([]string, error) { - allImports := make([]string, 0) - seen := make(map[string]struct{}) +// fset is optional. +func Imports(pkg *gnovm.MemPackage, fset *token.FileSet) ([]FileImport, error) { + allImports := make([]FileImport, 0, 16) + seen := make(map[string]struct{}, 16) for _, file := range pkg.Files { if !strings.HasSuffix(file.Name, ".gno") { continue @@ -23,50 +24,54 @@ func Imports(pkg *gnovm.MemPackage) ([]string, error) { if strings.HasSuffix(file.Name, "_filetest.gno") { continue } - imports, _, err := FileImports(file.Name, file.Body) + imports, err := FileImports(file.Name, file.Body, fset) if err != nil { return nil, err } for _, im := range imports { - if im.Error != nil { - return nil, err - } if _, ok := seen[im.PkgPath]; ok { continue } - allImports = append(allImports, im.PkgPath) + allImports = append(allImports, im) seen[im.PkgPath] = struct{}{} } } - sort.Strings(allImports) + sort.Slice(allImports, func(i, j int) bool { + return allImports[i].PkgPath < allImports[j].PkgPath + }) return allImports, nil } +// FileImport represents an import type FileImport struct { PkgPath string Spec *ast.ImportSpec - Error error } // FileImports returns the list of gno imports in the given file src. // The given filename is only used when recording position information. -func FileImports(filename string, src string) ([]*FileImport, *token.FileSet, error) { - fs := token.NewFileSet() - f, err := parser.ParseFile(fs, filename, src, parser.ImportsOnly) +func FileImports(filename string, src string, fset *token.FileSet) ([]FileImport, error) { + if fset == nil { + fset = token.NewFileSet() + } + f, err := parser.ParseFile(fset, filename, src, parser.ImportsOnly) if err != nil { - return nil, nil, err + return nil, err } - res := make([]*FileImport, len(f.Imports)) + res := make([]FileImport, len(f.Imports)) for i, im := range f.Imports { - fi := FileImport{Spec: im} importPath, err := strconv.Unquote(im.Path.Value) if err != nil { - fi.Error = fmt.Errorf("%v: unexpected invalid import path: %v", fs.Position(im.Pos()).String(), im.Path.Value) - } else { - fi.PkgPath = importPath + // should not happen - parser.ParseFile should already ensure we get + // a valid string literal here. + panic(fmt.Errorf("%v: unexpected invalid import path: %v", fset.Position(im.Pos()).String(), im.Path.Value)) + } + + res[i] = FileImport{ + PkgPath: importPath, + Spec: im, } - res[i] = &fi } - return res, fs, nil + return res, nil } diff --git a/gnovm/pkg/packages/imports_test.go b/gnovm/pkg/packages/imports_test.go index 14808dcbd6f..3750aa9108c 100644 --- a/gnovm/pkg/packages/imports_test.go +++ b/gnovm/pkg/packages/imports_test.go @@ -120,8 +120,12 @@ func TestImports(t *testing.T) { pkg, err := gnolang.ReadMemPackage(tmpDir, "test") require.NoError(t, err) - imports, err := Imports(pkg) + imports, err := Imports(pkg, nil) require.NoError(t, err) + importsStrings := make([]string, len(imports)) + for idx, imp := range imports { + importsStrings[idx] = imp.PkgPath + } - require.Equal(t, expected, imports) + require.Equal(t, expected, importsStrings) } diff --git a/gnovm/pkg/test/filetest.go b/gnovm/pkg/test/filetest.go index 12bc9ed7f28..648cbb2a759 100644 --- a/gnovm/pkg/test/filetest.go +++ b/gnovm/pkg/test/filetest.go @@ -175,12 +175,20 @@ type runResult struct { } func (opts *TestOptions) runTest(m *gno.Machine, pkgPath, filename string, content []byte) (rr runResult) { + pkgName := gno.Name(pkgPath[strings.LastIndexByte(pkgPath, '/')+1:]) + // Eagerly load imports. // This is executed using opts.Store, rather than the transaction store; // it allows us to only have to load the imports once (and re-use the cached // versions). Running the tests in separate "transactions" means that they // don't get the parent store dirty. - if err := LoadImports(opts.TestStore, filename, content); err != nil { + if err := LoadImports(opts.TestStore, &gnovm.MemPackage{ + Name: string(pkgName), + Path: pkgPath, + Files: []*gnovm.MemFile{ + {Name: filename, Body: string(content)}, + }, + }); err != nil { // NOTE: we perform this here, so we can capture the runResult. return runResult{Error: err.Error()} } @@ -210,7 +218,6 @@ func (opts *TestOptions) runTest(m *gno.Machine, pkgPath, filename string, conte }() // Use last element after / (works also if slash is missing). - pkgName := gno.Name(pkgPath[strings.LastIndexByte(pkgPath, '/')+1:]) if !gno.IsRealmPath(pkgPath) { // Simple case - pure package. pn := gno.NewPackageNode(pkgName, pkgPath, &gno.FileSet{}) diff --git a/gnovm/pkg/test/imports.go b/gnovm/pkg/test/imports.go index 731bf9756dd..8b24fdeaa77 100644 --- a/gnovm/pkg/test/imports.go +++ b/gnovm/pkg/test/imports.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "go/token" "io" "math/big" "os" @@ -12,6 +13,7 @@ import ( "strings" "time" + "github.com/gnolang/gno/gnovm" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/pkg/packages" teststdlibs "github.com/gnolang/gno/gnovm/tests/stdlibs" @@ -214,13 +216,13 @@ func (e *stackWrappedError) String() string { return fmt.Sprintf("%v\nstack:\n%v", e.err, string(e.stack)) } -// LoadImports parses the given file and attempts to retrieve all pure packages +// LoadImports parses the given MemPackage and attempts to retrieve all pure packages // from the store. This is mostly useful for "eager import loading", whereby all // imports are pre-loaded in a permanent store, so that the tests can use // ephemeral transaction stores. -func LoadImports(store gno.Store, filename string, content []byte) (err error) { +func LoadImports(store gno.Store, memPkg *gnovm.MemPackage) (err error) { defer func() { - // This is slightly different from the handling below; we do not have a + // This is slightly different from other similar error handling; we do not have a // machine to work with, as this comes from an import; so we need // "machine-less" alternatives. (like v.String instead of v.Sprint) if r := recover(); r != nil { @@ -239,14 +241,12 @@ func LoadImports(store gno.Store, filename string, content []byte) (err error) { } }() - imports, fset, err := packages.FileImports(filename, string(content)) + fset := token.NewFileSet() + imports, err := packages.Imports(memPkg, fset) if err != nil { return err } for _, imp := range imports { - if imp.Error != nil { - return imp.Error - } if gno.IsRealmPath(imp.PkgPath) { // Don't eagerly load realms. // Realms persist state and can change the state of other realms in initialization. diff --git a/gnovm/pkg/test/test.go b/gnovm/pkg/test/test.go index 3ea3d4bc9bd..6d483d2fc9a 100644 --- a/gnovm/pkg/test/test.go +++ b/gnovm/pkg/test/test.go @@ -9,7 +9,6 @@ import ( "io" "math" "os" - "path" "path/filepath" "runtime/debug" "strconv" @@ -182,6 +181,11 @@ func Test(memPkg *gnovm.MemPackage, fsDir string, opts *TestOptions) error { var errs error + // Eagerly load imports. + if err := LoadImports(opts.TestStore, memPkg); err != nil { + return err + } + // Stands for "test", "integration test", and "filetest". // "integration test" are the test files with `package xxx_test` (they are // not necessarily integration tests, it's just for our internal reference.) @@ -424,10 +428,6 @@ func parseMemPackageTests(store gno.Store, memPkg *gnovm.MemPackage) (tset, itse continue // skip this file. } - if err := LoadImports(store, path.Join(memPkg.Path, mfile.Name), []byte(mfile.Body)); err != nil { - errs = multierr.Append(errs, err) - } - n, err := gno.ParseFile(mfile.Name, mfile.Body) if err != nil { errs = multierr.Append(errs, err)