From 0572de4c016f78a2abcd05d4794673d34bf9f80a Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 2 Jul 2024 18:43:08 +0200 Subject: [PATCH] feat(gnovm): add `gno fmt` command (#2156) Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- .github/workflows/examples.yml | 16 + .github/workflows/gnofmt_template.yml | 24 ++ .github/workflows/gnovm.yml | 6 + examples/Makefile | 9 +- examples/gno.land/p/demo/avl/tree_test.gno | 4 +- examples/gno.land/p/demo/bf/bf_test.gno | 5 +- .../gno.land/p/demo/cford32/cford32_test.gno | 1 - .../p/demo/grc/exts/vault/vault_filetest.gno | 1 - .../grc/grc1155/basic_grc1155_token_test.gno | 3 - examples/gno.land/p/demo/grc/grc1155/gno.mod | 1 - .../p/demo/grc/grc721/basic_nft_test.gno | 3 - examples/gno.land/p/demo/grc/grc721/gno.mod | 1 - .../demo/grc/grc721/grc721_metadata_test.gno | 1 - .../p/demo/grc/grc721/grc721_royalty_test.gno | 2 - .../p/demo/grc/grc721/igrc721_royalty.gno | 4 +- examples/gno.land/p/demo/groups/groups.gno | 4 +- examples/gno.land/p/demo/int256/absolute.gno | 4 +- .../gno.land/p/demo/int256/arithmetic.gno | 4 +- .../gno.land/p/demo/int256/bitwise_test.gno | 6 +- examples/gno.land/p/demo/int256/cmp_test.gno | 4 +- .../gno.land/p/demo/int256/conversion.gno | 4 +- .../gno.land/p/demo/int256/int256_test.gno | 4 +- examples/gno.land/p/demo/json/buffer_test.gno | 4 +- examples/gno.land/p/demo/json/decode_test.gno | 1 - examples/gno.land/p/demo/json/encode_test.gno | 4 +- examples/gno.land/p/demo/json/parser_test.gno | 5 +- examples/gno.land/p/demo/json/path_test.gno | 4 +- .../p/demo/math_eval/int32/int32_test.gno | 7 +- .../gno.land/p/demo/merkle/merkle_test.gno | 1 - examples/gno.land/p/demo/mux/response.gno | 4 +- examples/gno.land/p/demo/mux/router_test.gno | 5 +- examples/gno.land/p/demo/stack/stack_test.gno | 4 +- .../p/demo/tamagotchi/z0_filetest.gno | 1 - .../gno.land/p/demo/uint256/bitwise_test.gno | 4 +- .../p/demo/uint256/conversion_test.gno | 4 +- .../r/demo/art/millipede/millipede_test.gno | 4 +- examples/gno.land/r/demo/echo/echo_test.gno | 4 +- examples/gno.land/r/demo/foo20/foo20_test.gno | 1 - .../r/demo/keystore/keystore_test.gno | 1 - examples/gno.land/r/demo/microblog/gno.mod | 1 - .../r/demo/microblog/microblog_test.gno | 3 - .../r/demo/tamagotchi/z0_filetest.gno | 3 - examples/gno.land/r/demo/tests/gno.mod | 5 +- examples/gno.land/r/demo/tests/tests_test.gno | 2 - .../gno.land/r/demo/users/z_0_b_filetest.gno | 2 - .../gno.land/r/demo/users/z_6_filetest.gno | 1 - .../r/gnoland/ghverify/contract_test.gno | 1 - .../r/x/manfred_outfmt/outfmt_test.gno | 1 + .../upgrade_d/v1/z_filetest.gno | 2 - .../upgrade_d/v2/z_filetest.gno | 4 +- .../r/x/map_delete/map_delete_test.gno | 4 +- .../evaluation_test.gno | 2 - .../r/x/nir1218_evaluation_proposal/tally.gno | 4 +- .../r/x/nir1218_evaluation_proposal/vote.gno | 4 +- gnovm/Makefile | 5 +- gnovm/cmd/gno/fmt.go | 291 +++++++++++++++ gnovm/cmd/gno/fmt_test.go | 18 + gnovm/cmd/gno/lint.go | 2 +- gnovm/cmd/gno/main.go | 2 +- gnovm/cmd/gno/test_test.go | 29 -- gnovm/cmd/gno/testdata/gno_fmt/empty.txtar | 10 + .../testdata/gno_fmt/import_cleaning.txtar | 30 ++ gnovm/cmd/gno/testdata/gno_fmt/include.txtar | 31 ++ .../gno/testdata/gno_fmt/multi_import.txtar | 96 +++++ .../testdata/gno_fmt/noimport_format.txtar | 39 ++ .../gno/testdata/gno_fmt/parse_error.txtar | 19 + .../gno/testdata/gno_fmt/shadow_import.txtar | 53 +++ .../bad_import.txtar} | 0 .../file_error.txtar} | 0 .../file_error_txtar} | 0 .../no_error.txtar} | 0 .../no_gnomod.txtar} | 0 .../not_declared.txtar} | 0 .../testdata/gno_test/fmt_write_import.txtar | 25 ++ gnovm/cmd/gno/testdata_test.go | 46 +++ gnovm/cmd/gno/transpile.go | 2 +- gnovm/cmd/gno/util.go | 64 +++- gnovm/pkg/gnofmt/package.go | 156 ++++++++ gnovm/pkg/gnofmt/processes_test.go | 96 +++++ gnovm/pkg/gnofmt/processor.go | 334 ++++++++++++++++++ gnovm/pkg/gnofmt/resolver.go | 110 ++++++ gnovm/pkg/gnofmt/resolver_mock.go | 86 +++++ gnovm/pkg/gnofmt/utils.go | 69 ++++ gnovm/stdlibs/crypto/ed25519/ed25519_test.gno | 1 - gnovm/stdlibs/crypto/sha256/sha256_test.gno | 1 - gnovm/stdlibs/hash/marshal_test.gno | 3 - gnovm/stdlibs/io/io_test.gno | 1 - gnovm/stdlibs/math/rand/example_test.gno | 2 - gnovm/stdlibs/math/rand/rand_test.gno | 1 - gnovm/stdlibs/math/rand/regress_test.gno | 9 - gnovm/stdlibs/net/url/url_test.gno | 3 +- gnovm/stdlibs/testing/random_test.gno | 1 - gnovm/stdlibs/unicode/utf16/utf16_test.gno | 1 - .../tests/integ/unformated/missing_import.gno | 3 + 94 files changed, 1659 insertions(+), 188 deletions(-) create mode 100644 .github/workflows/gnofmt_template.yml create mode 100644 gnovm/cmd/gno/fmt.go create mode 100644 gnovm/cmd/gno/fmt_test.go delete mode 100644 gnovm/cmd/gno/test_test.go create mode 100644 gnovm/cmd/gno/testdata/gno_fmt/empty.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_fmt/import_cleaning.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_fmt/include.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_fmt/multi_import.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_fmt/noimport_format.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_fmt/parse_error.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_fmt/shadow_import.txtar rename gnovm/cmd/gno/testdata/{gno_test/lint_bad_import.txtar => gno_lint/bad_import.txtar} (100%) rename gnovm/cmd/gno/testdata/{gno_test/lint_file_error.txtar => gno_lint/file_error.txtar} (100%) rename gnovm/cmd/gno/testdata/{gno_test/lint_file_error_txtar => gno_lint/file_error_txtar} (100%) rename gnovm/cmd/gno/testdata/{gno_test/lint_no_error.txtar => gno_lint/no_error.txtar} (100%) rename gnovm/cmd/gno/testdata/{gno_test/lint_no_gnomod.txtar => gno_lint/no_gnomod.txtar} (100%) rename gnovm/cmd/gno/testdata/{gno_test/lint_not_declared.txtar => gno_lint/not_declared.txtar} (100%) create mode 100644 gnovm/cmd/gno/testdata/gno_test/fmt_write_import.txtar create mode 100644 gnovm/cmd/gno/testdata_test.go create mode 100644 gnovm/pkg/gnofmt/package.go create mode 100644 gnovm/pkg/gnofmt/processes_test.go create mode 100644 gnovm/pkg/gnofmt/processor.go create mode 100644 gnovm/pkg/gnofmt/resolver.go create mode 100644 gnovm/pkg/gnofmt/resolver_mock.go create mode 100644 gnovm/pkg/gnofmt/utils.go create mode 100644 gnovm/tests/integ/unformated/missing_import.gno diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 54b3cf7d635..d11710344b1 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -71,6 +71,22 @@ jobs: - run: make lint -C ./examples # TODO: consider running lint on every other directories, maybe in "warning" mode? # TODO: track coverage + fmt: + strategy: + fail-fast: false + matrix: + goversion: [ "1.22.x" ] + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.goversion }} + - run: | + make fmt -C ./examples + # Check if there are changes after running make fmt + git diff --exit-code || (echo "Some gno files are not formatted, please run 'make fmt'." && exit 1) mod-tidy: strategy: fail-fast: false diff --git a/.github/workflows/gnofmt_template.yml b/.github/workflows/gnofmt_template.yml new file mode 100644 index 00000000000..1ba66d0fbe3 --- /dev/null +++ b/.github/workflows/gnofmt_template.yml @@ -0,0 +1,24 @@ +on: + workflow_call: + inputs: + path: + required: true + type: string + go-version: + required: true + type: string + +jobs: + fmt: + runs-on: ubuntu-latest + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go-version }} + - name: Checkout code + uses: actions/checkout@v4 + - name: Fmt + env: + GNOFMT_PATH: ${{ inputs.path }} + run: go run ./gnovm/cmd/gno fmt -v -diff $GNOFMT_PATH diff --git a/.github/workflows/gnovm.yml b/.github/workflows/gnovm.yml index 8212ec5871d..102262b1413 100644 --- a/.github/workflows/gnovm.yml +++ b/.github/workflows/gnovm.yml @@ -19,3 +19,9 @@ jobs: modulepath: "gnovm" secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }} + fmt: + name: Run Gno Fmt + uses: ./.github/workflows/gnofmt_template.yml + with: + path: "gnovm/stdlibs/..." + go-version: "1.22.x" diff --git a/examples/Makefile b/examples/Makefile index 4894e28a1bb..b9485ff7c01 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -56,14 +56,9 @@ clean: find . \( -name "*.gno.gen.go" -or -name ".*.gno.gen_test.go" \) -delete .PHONY: fmt -GOFMT_FLAGS ?= -w +GNOFMT_FLAGS ?= -w fmt: - go run -modfile ../misc/devdeps/go.mod mvdan.cc/gofumpt $(GOFMT_FLAGS) `find . -name "*.gno"` - -.PHONY: imports -GOIMPORTS_FLAGS ?= -w -imports: - $(rundep) golang.org/x/tools/cmd/goimports $(GOIMPORTS_FLAGS) . + go run ../gnovm/cmd/gno fmt $(GNOFMT_FLAGS) ./... .PHONY: tidy tidy: diff --git a/examples/gno.land/p/demo/avl/tree_test.gno b/examples/gno.land/p/demo/avl/tree_test.gno index 76b108933c6..8f6efcc5bad 100644 --- a/examples/gno.land/p/demo/avl/tree_test.gno +++ b/examples/gno.land/p/demo/avl/tree_test.gno @@ -1,8 +1,6 @@ package avl -import ( - "testing" -) +import "testing" func TestNewTree(t *testing.T) { tree := NewTree() diff --git a/examples/gno.land/p/demo/bf/bf_test.gno b/examples/gno.land/p/demo/bf/bf_test.gno index 1be4a86ac33..c55ad3a316c 100644 --- a/examples/gno.land/p/demo/bf/bf_test.gno +++ b/examples/gno.land/p/demo/bf/bf_test.gno @@ -1,9 +1,6 @@ package bf -import ( - "bytes" - "testing" -) +import "testing" func TestExecuteBrainfuck(t *testing.T) { testCases := []struct { diff --git a/examples/gno.land/p/demo/cford32/cford32_test.gno b/examples/gno.land/p/demo/cford32/cford32_test.gno index 1a17d64c856..6f269c1b9ed 100644 --- a/examples/gno.land/p/demo/cford32/cford32_test.gno +++ b/examples/gno.land/p/demo/cford32/cford32_test.gno @@ -6,7 +6,6 @@ import ( "fmt" "io" "math" - "strconv" "strings" "testing" ) diff --git a/examples/gno.land/p/demo/grc/exts/vault/vault_filetest.gno b/examples/gno.land/p/demo/grc/exts/vault/vault_filetest.gno index d888a3b5f93..34d38afef1f 100644 --- a/examples/gno.land/p/demo/grc/exts/vault/vault_filetest.gno +++ b/examples/gno.land/p/demo/grc/exts/vault/vault_filetest.gno @@ -2,7 +2,6 @@ package main import ( "std" - "time" "gno.land/p/demo/grc/exts/vault" "gno.land/p/demo/grc/grc20" diff --git a/examples/gno.land/p/demo/grc/grc1155/basic_grc1155_token_test.gno b/examples/gno.land/p/demo/grc/grc1155/basic_grc1155_token_test.gno index d7abf8e1153..701f5c95afe 100644 --- a/examples/gno.land/p/demo/grc/grc1155/basic_grc1155_token_test.gno +++ b/examples/gno.land/p/demo/grc/grc1155/basic_grc1155_token_test.gno @@ -1,11 +1,8 @@ package grc1155 import ( - "fmt" "std" "testing" - - "gno.land/p/demo/users" ) const dummyURI = "ipfs://xyz" diff --git a/examples/gno.land/p/demo/grc/grc1155/gno.mod b/examples/gno.land/p/demo/grc/grc1155/gno.mod index b8db3675cf0..8f3f36019f9 100644 --- a/examples/gno.land/p/demo/grc/grc1155/gno.mod +++ b/examples/gno.land/p/demo/grc/grc1155/gno.mod @@ -3,5 +3,4 @@ module gno.land/p/demo/grc/grc1155 require ( gno.land/p/demo/avl v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/p/demo/grc/grc721/basic_nft_test.gno b/examples/gno.land/p/demo/grc/grc721/basic_nft_test.gno index 70255c5e9d1..9f4a1e12792 100644 --- a/examples/gno.land/p/demo/grc/grc721/basic_nft_test.gno +++ b/examples/gno.land/p/demo/grc/grc721/basic_nft_test.gno @@ -3,9 +3,6 @@ package grc721 import ( "std" "testing" - - "gno.land/p/demo/testutils" - "gno.land/p/demo/users" ) var ( diff --git a/examples/gno.land/p/demo/grc/grc721/gno.mod b/examples/gno.land/p/demo/grc/grc721/gno.mod index 1eada28e4ee..061ea2988c3 100644 --- a/examples/gno.land/p/demo/grc/grc721/gno.mod +++ b/examples/gno.land/p/demo/grc/grc721/gno.mod @@ -4,5 +4,4 @@ require ( gno.land/p/demo/avl v0.0.0-latest gno.land/p/demo/testutils v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/p/demo/grc/grc721/grc721_metadata_test.gno b/examples/gno.land/p/demo/grc/grc721/grc721_metadata_test.gno index b7ca6932fe1..ec152f5d20d 100644 --- a/examples/gno.land/p/demo/grc/grc721/grc721_metadata_test.gno +++ b/examples/gno.land/p/demo/grc/grc721/grc721_metadata_test.gno @@ -5,7 +5,6 @@ import ( "testing" "gno.land/p/demo/testutils" - "gno.land/p/demo/users" ) func TestSetMetadata(t *testing.T) { diff --git a/examples/gno.land/p/demo/grc/grc721/grc721_royalty_test.gno b/examples/gno.land/p/demo/grc/grc721/grc721_royalty_test.gno index 8c7bb33caa5..3087fe35d13 100644 --- a/examples/gno.land/p/demo/grc/grc721/grc721_royalty_test.gno +++ b/examples/gno.land/p/demo/grc/grc721/grc721_royalty_test.gno @@ -5,8 +5,6 @@ import ( "testing" "gno.land/p/demo/testutils" - "gno.land/p/demo/ufmt" - "gno.land/p/demo/users" ) func TestSetTokenRoyalty(t *testing.T) { diff --git a/examples/gno.land/p/demo/grc/grc721/igrc721_royalty.gno b/examples/gno.land/p/demo/grc/grc721/igrc721_royalty.gno index a8a74ea15cc..c4603d6328e 100644 --- a/examples/gno.land/p/demo/grc/grc721/igrc721_royalty.gno +++ b/examples/gno.land/p/demo/grc/grc721/igrc721_royalty.gno @@ -1,8 +1,6 @@ package grc721 -import ( - "std" -) +import "std" // IGRC2981 follows the Ethereum standard type IGRC2981 interface { diff --git a/examples/gno.land/p/demo/groups/groups.gno b/examples/gno.land/p/demo/groups/groups.gno index d7315ac982c..fcf77dd2a74 100644 --- a/examples/gno.land/p/demo/groups/groups.gno +++ b/examples/gno.land/p/demo/groups/groups.gno @@ -1,8 +1,6 @@ package groups -import ( - "gno.land/r/demo/boards" -) +import "gno.land/r/demo/boards" // TODO implement something and test. type Group struct { diff --git a/examples/gno.land/p/demo/int256/absolute.gno b/examples/gno.land/p/demo/int256/absolute.gno index 23beb4071a4..825dd60c62a 100644 --- a/examples/gno.land/p/demo/int256/absolute.gno +++ b/examples/gno.land/p/demo/int256/absolute.gno @@ -1,8 +1,6 @@ package int256 -import ( - "gno.land/p/demo/uint256" -) +import "gno.land/p/demo/uint256" // Abs returns |z| func (z *Int) Abs() *uint256.Uint { diff --git a/examples/gno.land/p/demo/int256/arithmetic.gno b/examples/gno.land/p/demo/int256/arithmetic.gno index ccb494bec9a..ce05426f585 100644 --- a/examples/gno.land/p/demo/int256/arithmetic.gno +++ b/examples/gno.land/p/demo/int256/arithmetic.gno @@ -1,8 +1,6 @@ package int256 -import ( - "gno.land/p/demo/uint256" -) +import "gno.land/p/demo/uint256" func (z *Int) Add(x, y *Int) *Int { z.initiateAbs() diff --git a/examples/gno.land/p/demo/int256/bitwise_test.gno b/examples/gno.land/p/demo/int256/bitwise_test.gno index ea51360ea65..8dc16cd17ac 100644 --- a/examples/gno.land/p/demo/int256/bitwise_test.gno +++ b/examples/gno.land/p/demo/int256/bitwise_test.gno @@ -1,11 +1,9 @@ package int256 -import ( - "gno.land/p/demo/uint256" -) - import ( "testing" + + "gno.land/p/demo/uint256" ) func TestOr(t *testing.T) { diff --git a/examples/gno.land/p/demo/int256/cmp_test.gno b/examples/gno.land/p/demo/int256/cmp_test.gno index 155cfa94b92..81b9231babe 100644 --- a/examples/gno.land/p/demo/int256/cmp_test.gno +++ b/examples/gno.land/p/demo/int256/cmp_test.gno @@ -1,8 +1,6 @@ package int256 -import ( - "testing" -) +import "testing" func TestEq(t *testing.T) { tests := []struct { diff --git a/examples/gno.land/p/demo/int256/conversion.gno b/examples/gno.land/p/demo/int256/conversion.gno index c6955f37421..ee6e7560f15 100644 --- a/examples/gno.land/p/demo/int256/conversion.gno +++ b/examples/gno.land/p/demo/int256/conversion.gno @@ -1,8 +1,6 @@ package int256 -import ( - "gno.land/p/demo/uint256" -) +import "gno.land/p/demo/uint256" // SetInt64 sets z to x and returns z. func (z *Int) SetInt64(x int64) *Int { diff --git a/examples/gno.land/p/demo/int256/int256_test.gno b/examples/gno.land/p/demo/int256/int256_test.gno index 974cc6ed87f..7c8181d1bec 100644 --- a/examples/gno.land/p/demo/int256/int256_test.gno +++ b/examples/gno.land/p/demo/int256/int256_test.gno @@ -1,9 +1,7 @@ // ported from github.com/mempooler/int256 package int256 -import ( - "testing" -) +import "testing" func TestSign(t *testing.T) { tests := []struct { diff --git a/examples/gno.land/p/demo/json/buffer_test.gno b/examples/gno.land/p/demo/json/buffer_test.gno index a1acce4eba0..b8dce390a61 100644 --- a/examples/gno.land/p/demo/json/buffer_test.gno +++ b/examples/gno.land/p/demo/json/buffer_test.gno @@ -1,8 +1,6 @@ package json -import ( - "testing" -) +import "testing" func TestBufferCurrent(t *testing.T) { tests := []struct { diff --git a/examples/gno.land/p/demo/json/decode_test.gno b/examples/gno.land/p/demo/json/decode_test.gno index 5a04846224e..8aad07169f2 100644 --- a/examples/gno.land/p/demo/json/decode_test.gno +++ b/examples/gno.land/p/demo/json/decode_test.gno @@ -2,7 +2,6 @@ package json import ( "bytes" - "encoding/json" "testing" ) diff --git a/examples/gno.land/p/demo/json/encode_test.gno b/examples/gno.land/p/demo/json/encode_test.gno index 33a1fae3d4e..e8e53993b5c 100644 --- a/examples/gno.land/p/demo/json/encode_test.gno +++ b/examples/gno.land/p/demo/json/encode_test.gno @@ -1,8 +1,6 @@ package json -import ( - "testing" -) +import "testing" func TestMarshal_Primitive(t *testing.T) { tests := []struct { diff --git a/examples/gno.land/p/demo/json/parser_test.gno b/examples/gno.land/p/demo/json/parser_test.gno index 44a2fee6404..078aa048a61 100644 --- a/examples/gno.land/p/demo/json/parser_test.gno +++ b/examples/gno.land/p/demo/json/parser_test.gno @@ -1,9 +1,6 @@ package json -import ( - "strconv" - "testing" -) +import "testing" func TestParseStringLiteral(t *testing.T) { tests := []struct { diff --git a/examples/gno.land/p/demo/json/path_test.gno b/examples/gno.land/p/demo/json/path_test.gno index dd242849f03..f68e3eb679f 100644 --- a/examples/gno.land/p/demo/json/path_test.gno +++ b/examples/gno.land/p/demo/json/path_test.gno @@ -1,8 +1,6 @@ package json -import ( - "testing" -) +import "testing" func TestParseJSONPath(t *testing.T) { tests := []struct { diff --git a/examples/gno.land/p/demo/math_eval/int32/int32_test.gno b/examples/gno.land/p/demo/math_eval/int32/int32_test.gno index 3e43210c495..a50a91fbd31 100644 --- a/examples/gno.land/p/demo/math_eval/int32/int32_test.gno +++ b/examples/gno.land/p/demo/math_eval/int32/int32_test.gno @@ -1,11 +1,6 @@ package int32 -import ( - "std" - "testing" - - "gno.land/p/demo/ufmt" -) +import "testing" func TestOne(t *testing.T) { ttt := []struct { diff --git a/examples/gno.land/p/demo/merkle/merkle_test.gno b/examples/gno.land/p/demo/merkle/merkle_test.gno index 7f1273e1d82..4aa99baa6d0 100644 --- a/examples/gno.land/p/demo/merkle/merkle_test.gno +++ b/examples/gno.land/p/demo/merkle/merkle_test.gno @@ -3,7 +3,6 @@ package merkle import ( "fmt" "testing" - "time" ) type testData struct { diff --git a/examples/gno.land/p/demo/mux/response.gno b/examples/gno.land/p/demo/mux/response.gno index 5b579b1d515..a2a52c7c7aa 100644 --- a/examples/gno.land/p/demo/mux/response.gno +++ b/examples/gno.land/p/demo/mux/response.gno @@ -1,8 +1,6 @@ package mux -import ( - "strings" -) +import "strings" // ResponseWriter represents the response writer. type ResponseWriter struct { diff --git a/examples/gno.land/p/demo/mux/router_test.gno b/examples/gno.land/p/demo/mux/router_test.gno index 71a87b687d3..13fd5b97955 100644 --- a/examples/gno.land/p/demo/mux/router_test.gno +++ b/examples/gno.land/p/demo/mux/router_test.gno @@ -1,9 +1,6 @@ package mux -import ( - "strings" - "testing" -) +import "testing" func TestRouter_Render(t *testing.T) { // Define handlers and route configuration diff --git a/examples/gno.land/p/demo/stack/stack_test.gno b/examples/gno.land/p/demo/stack/stack_test.gno index 764c7f7f691..ee26ee3f5a4 100644 --- a/examples/gno.land/p/demo/stack/stack_test.gno +++ b/examples/gno.land/p/demo/stack/stack_test.gno @@ -1,8 +1,6 @@ package stack -import ( - "testing" -) +import "testing" func TestStack(t *testing.T) { s := New() // Empty stack diff --git a/examples/gno.land/p/demo/tamagotchi/z0_filetest.gno b/examples/gno.land/p/demo/tamagotchi/z0_filetest.gno index 36163065e7f..4b2c04b6d5c 100644 --- a/examples/gno.land/p/demo/tamagotchi/z0_filetest.gno +++ b/examples/gno.land/p/demo/tamagotchi/z0_filetest.gno @@ -1,7 +1,6 @@ package main import ( - "std" "time" "internal/os_test" diff --git a/examples/gno.land/p/demo/uint256/bitwise_test.gno b/examples/gno.land/p/demo/uint256/bitwise_test.gno index 4437e7d5c50..aba89edfabf 100644 --- a/examples/gno.land/p/demo/uint256/bitwise_test.gno +++ b/examples/gno.land/p/demo/uint256/bitwise_test.gno @@ -1,8 +1,6 @@ package uint256 -import ( - "testing" -) +import "testing" type logicOpTest struct { name string diff --git a/examples/gno.land/p/demo/uint256/conversion_test.gno b/examples/gno.land/p/demo/uint256/conversion_test.gno index 12ae99cc0e9..ee3aad0f819 100644 --- a/examples/gno.land/p/demo/uint256/conversion_test.gno +++ b/examples/gno.land/p/demo/uint256/conversion_test.gno @@ -1,8 +1,6 @@ package uint256 -import ( - "testing" -) +import "testing" func TestIsUint64(t *testing.T) { tests := []struct { diff --git a/examples/gno.land/r/demo/art/millipede/millipede_test.gno b/examples/gno.land/r/demo/art/millipede/millipede_test.gno index 37f9c3c9296..8455bb9962e 100644 --- a/examples/gno.land/r/demo/art/millipede/millipede_test.gno +++ b/examples/gno.land/r/demo/art/millipede/millipede_test.gno @@ -1,8 +1,6 @@ package millipede -import ( - "testing" -) +import "testing" func TestRender(t *testing.T) { cases := []struct { diff --git a/examples/gno.land/r/demo/echo/echo_test.gno b/examples/gno.land/r/demo/echo/echo_test.gno index cafbfa65781..8e5be0704d5 100644 --- a/examples/gno.land/r/demo/echo/echo_test.gno +++ b/examples/gno.land/r/demo/echo/echo_test.gno @@ -1,8 +1,6 @@ package echo -import ( - "testing" -) +import "testing" func Test(t *testing.T) { if Render("aa") != "aa" { diff --git a/examples/gno.land/r/demo/foo20/foo20_test.gno b/examples/gno.land/r/demo/foo20/foo20_test.gno index 8cb8759fbe2..9c452ed6e3b 100644 --- a/examples/gno.land/r/demo/foo20/foo20_test.gno +++ b/examples/gno.land/r/demo/foo20/foo20_test.gno @@ -1,7 +1,6 @@ package foo20 import ( - "errors" "std" "testing" diff --git a/examples/gno.land/r/demo/keystore/keystore_test.gno b/examples/gno.land/r/demo/keystore/keystore_test.gno index 998c8457751..b2fc01c66b0 100644 --- a/examples/gno.land/r/demo/keystore/keystore_test.gno +++ b/examples/gno.land/r/demo/keystore/keystore_test.gno @@ -1,7 +1,6 @@ package keystore import ( - "fmt" "std" "strings" "testing" diff --git a/examples/gno.land/r/demo/microblog/gno.mod b/examples/gno.land/r/demo/microblog/gno.mod index 3285127b025..cff2ced8bc8 100644 --- a/examples/gno.land/r/demo/microblog/gno.mod +++ b/examples/gno.land/r/demo/microblog/gno.mod @@ -1,7 +1,6 @@ module gno.land/r/demo/microblog require ( - gno.land/p/demo/avl v0.0.0-latest gno.land/p/demo/microblog v0.0.0-latest gno.land/p/demo/testutils v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/r/demo/microblog/microblog_test.gno b/examples/gno.land/r/demo/microblog/microblog_test.gno index 83b20286fee..5b8ec9da370 100644 --- a/examples/gno.land/r/demo/microblog/microblog_test.gno +++ b/examples/gno.land/r/demo/microblog/microblog_test.gno @@ -1,13 +1,10 @@ package microblog import ( - "log" "std" "strings" "testing" - "gno.land/p/demo/avl" - "gno.land/p/demo/microblog" "gno.land/p/demo/testutils" ) diff --git a/examples/gno.land/r/demo/tamagotchi/z0_filetest.gno b/examples/gno.land/r/demo/tamagotchi/z0_filetest.gno index 373d737b3b5..e494ec5cbc8 100644 --- a/examples/gno.land/r/demo/tamagotchi/z0_filetest.gno +++ b/examples/gno.land/r/demo/tamagotchi/z0_filetest.gno @@ -1,9 +1,6 @@ package main import ( - "std" - "time" - "gno.land/r/demo/tamagotchi" ) diff --git a/examples/gno.land/r/demo/tests/gno.mod b/examples/gno.land/r/demo/tests/gno.mod index 9c5162f848e..f34d41d327a 100644 --- a/examples/gno.land/r/demo/tests/gno.mod +++ b/examples/gno.land/r/demo/tests/gno.mod @@ -1,6 +1,3 @@ module gno.land/r/demo/tests -require ( - gno.land/p/demo/testutils v0.0.0-latest - gno.land/r/demo/tests/subtests v0.0.0-latest -) +require gno.land/r/demo/tests/subtests v0.0.0-latest diff --git a/examples/gno.land/r/demo/tests/tests_test.gno b/examples/gno.land/r/demo/tests/tests_test.gno index 41d89526a4b..ccbc6b91265 100644 --- a/examples/gno.land/r/demo/tests/tests_test.gno +++ b/examples/gno.land/r/demo/tests/tests_test.gno @@ -3,8 +3,6 @@ package tests import ( "std" "testing" - - "gno.land/p/demo/testutils" ) func TestAssertOriginCall(t *testing.T) { diff --git a/examples/gno.land/r/demo/users/z_0_b_filetest.gno b/examples/gno.land/r/demo/users/z_0_b_filetest.gno index e8a7caff58d..9095057076c 100644 --- a/examples/gno.land/r/demo/users/z_0_b_filetest.gno +++ b/examples/gno.land/r/demo/users/z_0_b_filetest.gno @@ -3,8 +3,6 @@ package main // SEND: 199000000ugnot import ( - "std" - "gno.land/r/demo/users" ) diff --git a/examples/gno.land/r/demo/users/z_6_filetest.gno b/examples/gno.land/r/demo/users/z_6_filetest.gno index bc6d747ebbe..008ba03a936 100644 --- a/examples/gno.land/r/demo/users/z_6_filetest.gno +++ b/examples/gno.land/r/demo/users/z_6_filetest.gno @@ -3,7 +3,6 @@ package main import ( "std" - "gno.land/p/demo/testutils" "gno.land/r/demo/users" ) diff --git a/examples/gno.land/r/gnoland/ghverify/contract_test.gno b/examples/gno.land/r/gnoland/ghverify/contract_test.gno index 3f12bd0a7d6..d9c399942ae 100644 --- a/examples/gno.land/r/gnoland/ghverify/contract_test.gno +++ b/examples/gno.land/r/gnoland/ghverify/contract_test.gno @@ -2,7 +2,6 @@ package ghverify import ( "std" - "strings" "testing" "gno.land/p/demo/testutils" diff --git a/examples/gno.land/r/x/manfred_outfmt/outfmt_test.gno b/examples/gno.land/r/x/manfred_outfmt/outfmt_test.gno index 7bcd9e48f80..69c07bbbf16 100644 --- a/examples/gno.land/r/x/manfred_outfmt/outfmt_test.gno +++ b/examples/gno.land/r/x/manfred_outfmt/outfmt_test.gno @@ -45,6 +45,7 @@ Numbers: 24 25 2 // json { + got := outfmt.Render("?fmt=json") expected := `{"Number":746,"Text":"Hello Gnomes!","Numbers":[57,82,16,14,28,32]}` if got != expected { diff --git a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_d/v1/z_filetest.gno b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_d/v1/z_filetest.gno index 8f7537b04eb..3f7213eadf5 100644 --- a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_d/v1/z_filetest.gno +++ b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_d/v1/z_filetest.gno @@ -1,8 +1,6 @@ package main import ( - "fmt" - "gno.land/r/x/manfred_upgrade_patterns/upgrade_d/v1" ) diff --git a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_d/v2/z_filetest.gno b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_d/v2/z_filetest.gno index f884b5278ab..6fec97f49a2 100644 --- a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_d/v2/z_filetest.gno +++ b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_d/v2/z_filetest.gno @@ -1,8 +1,6 @@ package main -import ( - "gno.land/r/x/manfred_upgrade_patterns/upgrade_d/v2" -) +import "gno.land/r/x/manfred_upgrade_patterns/upgrade_d/v2" func main() { println("a", v2.Get("a")) diff --git a/examples/gno.land/r/x/map_delete/map_delete_test.gno b/examples/gno.land/r/x/map_delete/map_delete_test.gno index a0118befed2..c71fd4d2933 100644 --- a/examples/gno.land/r/x/map_delete/map_delete_test.gno +++ b/examples/gno.land/r/x/map_delete/map_delete_test.gno @@ -1,8 +1,6 @@ package mapdelete -import ( - "testing" -) +import "testing" func TestGetMap(t *testing.T) { if !(GetMap(3)) { diff --git a/examples/gno.land/r/x/nir1218_evaluation_proposal/evaluation_test.gno b/examples/gno.land/r/x/nir1218_evaluation_proposal/evaluation_test.gno index b96f2022650..f9e0913010d 100644 --- a/examples/gno.land/r/x/nir1218_evaluation_proposal/evaluation_test.gno +++ b/examples/gno.land/r/x/nir1218_evaluation_proposal/evaluation_test.gno @@ -8,10 +8,8 @@ package evaluation */ import ( - "std" "testing" - "gno.land/p/demo/avl" "gno.land/p/demo/testutils" "gno.land/p/demo/ufmt" ) diff --git a/examples/gno.land/r/x/nir1218_evaluation_proposal/tally.gno b/examples/gno.land/r/x/nir1218_evaluation_proposal/tally.gno index e02fba74341..30e88203f08 100644 --- a/examples/gno.land/r/x/nir1218_evaluation_proposal/tally.gno +++ b/examples/gno.land/r/x/nir1218_evaluation_proposal/tally.gno @@ -1,8 +1,6 @@ package evaluation -import ( - "gno.land/p/demo/avl" -) +import "gno.land/p/demo/avl" type TallyResult struct { results avl.Tree diff --git a/examples/gno.land/r/x/nir1218_evaluation_proposal/vote.gno b/examples/gno.land/r/x/nir1218_evaluation_proposal/vote.gno index aad30cb1de4..f46fc18823b 100644 --- a/examples/gno.land/r/x/nir1218_evaluation_proposal/vote.gno +++ b/examples/gno.land/r/x/nir1218_evaluation_proposal/vote.gno @@ -1,8 +1,6 @@ package evaluation -import ( - "std" -) +import "std" const ( VoteYes = "YES" diff --git a/gnovm/Makefile b/gnovm/Makefile index aa80c61ac7d..6e939289fb8 100644 --- a/gnovm/Makefile +++ b/gnovm/Makefile @@ -17,6 +17,7 @@ CGO_ENABLED ?= 0 export CGO_ENABLED # flags for `make fmt`. -w will write the result to the destination files. GOFMT_FLAGS ?= -w +GNOFMT_FLAGS ?= -w # flags for `make imports`. GOIMPORTS_FLAGS ?= $(GOFMT_FLAGS) # test suite flags. @@ -52,9 +53,9 @@ lint: $(rundep) github.com/golangci/golangci-lint/cmd/golangci-lint run --config ../.github/golangci.yml ./... .PHONY: fmt -fmt: +fmt: + go run ./cmd/gno fmt $(GNOFMT_FLAGS) ./stdlibs/... $(rundep) mvdan.cc/gofumpt $(GOFMT_FLAGS) . - $(rundep) mvdan.cc/gofumpt $(GOFMT_FLAGS) `find stdlibs -name "*.gno"` .PHONY: imports imports: diff --git a/gnovm/cmd/gno/fmt.go b/gnovm/cmd/gno/fmt.go new file mode 100644 index 00000000000..7c5ad42c2b0 --- /dev/null +++ b/gnovm/cmd/gno/fmt.go @@ -0,0 +1,291 @@ +package main + +import ( + "bytes" + "context" + "errors" + "flag" + "fmt" + "go/format" + "go/parser" + "go/scanner" + "go/token" + "os" + "path/filepath" + "strings" + + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/gnovm/pkg/gnofmt" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/rogpeppe/go-internal/diff" +) + +type fmtCfg struct { + write bool + quiet bool + diff bool + verbose bool + imports bool + include fmtIncludes +} + +func newFmtCmd(io commands.IO) *commands.Command { + cfg := &fmtCfg{} + return commands.NewCommand( + commands.Metadata{ + Name: "fmt", + ShortUsage: "gno fmt [flags] [path ...]", + ShortHelp: "Run gno file formatter.", + LongHelp: "The `gno fmt` tool processes, formats, and cleans up `gno` source files.", + }, + cfg, + func(_ context.Context, args []string) error { + return execFmt(cfg, args, io) + }) +} + +func (c *fmtCfg) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar( + &c.write, + "w", + false, + "write result to (source) file instead of stdout", + ) + + fs.BoolVar( + &c.verbose, + "v", + false, + "verbose mode", + ) + + fs.BoolVar( + &c.quiet, + "q", + false, + "quiet mode", + ) + + fs.Var( + &c.include, + "include", + "specify additional directories containing packages to resolve", + ) + + fs.BoolVar( + &c.imports, + "imports", + true, + "attempt to format, resolve and sort file imports", + ) + + fs.BoolVar( + &c.diff, + "diff", + false, + "print and make the command fail if any diff is found", + ) +} + +type fmtProcessFileFunc func(file string, io commands.IO) []byte + +func execFmt(cfg *fmtCfg, args []string, io commands.IO) error { + if len(args) == 0 { + return flag.ErrHelp + } + + paths, err := targetsFromPatterns(args) + if err != nil { + return fmt.Errorf("unable to get targets paths from patterns: %w", err) + } + + files, err := gnoFilesFromArgs(paths) + if err != nil { + return fmt.Errorf("unable to gather gno files: %w", err) + } + + processFileFunc, err := fmtGetProcessFileFunc(cfg, io) + if err != nil { + return err + } + + errCount := fmtProcessFiles(cfg, files, processFileFunc, io) + if errCount > 0 { + if !cfg.verbose { + return commands.ExitCodeError(1) + } + + return fmt.Errorf("failed to format %d files", errCount) + } + + return nil +} + +func fmtGetProcessFileFunc(cfg *fmtCfg, io commands.IO) (fmtProcessFileFunc, error) { + if cfg.imports { + return fmtFormatFileImports(cfg, io) + } + return fmtFormatFile, nil +} + +func fmtProcessFiles(cfg *fmtCfg, files []string, processFile fmtProcessFileFunc, io commands.IO) int { + errCount := 0 + for _, file := range files { + if fmtProcessSingleFile(cfg, file, processFile, io) { + continue // ok + } + + errCount++ + } + return errCount +} + +// fmtProcessSingleFile process a single file and return false if any error occurred +func fmtProcessSingleFile(cfg *fmtCfg, file string, processFile fmtProcessFileFunc, io commands.IO) bool { + if cfg.verbose { + io.Printfln("processing %q", file) + } + + fi, err := os.Stat(file) + if err != nil { + io.ErrPrintfln("unable to stat %q: %v", file, err) + return false + } + + out := processFile(file, io) + if out == nil { + return false + } + + if cfg.diff && fmtProcessDiff(file, out, io) { + return false + } + if !cfg.write { + if !cfg.diff && !cfg.quiet { + io.Println(string(out)) + } + return true + } + + perms := fi.Mode() & os.ModePerm + if err = os.WriteFile(file, out, perms); err != nil { + io.ErrPrintfln("unable to write %q: %v", file, err) + return false + } + + return true +} + +func fmtProcessDiff(file string, data []byte, io commands.IO) bool { + oldFile, err := os.ReadFile(file) + if err != nil { + io.ErrPrintfln("unable to read %q for diffing: %v", file, err) + return true + } + + if d := diff.Diff(file, oldFile, file+".formatted", data); d != nil { + io.ErrPrintln(string(d)) + return true + } + + return false +} + +func fmtFormatFileImports(cfg *fmtCfg, io commands.IO) (fmtProcessFileFunc, error) { + r := gnofmt.NewFSResolver() + + gnoroot := gnoenv.RootDir() + + pkgHandler := func(path string, err error) error { + if err == nil { + return nil + } + + if !fmtPrintScannerError(err, io) { + io.ErrPrintfln("unable to load %q: %w", err.Error()) + } + + return nil + } + + // Load any additional packages supplied by the user + for _, include := range cfg.include { + absp, err := filepath.Abs(include) + if err != nil { + return nil, fmt.Errorf("unable to determine absolute path of %q: %w", include, err) + } + + if err := r.LoadPackages(absp, pkgHandler); err != nil { + return nil, fmt.Errorf("unable to load %q: %w", absp, err) + } + } + + // Load stdlibs + stdlibs := filepath.Join(gnoroot, "gnovm", "stdlibs") + if err := r.LoadPackages(stdlibs, pkgHandler); err != nil { + return nil, fmt.Errorf("unable to load %q: %w", stdlibs, err) + } + + // Load examples directory + examples := filepath.Join(gnoroot, "examples") + if err := r.LoadPackages(examples, pkgHandler); err != nil { + return nil, fmt.Errorf("unable to load %q: %w", examples, err) + } + + p := gnofmt.NewProcessor(r) + return func(file string, io commands.IO) []byte { + data, err := p.FormatFile(file) + if err == nil { + return data + } + + if !fmtPrintScannerError(err, io) { + io.ErrPrintfln("format error: %s", err.Error()) + } + + return nil + }, nil +} + +func fmtFormatFile(file string, io commands.IO) []byte { + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, file, nil, parser.AllErrors|parser.ParseComments) + if err != nil { + fmtPrintScannerError(err, io) + return nil + } + + var buf bytes.Buffer + if err := format.Node(&buf, fset, node); err != nil { + io.ErrPrintfln("format error: %s", err.Error()) + return nil + } + + return buf.Bytes() +} + +func fmtPrintScannerError(err error, io commands.IO) bool { + // Get underlying parse error + for ; err != nil; err = errors.Unwrap(err) { + if scanErrors, ok := err.(scanner.ErrorList); ok { + for _, e := range scanErrors { + io.ErrPrintln(e) + } + + return true + } + } + + return false +} + +type fmtIncludes []string + +func (i fmtIncludes) String() string { + return strings.Join(i, ",") +} + +func (i *fmtIncludes) Set(path string) error { + *i = append(*i, path) + return nil +} diff --git a/gnovm/cmd/gno/fmt_test.go b/gnovm/cmd/gno/fmt_test.go new file mode 100644 index 00000000000..3b3d1bd51a6 --- /dev/null +++ b/gnovm/cmd/gno/fmt_test.go @@ -0,0 +1,18 @@ +package main + +import "testing" + +func TestFmtApp(t *testing.T) { + tc := []testMainCase{ + { + args: []string{"fmt"}, + errShouldBe: "flag: help requested", + }, { + args: []string{"fmt", "../../tests/integ/unformated/missing_import.gno"}, + stdoutShouldContain: "strconv", + }, + + // XXX: more complex output are tested in `testdata/gno_test/fmt_*.txtar`. + } + testMainCaseRun(t, tc) +} diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index e8f5e5d3824..6c497c7e2c0 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -62,7 +62,7 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { rootDir = gnoenv.RootDir() } - pkgPaths, err := gnoPackagesFromArgs(args) + pkgPaths, err := gnoPackagesFromArgsRecursively(args) if err != nil { return fmt.Errorf("list packages from args: %w", err) } diff --git a/gnovm/cmd/gno/main.go b/gnovm/cmd/gno/main.go index 8b77cfd2a10..7a5799f2835 100644 --- a/gnovm/cmd/gno/main.go +++ b/gnovm/cmd/gno/main.go @@ -34,7 +34,7 @@ func newGnocliCmd(io commands.IO) *commands.Command { newDocCmd(io), newEnvCmd(io), newBugCmd(io), - // fmt -- gofmt + newFmtCmd(io), // graph // vendor -- download deps from the chain in vendor/ // list -- list packages diff --git a/gnovm/cmd/gno/test_test.go b/gnovm/cmd/gno/test_test.go deleted file mode 100644 index 98320f41cc9..00000000000 --- a/gnovm/cmd/gno/test_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "os" - "strconv" - "testing" - - "github.com/gnolang/gno/gnovm/pkg/integration" - "github.com/rogpeppe/go-internal/testscript" - "github.com/stretchr/testify/require" -) - -func Test_ScriptsTest(t *testing.T) { - updateScripts, _ := strconv.ParseBool(os.Getenv("UPDATE_SCRIPTS")) - p := testscript.Params{ - UpdateScripts: updateScripts, - Dir: "testdata/gno_test", - } - - if coverdir, ok := integration.ResolveCoverageDir(); ok { - err := integration.SetupTestscriptsCoverage(&p, coverdir) - require.NoError(t, err) - } - - err := integration.SetupGno(&p, t.TempDir()) - require.NoError(t, err) - - testscript.Run(t, p) -} diff --git a/gnovm/cmd/gno/testdata/gno_fmt/empty.txtar b/gnovm/cmd/gno/testdata/gno_fmt/empty.txtar new file mode 100644 index 00000000000..14f85227335 --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_fmt/empty.txtar @@ -0,0 +1,10 @@ +gno fmt noimport.gno +cmp stdout stdout.golden.gno +cmp stderr stderr.golden + +-- noimport.gno -- +package hello +-- stdout.golden.gno -- +package hello + +-- stderr.golden -- diff --git a/gnovm/cmd/gno/testdata/gno_fmt/import_cleaning.txtar b/gnovm/cmd/gno/testdata/gno_fmt/import_cleaning.txtar new file mode 100644 index 00000000000..bc186f98b6f --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_fmt/import_cleaning.txtar @@ -0,0 +1,30 @@ +# two files with with one declaration on the side + +gno fmt cleaning.gno +cmp stdout stdout.golden +cmp stderr stderr.golden + +-- cleaning.gno -- +package testdata + +import ( + "std" + + "gno.land/r/hello" +) + +var yes = rand.Val + +-- otherfile.gno -- +package testdata + +type S struct {} + +var rand = &S{} + +-- stdout.golden -- +package testdata + +var yes = rand.Val + +-- stderr.golden -- diff --git a/gnovm/cmd/gno/testdata/gno_fmt/include.txtar b/gnovm/cmd/gno/testdata/gno_fmt/include.txtar new file mode 100644 index 00000000000..92767983705 --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_fmt/include.txtar @@ -0,0 +1,31 @@ +# Test fmt with include and verbose enabled +gno fmt -include $WORK/pkg file.gno +cmp stdout stdout.golden +cmp stderr stderr.golden + +-- pkg/mypkg/file.gno -- +package mypkg + +func HelloFromMyPkg() string { + return "hello gnoland" +} + +-- pkg/mypkg/gno.mod -- +module gno.land/r/test/mypkg + +-- file.gno -- +package testdata + +var myVar = mypkg.HelloFromMyPkg() + +-- gno.mod -- +module gno.land/r/test/mypkg2 + +-- stdout.golden -- +package testdata + +import "gno.land/r/test/mypkg" + +var myVar = mypkg.HelloFromMyPkg() + +-- stderr.golden -- diff --git a/gnovm/cmd/gno/testdata/gno_fmt/multi_import.txtar b/gnovm/cmd/gno/testdata/gno_fmt/multi_import.txtar new file mode 100644 index 00000000000..9e16f33591d --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_fmt/multi_import.txtar @@ -0,0 +1,96 @@ +# Test format complex files with advanced declarations + +gno fmt file1.gno +cmp stdout stdout.golden +cmp stderr stderr.golden + +# do it again, output should be identitical +gno fmt file1.gno +cmp stdout stdout.golden +cmp stderr stderr.golden + +-- file1.gno -- +package testdata + +import ( + "std" + + // This comment should stay above the declaration + avl "gno.land/my/import/does/not/exist" + "doesnotexist" +) + + +// Pkg Is already imported +var myVar = avl.Node{} + +// Pkg exist and should be imported + var myVar2 = std.Banker{} + +// This one doesn't exist +var myVar3 = doesnotexistpkg.Bis{} + +// Package should exist but not the declaration + var myVar4 = io.DoesnNotExist{} + +// Declaration exist in a side file and should not be imported +var myVar5 = math.Sqrt(42) + +func myBlog() *blog.Blog { + myVar4 := time.Time{} + + var subFunc = func() { + // More complex catch declaration + println(string(ufmt.Sprintf("hello gno"))) + } + + return &blog.Blog{} +} + +-- file2.gno -- +package testdata + +type MathS struct {} + +var math = MathS{} + +-- stdout.golden -- +package testdata + +import ( + "std" + "time" + + // This comment should stay above the declaration + avl "gno.land/my/import/does/not/exist" + "gno.land/p/demo/blog" + "gno.land/p/demo/ufmt" +) + +// Pkg Is already imported +var myVar = avl.Node{} + +// Pkg exist and should be imported +var myVar2 = std.Banker{} + +// This one doesn't exist +var myVar3 = doesnotexistpkg.Bis{} + +// Package should exist but not the declaration +var myVar4 = io.DoesnNotExist{} + +// Declaration exist in a side file and should not be imported +var myVar5 = math.Sqrt(42) + +func myBlog() *blog.Blog { + myVar4 := time.Time{} + + var subFunc = func() { + // More complex catch declaration + println(string(ufmt.Sprintf("hello gno"))) + } + + return &blog.Blog{} +} + +-- stderr.golden -- diff --git a/gnovm/cmd/gno/testdata/gno_fmt/noimport_format.txtar b/gnovm/cmd/gno/testdata/gno_fmt/noimport_format.txtar new file mode 100644 index 00000000000..56fca6ddbd7 --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_fmt/noimport_format.txtar @@ -0,0 +1,39 @@ +# Test format without trying to resolve import + +gno fmt -imports=false cleaning.gno +cmp stdout stdout.golden +cmp stderr stderr.golden + +-- cleaning.gno -- +package testdata + +import ( + "std" + + "gno.land/r/hello" + + + +) + + var yes = rand.Val + +-- otherfile.gno -- +package testdata + +type S struct {} + +var rand = &S{} + +-- stdout.golden -- +package testdata + +import ( + "std" + + "gno.land/r/hello" +) + +var yes = rand.Val + +-- stderr.golden -- diff --git a/gnovm/cmd/gno/testdata/gno_fmt/parse_error.txtar b/gnovm/cmd/gno/testdata/gno_fmt/parse_error.txtar new file mode 100644 index 00000000000..083bdce8769 --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_fmt/parse_error.txtar @@ -0,0 +1,19 @@ +# Test error parsing + +! gno fmt file1.gno +cmp stdout stdout.golden +stderr 'file1.gno:6:1: expected declaration, found myVar' +stderr 'file1.gno:9:12: expected type, found' + +-- file1.gno -- +package testdata + +import "gno.land/p/demo/avl" + +// invalid syntax +myVar + avl.Node + +// invalid syntax +var myVal2 := "hello" + +-- stdout.golden -- diff --git a/gnovm/cmd/gno/testdata/gno_fmt/shadow_import.txtar b/gnovm/cmd/gno/testdata/gno_fmt/shadow_import.txtar new file mode 100644 index 00000000000..50d0970497e --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_fmt/shadow_import.txtar @@ -0,0 +1,53 @@ +# Test error parsing + +gno fmt -include v1 -include v2 v2/file_filetest.gno +cmp stdout stdout.golden +cmp stderr stderr.golden + +-- v1/file.gno -- +package v1 + +func Get(v string) string { + return "v1:"+ v +} + +-- v1/gno.mod -- +module "gno.land/r/dev/shadow/v1" + +-- v2/file.gno -- +package v1 + +func Get(v string) string { + return "v2:"+ v +} + + +-- v2/file_filetest.gno -- +package main + +import ( + // should be valid + "gno.land/r/dev/shadow/v2" +) + +func main() { + println("a", v1.Get("a")) +} + +-- v2/gno.mod -- +module "gno.land/r/dev/shadow/v2" + + +-- stdout.golden -- +package main + +import ( + // should be valid + "gno.land/r/dev/shadow/v2" +) + +func main() { + println("a", v1.Get("a")) +} + +-- stderr.golden -- diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_bad_import.txtar b/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/lint_bad_import.txtar rename to gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_file_error.txtar b/gnovm/cmd/gno/testdata/gno_lint/file_error.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/lint_file_error.txtar rename to gnovm/cmd/gno/testdata/gno_lint/file_error.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_file_error_txtar b/gnovm/cmd/gno/testdata/gno_lint/file_error_txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/lint_file_error_txtar rename to gnovm/cmd/gno/testdata/gno_lint/file_error_txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_no_error.txtar b/gnovm/cmd/gno/testdata/gno_lint/no_error.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/lint_no_error.txtar rename to gnovm/cmd/gno/testdata/gno_lint/no_error.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_no_gnomod.txtar b/gnovm/cmd/gno/testdata/gno_lint/no_gnomod.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/lint_no_gnomod.txtar rename to gnovm/cmd/gno/testdata/gno_lint/no_gnomod.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/lint_not_declared.txtar b/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/lint_not_declared.txtar rename to gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/fmt_write_import.txtar b/gnovm/cmd/gno/testdata/gno_test/fmt_write_import.txtar new file mode 100644 index 00000000000..6f316fab9e0 --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_test/fmt_write_import.txtar @@ -0,0 +1,25 @@ +# Test format with write flag + +gno fmt -w file1.gno +cmp file1.gno file1.gno.golden +cmp stderr stderr.golden + +-- file1.gno -- +package testdata + +import ( + "std" + "doesnotexist" +) + + var myVar1 = std.Banker{} + +-- file1.gno.golden -- +package testdata + +import ( + "std" +) + +var myVar1 = std.Banker{} +-- stderr.golden -- diff --git a/gnovm/cmd/gno/testdata_test.go b/gnovm/cmd/gno/testdata_test.go new file mode 100644 index 00000000000..15bc8d96e26 --- /dev/null +++ b/gnovm/cmd/gno/testdata_test.go @@ -0,0 +1,46 @@ +package main + +import ( + "os" + "path/filepath" + "strconv" + "testing" + + "github.com/gnolang/gno/gnovm/pkg/integration" + "github.com/rogpeppe/go-internal/testscript" + "github.com/stretchr/testify/require" +) + +func Test_Scripts(t *testing.T) { + testdata, err := filepath.Abs("testdata") + require.NoError(t, err) + + testdirs, err := os.ReadDir(testdata) + require.NoError(t, err) + + for _, dir := range testdirs { + if !dir.IsDir() { + continue + } + + name := dir.Name() + t.Logf("testing: %s", name) + t.Run(name, func(t *testing.T) { + updateScripts, _ := strconv.ParseBool(os.Getenv("UPDATE_SCRIPTS")) + p := testscript.Params{ + UpdateScripts: updateScripts, + Dir: filepath.Join(testdata, name), + } + + if coverdir, ok := integration.ResolveCoverageDir(); ok { + err := integration.SetupTestscriptsCoverage(&p, coverdir) + require.NoError(t, err) + } + + err := integration.SetupGno(&p, t.TempDir()) + require.NoError(t, err) + + testscript.Run(t, p) + }) + } +} diff --git a/gnovm/cmd/gno/transpile.go b/gnovm/cmd/gno/transpile.go index 2e12ee6f4b3..1e3081ca2b0 100644 --- a/gnovm/cmd/gno/transpile.go +++ b/gnovm/cmd/gno/transpile.go @@ -134,7 +134,7 @@ func execTranspile(cfg *transpileCfg, args []string, io commands.IO) error { } // transpile .gno packages and files. - paths, err := gnoPackagesFromArgs(args) + paths, err := gnoFilesFromArgsRecursively(args) if err != nil { return fmt.Errorf("list paths: %w", err) } diff --git a/gnovm/cmd/gno/util.go b/gnovm/cmd/gno/util.go index 480161c2b7e..90aedd5d27a 100644 --- a/gnovm/cmd/gno/util.go +++ b/gnovm/cmd/gno/util.go @@ -21,7 +21,7 @@ func isFileExist(path string) bool { return err == nil } -func gnoPackagesFromArgs(args []string) ([]string, error) { +func gnoFilesFromArgsRecursively(args []string) ([]string, error) { var paths []string for _, argPath := range args { @@ -31,7 +31,9 @@ func gnoPackagesFromArgs(args []string) ([]string, error) { } if !info.IsDir() { - paths = append(paths, ensurePathPrefix(argPath)) + if isGnoFile(fs.FileInfoToDirEntry(info)) { + paths = append(paths, ensurePathPrefix(argPath)) + } continue } @@ -48,6 +50,37 @@ func gnoPackagesFromArgs(args []string) ([]string, error) { return paths, nil } +func gnoFilesFromArgs(args []string) ([]string, error) { + var paths []string + + for _, argPath := range args { + info, err := os.Stat(argPath) + if err != nil { + return nil, fmt.Errorf("invalid file or package path: %w", err) + } + + if !info.IsDir() { + if isGnoFile(fs.FileInfoToDirEntry(info)) { + paths = append(paths, ensurePathPrefix(argPath)) + } + continue + } + + files, err := os.ReadDir(argPath) + if err != nil { + return nil, err + } + for _, f := range files { + if isGnoFile(f) { + path := filepath.Join(argPath, f.Name()) + paths = append(paths, ensurePathPrefix(path)) + } + } + } + + return paths, nil +} + func ensurePathPrefix(path string) string { if filepath.IsAbs(path) { return path @@ -86,6 +119,33 @@ func walkDirForGnoFiles(root string, addPath func(path string)) error { return filepath.WalkDir(root, walkFn) } +func gnoPackagesFromArgsRecursively(args []string) ([]string, error) { + var paths []string + + for _, argPath := range args { + info, err := os.Stat(argPath) + if err != nil { + return nil, fmt.Errorf("invalid file or package path: %w", err) + } + + if !info.IsDir() { + paths = append(paths, ensurePathPrefix(argPath)) + + continue + } + + // Gather package paths from the directory + err = walkDirForGnoFiles(argPath, func(path string) { + paths = append(paths, ensurePathPrefix(path)) + }) + if err != nil { + return nil, fmt.Errorf("unable to walk dir: %w", err) + } + } + + return paths, nil +} + // targetsFromPatterns returns a list of target paths that match the patterns. // Each pattern can represent a file or a directory, and if the pattern // includes "/...", the "..." is treated as a wildcard, matching any string. diff --git a/gnovm/pkg/gnofmt/package.go b/gnovm/pkg/gnofmt/package.go new file mode 100644 index 00000000000..c576bd8ee78 --- /dev/null +++ b/gnovm/pkg/gnofmt/package.go @@ -0,0 +1,156 @@ +package gnofmt + +import ( + "fmt" + "go/parser" + "go/token" + "io" + "os" + "path/filepath" + "strings" + + "github.com/gnolang/gno/gnovm/pkg/gnomod" +) + +type Package interface { + // Should return the package path + Path() string + // Should return the name of the package as defined at the top level of each file + Name() string + // Should return all gno filenames inside the package + Files() []string + // Should return a content reader for the given filename within the package + Read(filename string) (io.ReadCloser, error) +} + +type PackageReadWalkFunc func(filename string, r io.Reader, err error) error + +func ReadWalkPackage(pkg Package, fn PackageReadWalkFunc) error { + for _, filename := range pkg.Files() { + if !isGnoFile(filename) { + return nil + } + + r, err := pkg.Read(filename) + fnErr := fn(filename, r, err) + r.Close() + if fnErr != nil { + return fnErr + } + } + + return nil +} + +type fsPackage struct { + path string + name string + dir string + files []string // filenames +} + +// ParsePackage parses package from the given directory. +// It will return a nil package if no gno files are found. +// If a gno.mod is found, it will be used to determine the pkg path. +// If root is specified, it will be trimmed from the actual given dir to create the pkgpath if no gno.mod is found. +func ParsePackage(fset *token.FileSet, root string, dir string) (Package, error) { + files, err := os.ReadDir(dir) + if err != nil { + return nil, fmt.Errorf("unable to read dir %q: %w", dir, err) + } + + var pkgname string + + gnofiles := []string{} + for _, file := range files { + name := file.Name() + if !isGnoFile(name) { + continue + } + + // Ignore package name from test files + if isTestFile(name) { + gnofiles = append(gnofiles, name) + continue + } + + filename := filepath.Join(dir, name) + f, err := parser.ParseFile(fset, filename, nil, parser.PackageClauseOnly) + if err != nil { + return nil, fmt.Errorf("unable to parse file %q: %w", filename, err) + } + + if pkgname != "" && pkgname != f.Name.Name { + return nil, fmt.Errorf("conflict package name between %q and %q", pkgname, f.Name.Name) + } + + pkgname = f.Name.Name + gnofiles = append(gnofiles, name) + } + + if len(gnofiles) == 0 { + return nil, nil // Not a package + } + + var pkgpath string + + // Check for a gno.mod, in which case it will define the module path + gnoModPath := filepath.Join(dir, "gno.mod") + data, err := os.ReadFile(gnoModPath) + switch { + case os.IsNotExist(err): + if len(root) > 0 { + // Fallback on dir path trimmed from the root + pkgpath = strings.TrimPrefix(dir, filepath.Clean(root)) + pkgpath = strings.TrimPrefix(pkgpath, "/") + } + + case err == nil: + gnoMod, err := gnomod.Parse(gnoModPath, data) + if err != nil { + return nil, fmt.Errorf("unable to parse gnomod %q: %w", gnoModPath, err) + } + + gnoMod.Sanitize() + if err := gnoMod.Validate(); err != nil { + return nil, fmt.Errorf("unable to validate gnomod %q: %w", gnoModPath, err) + } + + pkgpath = gnoMod.Module.Mod.Path + default: + return nil, fmt.Errorf("unable to read %q: %w", gnoModPath, err) + } + + return &fsPackage{ + path: pkgpath, + files: gnofiles, + dir: dir, + name: pkgname, + }, nil +} + +func (p *fsPackage) Path() string { + return p.path +} + +func (p *fsPackage) Name() string { + return p.name +} + +func (p *fsPackage) Files() []string { + return p.files +} + +func (p *fsPackage) Read(filename string) (io.ReadCloser, error) { + if !isGnoFile(filename) { + return nil, fmt.Errorf("invalid gno file %q", filename) + } + + path := filepath.Join(p.dir, filename) + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("unable to open %q: %w", path, err) + } + + return file, nil +} diff --git a/gnovm/pkg/gnofmt/processes_test.go b/gnovm/pkg/gnofmt/processes_test.go new file mode 100644 index 00000000000..6c56012c3ad --- /dev/null +++ b/gnovm/pkg/gnofmt/processes_test.go @@ -0,0 +1,96 @@ +// For convenient purposes, more tests have been created using `testscripts` format and are located in `gnovm/cmd/testdata/gno_fmt/` folder + +package gnofmt + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFormatImportFromSource(t *testing.T) { + t.Parallel() + + mockResolver := newMockResolver() + + mp := newMockedPackage("example.com/mypkg", "mypkg") + pkgcontent := `package mypkg + +func MyFunc(str string) string{ + return "Hello: "+str +}` + mp.AddFile("my.gno", []byte(pkgcontent)) + mockResolver.AddPackage(mp) + + sourceCode := `package main + +func main() { + str := "hello, world" + mypkg.MyFunc(str) +}` + + // Add packages to the MockResolver + processor := NewProcessor(mockResolver) + formatted, err := processor.FormatImportFromSource("main.go", sourceCode) + require.NoError(t, err) + + expectedOutput := `package main + +import "example.com/mypkg" + +func main() { + str := "hello, world" + mypkg.MyFunc(str) +} +` + + require.Equal(t, expectedOutput, string(formatted)) +} + +func TestFormatImportFromFile(t *testing.T) { + t.Parallel() + + mockResolver := newMockResolver() + + // Add packages to the MockResolver + mp := newMockedPackage("example.com/mypkg", "mypkg") + pkgcontent := `package mypkg + +func MyFunc(str string) string{ + return "Hello: "+str +}` + mp.AddFile("my.gno", []byte(pkgcontent)) + mockResolver.AddPackage(mp) + + processor := NewProcessor(mockResolver) + sourceFile := "main.gno" + sourceCode := `package main + +func main() { + str := "hello, world" + println(mypkg.MyFunc(str)) +}` + + expectedOutput := `package main + +import "example.com/mypkg" + +func main() { + str := "hello, world" + println(mypkg.MyFunc(str)) +} +` + // Create a temporary directory and file + dir := t.TempDir() + filePath := filepath.Join(dir, sourceFile) + + err := os.WriteFile(filePath, []byte(sourceCode), 0o644) + require.NoError(t, err) + + formatted, err := processor.FormatFile(filePath) + require.NoError(t, err) + + require.Equal(t, expectedOutput, string(formatted)) +} diff --git a/gnovm/pkg/gnofmt/processor.go b/gnovm/pkg/gnofmt/processor.go new file mode 100644 index 00000000000..c6484fe6784 --- /dev/null +++ b/gnovm/pkg/gnofmt/processor.go @@ -0,0 +1,334 @@ +package gnofmt + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "io" + "path/filepath" + + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/imports" +) + +const tabWidth = 8 + +type parsedPackage struct { + error error + files map[string]*ast.File + decls map[*ast.Object]ast.Decl +} + +type Processor struct { + resolver Resolver + fset *token.FileSet + + // cache package parsing in `FormatFile` call + pkgdirCache map[string]Package // dir -> pkg cache package dir + + // cache for global parsed package + parsedPackage map[string]*parsedPackage // pkgdir -> parsed package +} + +func NewProcessor(r Resolver) *Processor { + return &Processor{ + resolver: r, + fset: token.NewFileSet(), + pkgdirCache: make(map[string]Package), + parsedPackage: make(map[string]*parsedPackage), + } +} + +// FormatImportFromSource parse and format the source from src. The type of the argument +// for the src parameter must be string, []byte, or [io.Reader]. +func (p *Processor) FormatImportFromSource(filename string, src any) ([]byte, error) { + // Parse the source file + nodefile, err := p.parseFile(filename, src) + if err != nil { + return nil, fmt.Errorf("unable to parse source: %w", err) + } + + // Collect top level declarations within the source + pkgDecls := make(map[*ast.Object]ast.Decl) + collectTopDeclaration(nodefile, pkgDecls) + + // Process and format the parsed node. + return p.processAndFormat(nodefile, filename, pkgDecls) +} + +// FormatFile processes a single Gno file from the given Package and filename. +func (p *Processor) FormatPackageFile(pkg Package, filename string) ([]byte, error) { + // Process package files. + pkgc := p.processPackageFiles(pkg.Path(), pkg) + if pkgc.error != nil { + return nil, fmt.Errorf("unable to process package: %w", pkgc.error) + } + + // Retrieve the nodefile for the file. + nodefile, ok := pkgc.files[filename] + if !ok { + return nil, fmt.Errorf("not a valid gno file: %s", filename) + } + + return p.processAndFormat(nodefile, filename, pkgc.decls) +} + +// FormatFile processes a single Gno file from the given file path. +func (p *Processor) FormatFile(file string) ([]byte, error) { + filename := filepath.Base(file) + dir := filepath.Dir(file) + + pkg, ok := p.pkgdirCache[dir] + if !ok { + var err error + pkg, err = ParsePackage(p.fset, "", dir) + if err != nil { + return nil, fmt.Errorf("unable to parse package %q: %w", dir, err) + } + p.pkgdirCache[dir] = pkg + } + + if pkg == nil { + fmt.Printf("-> parsing file: %q, %q\n", file, filename) + // Fallback on src + return p.FormatImportFromSource(filename, nil) + } + + path := pkg.Path() + if path == "" { + // Use dir as package path + path = dir + } + + // Process package files. + pkgc := p.processPackageFiles(dir, pkg) + if pkgc.error != nil { + return nil, fmt.Errorf("unable to process package: %w", pkgc.error) + } + + // Retrieve the nodefile for the file. + nodefile, ok := pkgc.files[filename] + if !ok { + return nil, fmt.Errorf("not a valid gno file: %s", filename) + } + + return p.processAndFormat(nodefile, filename, pkgc.decls) +} + +func (p *Processor) parseFile(path string, src any) (file *ast.File, err error) { + // Parse the source file + file, err = parser.ParseFile(p.fset, path, src, parser.ParseComments|parser.AllErrors) + if err != nil { + return nil, fmt.Errorf("unable to parse file %q: %w", path, err) + } + + return file, nil +} + +// Helper function to process and format a parsed AST node. +func (p *Processor) processAndFormat(file *ast.File, filename string, topDecls map[*ast.Object]ast.Decl) ([]byte, error) { + // Collect unresolved + unresolved := collectUnresolved(file, topDecls) + + // Cleanup and remove previous unused import + p.cleanupPreviousImports(file, topDecls, unresolved) + + // Resolve unresolved declarations + p.resolve(file, unresolved) + + // Process output + var buf bytes.Buffer + if err := printer.Fprint(&buf, p.fset, file); err != nil { + return nil, fmt.Errorf("unable to format file: %w", err) + } + + // Use go/imports for formating and sort imports. + ret, err := imports.Process(filename, buf.Bytes(), &imports.Options{ + TabWidth: tabWidth, + Comments: true, + TabIndent: true, + FormatOnly: true, + }) + if err != nil { + return nil, fmt.Errorf("unable to format import: %w", err) + } + + return ret, nil +} + +// processPackageFiles processes Gno package files and collects top-level declarations. +func (p *Processor) processPackageFiles(path string, pkg Package) *parsedPackage { + pkgc, ok := p.parsedPackage[path] + if ok { + return pkgc + } + + pkgc = &parsedPackage{ + decls: make(map[*ast.Object]ast.Decl), + files: map[string]*ast.File{}, + } + pkgc.error = ReadWalkPackage(pkg, func(filename string, r io.Reader, err error) error { + if err != nil { + return fmt.Errorf("unable to read %q: %w", filename, err) + } + + file, err := p.parseFile(filename, r) + if err != nil { + return err + } + + collectTopDeclaration(file, pkgc.decls) + pkgc.files[filename] = file + return nil + }) + p.parsedPackage[path] = pkgc + + return pkgc +} + +// collectTopDeclaration collects top-level declarations from a single file. +func collectTopDeclaration(file *ast.File, topDecls map[*ast.Object]ast.Decl) { + for _, decl := range file.Decls { + switch d := decl.(type) { + case *ast.GenDecl: + for _, spec := range d.Specs { + switch s := spec.(type) { + case *ast.TypeSpec: + topDecls[s.Name.Obj] = d + case *ast.ValueSpec: + for _, name := range s.Names { + topDecls[name.Obj] = d + } + } + } + case *ast.FuncDecl: + // Check for top-level function + if d.Recv == nil && d.Name != nil && d.Name.Obj != nil { + topDecls[d.Name.Obj] = d + } + } + } +} + +// collectUnresolved collects unresolved identifiers and declarations. +func collectUnresolved(file *ast.File, topDecls map[*ast.Object]ast.Decl) map[string]map[string]bool { + unresolved := map[string]map[string]bool{} + unresolvedList := []*ast.Ident{} + for _, u := range file.Unresolved { + if _, ok := unresolved[u.Name]; ok { + continue + } + + if isPredeclared(u.Name) { + continue + } + + unresolved[u.Name] = map[string]bool{} + unresolvedList = append(unresolvedList, u) + } + + ast.Inspect(file, func(n ast.Node) bool { + switch e := n.(type) { + case *ast.Ident: + if d := topDecls[e.Obj]; d != nil { + delete(unresolved, e.Name) + } + case *ast.SelectorExpr: + for _, u := range unresolvedList { + if u == e.X { + ident := e.X.(*ast.Ident) + unresolved[ident.Name][e.Sel.Name] = true + break + } + } + } + + return true + }) + + // Delete unresolved identifier without any selector + for u, v := range unresolved { + if len(v) == 0 { // no selector + delete(unresolved, u) + } + } + + return unresolved +} + +// cleanupPreviousImports removes resolved imports from the unresolved list. +func (p *Processor) cleanupPreviousImports(node *ast.File, knownDecls map[*ast.Object]ast.Decl, unresolved map[string]map[string]bool) { + imports := astutil.Imports(p.fset, node) + for _, imps := range imports { + for _, imp := range imps { + pkgpath := imp.Path.Value[1 : len(imp.Path.Value)-1] // unquote the value + + name := filepath.Base(pkgpath) + if pkg := p.resolver.ResolvePath(pkgpath); pkg != nil { + name = pkg.Name() + } + + isNamedImport := imp.Name != nil && imp.Name.Name != "_" + if isNamedImport { + name = imp.Name.Name + } + + if _, ok := unresolved[name]; ok { + delete(unresolved, name) + continue + } + + if isNamedImport { + astutil.DeleteNamedImport(p.fset, node, name, pkgpath) + } else { + astutil.DeleteImport(p.fset, node, pkgpath) + } + } + } + + // Mark knownDecls as resolved + for obj := range knownDecls { + delete(unresolved, obj.Name) + } +} + +// resolve tries to resolve unresolved package using `Resolver` +func (p *Processor) resolve( + node *ast.File, + unresolved map[string]map[string]bool, +) { + for decl, sels := range unresolved { + for _, pkg := range p.resolver.ResolveName(decl) { + if !hasDeclExposed(p, sels, pkg) { + continue + } + + astutil.AddImport(p.fset, node, pkg.Path()) + delete(unresolved, decl) + break + } + } +} + +// hasDeclExposed checks if declarations are exposed in the specified path. +func hasDeclExposed(p *Processor, decls map[string]bool, pkg Package) bool { + exposed := p.processPackageFiles(pkg.Path(), pkg) + if exposed.error != nil { + return false + } + + for obj := range exposed.decls { + if !ast.IsExported(obj.Name) { + continue + } + + if decls[obj.Name] { + return true + } + } + + return false +} diff --git a/gnovm/pkg/gnofmt/resolver.go b/gnovm/pkg/gnofmt/resolver.go new file mode 100644 index 00000000000..7441f488b5f --- /dev/null +++ b/gnovm/pkg/gnofmt/resolver.go @@ -0,0 +1,110 @@ +package gnofmt + +import ( + "fmt" + "go/token" + "io/fs" + "path/filepath" + "strings" +) + +type Resolver interface { + // ResolveName should resolve the given package name by returning a list + // of packages matching the given name + ResolveName(pkgname string) []Package + // ResolvePath should resolve the given package path by returning a + // single package + ResolvePath(pkgpath string) Package +} + +type FSResolver struct { + fset *token.FileSet + visited map[string]bool + pkgpath map[string]Package // pkg path -> pkg + pkgs map[string][]Package // pkg name -> []pkg +} + +func NewFSResolver() *FSResolver { + return &FSResolver{ + fset: token.NewFileSet(), + visited: map[string]bool{}, + pkgpath: map[string]Package{}, + pkgs: map[string][]Package{}, + } +} + +func (r *FSResolver) ResolveName(pkgname string) []Package { + // First stdlibs, then external packages + return r.pkgs[pkgname] +} + +func (r *FSResolver) ResolvePath(pkgpath string) Package { + return r.pkgpath[pkgpath] +} + +// PackageHandler is a callback passed to the resolver during package loading. +// PackageHandler will be called on each package. If no error is passed, that +// means that the package has been fully loaded. +// If any handled error is returned from the handler, the package process will +// immediately stop. +type PackageHandler func(path string, err error) error + +func basicPkgHandler(path string, err error) error { + return err +} + +// LoadPackages lists all packages in the directory (excluding those which can't be processed). +func (r *FSResolver) LoadPackages(root string, pkgHandler PackageHandler) error { + if pkgHandler == nil { + pkgHandler = basicPkgHandler + } + + err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err // skip error + } + + if !d.IsDir() { + return nil + } + + if strings.HasPrefix(d.Name(), ".") { + return filepath.SkipDir + } + + // Skip already visited dir + if r.visited[path] { + return filepath.SkipDir + } + r.visited[path] = true + + pkg, err := ParsePackage(r.fset, root, path) + if err != nil { + return pkgHandler( + path, + fmt.Errorf("unable to inspect package %q: %w", path, err), + ) + } + + if pkg == nil || pkg.Path() == "" { + // not a package + return nil + } + + // Check for conflict with previous import path + if _, ok := r.pkgpath[pkg.Path()]; ok { + // Stop on path conflict, has a package path should be uniq + return pkgHandler( + path, + fmt.Errorf("%q has been declared twice", pkg.Path()), + ) + } + + r.pkgpath[pkg.Path()] = pkg + r.pkgs[pkg.Name()] = append(r.pkgs[pkg.Name()], pkg) + + return pkgHandler(path, nil) + }) + + return err +} diff --git a/gnovm/pkg/gnofmt/resolver_mock.go b/gnovm/pkg/gnofmt/resolver_mock.go new file mode 100644 index 00000000000..8ee35f3198f --- /dev/null +++ b/gnovm/pkg/gnofmt/resolver_mock.go @@ -0,0 +1,86 @@ +package gnofmt + +import ( + "bytes" + "fmt" + "io" +) + +type mockResolver struct { + pkgspath map[string]Package // pkg path -> pkg + pkgs map[string][]Package // pkg name -> []pkg +} + +func newMockResolver() *mockResolver { + return &mockResolver{ + pkgspath: make(map[string]Package), + pkgs: make(map[string][]Package), + } +} + +type mockPackage struct { + PkgPath string + PkgName string + filesname []string + files [][]byte +} + +func newMockedPackage(path, name string) *mockPackage { + return &mockPackage{PkgPath: path, PkgName: name} +} + +func (m *mockPackage) AddFile(filename string, body []byte) { + m.filesname = append(m.filesname, filename) + m.files = append(m.files, body) +} + +// Should return the package path +func (m *mockPackage) Path() string { + return m.PkgPath +} + +// Should return the name of the as definied at the top level of each +// files +func (m *mockPackage) Name() string { + return m.PkgName +} + +// Should return all gno filename inside the package +func (m *mockPackage) Files() []string { + return m.filesname +} + +// ReaderCloser wraps an io.Reader and provides a no-op Close method. +type readerCloser struct { + io.Reader +} + +func (readerCloser) Close() error { return nil } + +// Should return a content reader for the the given filename within the package +func (m *mockPackage) Read(filename string) (io.ReadCloser, error) { + for i, file := range m.filesname { + if file != filename { + continue + } + + r := bytes.NewReader(m.files[i]) + return &readerCloser{r}, nil + } + + return nil, fmt.Errorf("file not found %q", filename) +} + +func (m *mockResolver) AddPackage(pkg Package) []Package { + m.pkgs[pkg.Name()] = append(m.pkgs[pkg.Name()], pkg) + m.pkgspath[pkg.Path()] = pkg + return nil +} + +func (m *mockResolver) ResolveName(pkgname string) []Package { + return m.pkgs[pkgname] +} + +func (m *mockResolver) ResolvePath(pkgpath string) Package { + return m.pkgspath[pkgpath] +} diff --git a/gnovm/pkg/gnofmt/utils.go b/gnovm/pkg/gnofmt/utils.go new file mode 100644 index 00000000000..95e0fb53c3b --- /dev/null +++ b/gnovm/pkg/gnofmt/utils.go @@ -0,0 +1,69 @@ +package gnofmt + +import ( + "path/filepath" + "strings" +) + +func isGnoFile(name string) bool { + return filepath.Ext(name) == ".gno" && !strings.HasPrefix(name, ".") +} + +func isTestFile(name string) bool { + return strings.HasSuffix(name, "_filetest.gno") || strings.HasSuffix(name, "_test.gno") +} + +// isPredeclared reports whether an identifier is predeclared. +func isPredeclared(s string) bool { + return predeclaredTypes[s] || predeclaredFuncs[s] || predeclaredConstants[s] +} + +var ( + predeclaredTypes = map[string]bool{ + "any": true, + "bool": true, + "byte": true, + "comparable": true, + "complex64": true, + "complex128": true, + "error": true, + "float32": true, + "float64": true, + "int": true, + "int8": true, + "int16": true, + "int32": true, + "int64": true, + "rune": true, + "string": true, + "uint": true, + "uint8": true, + "uint16": true, + "uint32": true, + "uint64": true, + "uintptr": true, + } + predeclaredFuncs = map[string]bool{ + "append": true, + "cap": true, + "close": true, + "complex": true, + "copy": true, + "delete": true, + "imag": true, + "len": true, + "make": true, + "new": true, + "panic": true, + "print": true, + "println": true, + "real": true, + "recover": true, + } + predeclaredConstants = map[string]bool{ + "false": true, + "iota": true, + "nil": true, + "true": true, + } +) diff --git a/gnovm/stdlibs/crypto/ed25519/ed25519_test.gno b/gnovm/stdlibs/crypto/ed25519/ed25519_test.gno index 419c3bc61a7..615ea0eb6a7 100644 --- a/gnovm/stdlibs/crypto/ed25519/ed25519_test.gno +++ b/gnovm/stdlibs/crypto/ed25519/ed25519_test.gno @@ -3,7 +3,6 @@ package ed25519 import ( "crypto/ed25519" "encoding/hex" - "std" "testing" ) diff --git a/gnovm/stdlibs/crypto/sha256/sha256_test.gno b/gnovm/stdlibs/crypto/sha256/sha256_test.gno index 2522d822307..26d96cd547e 100644 --- a/gnovm/stdlibs/crypto/sha256/sha256_test.gno +++ b/gnovm/stdlibs/crypto/sha256/sha256_test.gno @@ -3,7 +3,6 @@ package sha256 import ( "crypto/sha256" "encoding/hex" - "std" "testing" ) diff --git a/gnovm/stdlibs/hash/marshal_test.gno b/gnovm/stdlibs/hash/marshal_test.gno index b31d35faa77..bf823d97cce 100644 --- a/gnovm/stdlibs/hash/marshal_test.gno +++ b/gnovm/stdlibs/hash/marshal_test.gno @@ -10,9 +10,6 @@ package hash import ( "bytes" - "crypto/md5" - "crypto/sha1" - "crypto/sha256" "encoding" "encoding/hex" "hash" diff --git a/gnovm/stdlibs/io/io_test.gno b/gnovm/stdlibs/io/io_test.gno index 613b7d13e35..a7533a87799 100644 --- a/gnovm/stdlibs/io/io_test.gno +++ b/gnovm/stdlibs/io/io_test.gno @@ -9,7 +9,6 @@ import ( "errors" "fmt" "io" - "os" "strings" "testing" ) diff --git a/gnovm/stdlibs/math/rand/example_test.gno b/gnovm/stdlibs/math/rand/example_test.gno index 0cffec1f3d1..83da46b2a3c 100644 --- a/gnovm/stdlibs/math/rand/example_test.gno +++ b/gnovm/stdlibs/math/rand/example_test.gno @@ -7,9 +7,7 @@ package rand_test import ( "fmt" "math/rand" - "os" "strings" - "time" ) // These tests serve as an example but also make sure we don't change diff --git a/gnovm/stdlibs/math/rand/rand_test.gno b/gnovm/stdlibs/math/rand/rand_test.gno index 6e2e9cd4079..0eff890dcc8 100644 --- a/gnovm/stdlibs/math/rand/rand_test.gno +++ b/gnovm/stdlibs/math/rand/rand_test.gno @@ -8,7 +8,6 @@ import ( "errors" "fmt" "math" - "os" "testing" ) diff --git a/gnovm/stdlibs/math/rand/regress_test.gno b/gnovm/stdlibs/math/rand/regress_test.gno index 256500de66b..6ef6e1e0bf8 100644 --- a/gnovm/stdlibs/math/rand/regress_test.gno +++ b/gnovm/stdlibs/math/rand/regress_test.gno @@ -11,15 +11,6 @@ package rand -import ( - "bytes" - "fmt" - "io" - "os" - "strings" - "testing" -) - /* XXX: disabled because reflect, but would be good to enable it switching reflect to simple fn calls. diff --git a/gnovm/stdlibs/net/url/url_test.gno b/gnovm/stdlibs/net/url/url_test.gno index 0471a0a9e85..9e173becc0b 100644 --- a/gnovm/stdlibs/net/url/url_test.gno +++ b/gnovm/stdlibs/net/url/url_test.gno @@ -5,12 +5,13 @@ package url import ( - "bytes" + // encodingPkg "encoding" // "encoding/gob" "encoding/json" "fmt" "io" + // "net" "strings" "testing" diff --git a/gnovm/stdlibs/testing/random_test.gno b/gnovm/stdlibs/testing/random_test.gno index 8c1c741b2b8..7fcc00552ea 100644 --- a/gnovm/stdlibs/testing/random_test.gno +++ b/gnovm/stdlibs/testing/random_test.gno @@ -2,7 +2,6 @@ package testing import ( "math" - "strconv" "time" ) diff --git a/gnovm/stdlibs/unicode/utf16/utf16_test.gno b/gnovm/stdlibs/unicode/utf16/utf16_test.gno index b56fd81494d..11ff921c3dc 100644 --- a/gnovm/stdlibs/unicode/utf16/utf16_test.gno +++ b/gnovm/stdlibs/unicode/utf16/utf16_test.gno @@ -7,7 +7,6 @@ package utf16 import ( "testing" "unicode" - "unicode/utf16" ) type encodeTest struct { diff --git a/gnovm/tests/integ/unformated/missing_import.gno b/gnovm/tests/integ/unformated/missing_import.gno new file mode 100644 index 00000000000..03caed3433e --- /dev/null +++ b/gnovm/tests/integ/unformated/missing_import.gno @@ -0,0 +1,3 @@ +package gno + +var hello = strconv.Itoa(42)