Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tools): auto migrate dependency tools #3505

Merged
merged 19 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
}
jeronimoalbi marked this conversation as resolved.
Show resolved Hide resolved

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