Skip to content

Commit

Permalink
feat(tools): auto migrate dependency tools (#3505)
Browse files Browse the repository at this point in the history
* create method to update go imports into a go file

* add tools helpers

* add migration pre handler

* fix the method to update tools import

* uncomment migrations questions

* add changelog

* idente ignite/pkg/cosmosgen/install_test.go tests

Co-authored-by: Jerónimo Albi <[email protected]>

* fix ignite/cmd/chain.go mesages

Co-authored-by: Jerónimo Albi <[email protected]>

* fix ignite/cmd/chain.go remove msg

Co-authored-by: Jerónimo Albi <[email protected]>

* ident ignite/pkg/cosmosgen/install_test.go

Co-authored-by: Jerónimo Albi <[email protected]>

* fix double ? in the ask question and improve comments

* use io.writer intead return a byte array to goanalysis.UpdateInitImports method

* fix goanalysis test package name

* Update ignite/cmd/chain.go vars

Co-authored-by: Jerónimo Albi <[email protected]>

* run gofmt

---------

Co-authored-by: Jerónimo Albi <[email protected]>
  • Loading branch information
Pantani and jeronimoalbi authored May 23, 2023
1 parent 91d0080 commit b543016
Show file tree
Hide file tree
Showing 6 changed files with 520 additions and 4 deletions.
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

- [#3505](https://github.com/ignite/cli/pull/3505) Auto migrate dependency tools

### Changes

- [#3444](https://github.com/ignite/cli/pull/3444) Add support for ICS chains in ts-client generation
Expand Down
66 changes: 63 additions & 3 deletions ignite/cmd/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
Expand All @@ -13,13 +15,16 @@ import (
"github.com/ignite/cli/ignite/pkg/cliui"
"github.com/ignite/cli/ignite/pkg/cliui/colors"
"github.com/ignite/cli/ignite/pkg/cliui/icons"
"github.com/ignite/cli/ignite/pkg/cosmosgen"
"github.com/ignite/cli/ignite/pkg/goanalysis"
"github.com/ignite/cli/ignite/pkg/xast"
)

const (
msgMigration = "Migrating blockchain config file from v%d to v%d..."
msgMigrationCancel = "Stopping because config version v%d is required to run the command"
msgMigrationPrefix = "Your blockchain config version is v%d and the latest is v%d."
msgMigrationPrompt = "Would you like to upgrade your config file to v%d"
toolsFile = "tools/tools.go"
)

// NewChain returns a command that groups sub commands related to compiling, serving
Expand Down Expand Up @@ -78,7 +83,7 @@ chain.
`,
Aliases: []string{"c"},
Args: cobra.ExactArgs(1),
PersistentPreRunE: configMigrationPreRunHandler,
PersistentPreRunE: preRunHandler,
}

// Add flags required for the configMigrationPreRunHandler
Expand All @@ -97,10 +102,65 @@ chain.
return c
}

func configMigrationPreRunHandler(cmd *cobra.Command, _ []string) (err error) {
func preRunHandler(cmd *cobra.Command, _ []string) error {
session := cliui.New()
defer session.End()

if err := configMigrationPreRunHandler(cmd, session); err != nil {
return err
}
return toolsMigrationPreRunHandler(cmd, session)
}

func toolsMigrationPreRunHandler(cmd *cobra.Command, session *cliui.Session) (err error) {
session.StartSpinner("Checking missing tools...")

appPath := flagGetPath(cmd)
toolsFilename := filepath.Join(appPath, toolsFile)
f, _, err := xast.ParseFile(toolsFilename)
if err != nil {
return err
}

missing := cosmosgen.MissingTools(f)
unused := cosmosgen.UnusedTools(f)

session.StopSpinner()
if len(missing) > 0 {
question := fmt.Sprintf(
"Some required imports are missing in %s file: %s. Would you like to add them",
toolsFilename,
strings.Join(missing, ", "),
)
if err := session.AskConfirm(question); err != nil {
missing = []string{}
}
}

if len(unused) > 0 {
question := fmt.Sprintf(
"File %s contains deprecated imports: %s. Would you like to remove them",
toolsFilename,
strings.Join(unused, ", "),
)
if err := session.AskConfirm(question); err != nil {
unused = []string{}
}
}
if len(missing) == 0 && len(unused) == 0 {
return nil
}
session.StartSpinner("Migrating tools...")

var buf bytes.Buffer
if err := goanalysis.UpdateInitImports(f, &buf, missing, unused); err != nil {
return err
}

return os.WriteFile(toolsFilename, buf.Bytes(), 0o644)
}

func configMigrationPreRunHandler(cmd *cobra.Command, session *cliui.Session) (err error) {
appPath := flagGetPath(cmd)
configPath := getConfig(cmd)
if configPath == "" {
Expand Down
42 changes: 42 additions & 0 deletions ignite/pkg/cosmosgen/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package cosmosgen
import (
"context"
"errors"
"go/ast"

"github.com/ignite/cli/ignite/pkg/goanalysis"
"github.com/ignite/cli/ignite/pkg/gocmd"
)

// DepTools necessary tools to build and run the chain.
func DepTools() []string {
return []string{
// the gocosmos plugin.
Expand All @@ -33,3 +36,42 @@ func InstallDepTools(ctx context.Context, appPath string) error {
}
return err
}

// MissingTools find missing tools import indo a *ast.File.
func MissingTools(f *ast.File) (missingTools []string) {
imports := make(map[string]string)
for name, imp := range goanalysis.FormatImports(f) {
imports[imp] = name
}

for _, tool := range DepTools() {
if _, ok := imports[tool]; !ok {
missingTools = append(missingTools, tool)
}
}
return
}

// UnusedTools find unused tools import indo a *ast.File.
func UnusedTools(f *ast.File) (unusedTools []string) {
unused := []string{
// regen protoc plugin
"github.com/regen-network/cosmos-proto/protoc-gen-gocosmos",

// old ignite repo.
"github.com/ignite-hq/cli/ignite/pkg/cmdrunner",
"github.com/ignite-hq/cli/ignite/pkg/cmdrunner/step",
}

imports := make(map[string]string)
for name, imp := range goanalysis.FormatImports(f) {
imports[imp] = name
}

for _, tool := range unused {
if _, ok := imports[tool]; ok {
unusedTools = append(unusedTools, tool)
}
}
return
}
106 changes: 106 additions & 0 deletions ignite/pkg/cosmosgen/install_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package cosmosgen_test

import (
"go/ast"
"go/token"
"strconv"
"testing"

"github.com/stretchr/testify/require"

"github.com/ignite/cli/ignite/pkg/cosmosgen"
)

func TestMissingTools(t *testing.T) {
tests := []struct {
name string
astFile *ast.File
want []string
}{
{
name: "no missing tools",
astFile: createASTFileWithImports(cosmosgen.DepTools()...),
want: nil,
},
{
name: "some missing tools",
astFile: createASTFileWithImports(
"github.com/golang/protobuf/protoc-gen-go",
"github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway",
"github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger",
),
want: []string{
"github.com/cosmos/gogoproto/protoc-gen-gocosmos",
"github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2",
},
},
{
name: "all tools missing",
astFile: createASTFileWithImports(),
want: cosmosgen.DepTools(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := cosmosgen.MissingTools(tt.astFile)
require.EqualValues(t, tt.want, got)
})
}
}

func TestUnusedTools(t *testing.T) {
tests := []struct {
name string
astFile *ast.File
want []string
}{
{
name: "all unused tools",
astFile: createASTFileWithImports(
"fmt",
"github.com/regen-network/cosmos-proto/protoc-gen-gocosmos",
"github.com/ignite-hq/cli/ignite/pkg/cmdrunner",
"github.com/ignite-hq/cli/ignite/pkg/cmdrunner/step",
),
want: []string{
"github.com/regen-network/cosmos-proto/protoc-gen-gocosmos",
"github.com/ignite-hq/cli/ignite/pkg/cmdrunner",
"github.com/ignite-hq/cli/ignite/pkg/cmdrunner/step",
},
},
{
name: "some unused tools",
astFile: createASTFileWithImports(
"fmt",
"github.com/ignite-hq/cli/ignite/pkg/cmdrunner",
),
want: []string{"github.com/ignite-hq/cli/ignite/pkg/cmdrunner"},
},
{
name: "no tools unused",
astFile: createASTFileWithImports("fmt"),
want: nil,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := cosmosgen.UnusedTools(tt.astFile)
require.EqualValues(t, tt.want, got)
})
}
}

// createASTFileWithImports helper function to create an AST file with given imports.
func createASTFileWithImports(imports ...string) *ast.File {
f := &ast.File{Imports: make([]*ast.ImportSpec, len(imports))}
for i, imp := range imports {
f.Imports[i] = &ast.ImportSpec{
Path: &ast.BasicLit{
Kind: token.STRING,
Value: strconv.Quote(imp),
},
}
}
return f
}
58 changes: 57 additions & 1 deletion ignite/pkg/goanalysis/goanalysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@ import (
"errors"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"io"
"os"
"path/filepath"
"strconv"
"strings"
)

const (
mainPackage = "main"
goFileExtension = ".go"
toolsBuildTag = "//go:build tools\n\n"
)

// ErrMultipleMainPackagesFound is returned when multiple main packages found while expecting only one.
Expand Down Expand Up @@ -199,7 +203,7 @@ func FormatImports(f *ast.File) map[string]string {
m := make(map[string]string) // name -> import
for _, imp := range f.Imports {
var importName string
if imp.Name != nil {
if imp.Name != nil && imp.Name.Name != "_" && imp.Name.Name != "." {
importName = imp.Name.Name
} else {
importParts := strings.Split(imp.Path.Value, "/")
Expand All @@ -211,3 +215,55 @@ func FormatImports(f *ast.File) map[string]string {
}
return m
}

// UpdateInitImports helper function to remove and add underscore (init) imports to an *ast.File.
func UpdateInitImports(file *ast.File, writer io.Writer, importsToAdd, importsToRemove []string) error {
// Create a map for faster lookup of items to remove
importMap := make(map[string]bool)
for _, astImport := range file.Imports {
value, err := strconv.Unquote(astImport.Path.Value)
if err != nil {
return err
}
importMap[value] = true
}
for _, removeImport := range importsToRemove {
importMap[removeImport] = false
}
for _, addImport := range importsToAdd {
importMap[addImport] = true
}

// Add the imports
for _, d := range file.Decls {
if dd, ok := d.(*ast.GenDecl); ok {
if dd.Tok == token.IMPORT {
file.Imports = make([]*ast.ImportSpec, 0)
dd.Specs = make([]ast.Spec, 0)
for imp, exist := range importMap {
if exist {
spec := createUnderscoreImport(imp)
file.Imports = append(file.Imports, spec)
dd.Specs = append(dd.Specs, spec)
}
}
}
}
}

if _, err := writer.Write([]byte(toolsBuildTag)); err != nil {
return fmt.Errorf("failed to write the build tag: %w", err)
}
return format.Node(writer, token.NewFileSet(), file)
}

// createUnderscoreImports helper function to create an AST underscore import with given path.
func createUnderscoreImport(imp string) *ast.ImportSpec {
return &ast.ImportSpec{
Name: ast.NewIdent("_"),
Path: &ast.BasicLit{
Kind: token.STRING,
Value: strconv.Quote(imp),
},
}
}
Loading

0 comments on commit b543016

Please sign in to comment.