diff --git a/changelog.md b/changelog.md index 72416fb332..92a8a77674 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,12 @@ ## Unreleased +## [`v29.5.0`](https://github.com/ignite/cli/releases/tag/v29.5.0) + +### Changes + +- [#4822](https://github.com/ignite/cli/pull/4822) Add more functions in `xast` package and import its debuggability. + ## [`v29.4.2`](https://github.com/ignite/cli/releases/tag/v29.4.2) ### Changes diff --git a/ignite/pkg/xast/function.go b/ignite/pkg/xast/function.go index 515c43161c..5ac7e3cece 100644 --- a/ignite/pkg/xast/function.go +++ b/ignite/pkg/xast/function.go @@ -166,6 +166,14 @@ func AppendFuncAtLine(code string, lineNumber uint64) FunctionOptions { // AppendInsideFuncCall adds an argument to a function call. For instances, the method have a parameter a // // call 'New(param1, param2)' and we want to add the param3 the result will be 'New(param1, param2, param3)'. +// AppendInsideFuncCall appends code inside a function call. +// The callName parameter can be either: +// - Simple name: "NewKeeper" matches any call to NewKeeper regardless of package/receiver +// - Qualified name: "foo.NewKeeper" matches only calls to NewKeeper with foo as the package/receiver +// +// The code parameter is the argument to insert, and index specifies the position: +// - index >= 0: insert at the specified position +// - index == -1: append at the end func AppendInsideFuncCall(callName, code string, index int) FunctionOptions { return func(c *functionOpts) { c.insideCall = append(c.insideCall, functionCall{ @@ -177,8 +185,12 @@ func AppendInsideFuncCall(callName, code string, index int) FunctionOptions { } // AppendFuncStruct adds a field to a struct literal. For instance, -// // the struct has only one parameter 'Params{Param1: param1}' and we want to add -// // the param2 the result will be 'Params{Param1: param1, Param2: param2}'. +// the struct has only one parameter 'Params{Param1: param1}' and we want to add +// the param2 the result will be 'Params{Param1: param1, Param2: param2}'. +// +// The name parameter can be either: +// - Simple name: "Keeper" matches any struct literal of type Keeper regardless of package +// - Qualified name: "keeper.Keeper" matches only struct literals with keeper as the package func AppendFuncStruct(name, param, code string) FunctionOptions { return func(c *functionOpts) { c.insideStruct = append(c.insideStruct, functionStruct{ @@ -233,7 +245,7 @@ func ModifyFunction(content string, funcName string, functions ...FunctionOption fset := token.NewFileSet() file, err := parser.ParseFile(fset, "", content, parser.ParseComments) if err != nil { - return "", errors.Errorf("failed to parse file: %w", err) + return "", errors.Errorf("failed to parse file (%s): %w", funcName, err) } cmap := ast.NewCommentMap(fset, file, file.Comments) @@ -363,7 +375,7 @@ func addNewLine(fileSet *token.FileSet, funcDecl *ast.FuncDecl, newLines []funct for _, newLine := range newLines { // Validate line number if newLine.number > uint64(len(funcDecl.Body.List))-1 { - return errors.Errorf("line number %d out of range", newLine.number) + return errors.Errorf("line number %d out of range (max %d)", newLine.number, len(funcDecl.Body.List)-1) } // Parse insertion code @@ -484,6 +496,12 @@ func exprName(expr ast.Expr) (string, bool) { case *ast.Ident: return exp.Name, true case *ast.SelectorExpr: + // Check if X is an identifier to get the package name + if ident, ok := exp.X.(*ast.Ident); ok { + // Return qualified name: package.Function + return ident.Name + "." + exp.Sel.Name, true + } + // Fallback to just the selector name if X is not an identifier return exp.Sel.Name, true default: return "", false @@ -691,16 +709,30 @@ func applyFunctionOptions(fileSet *token.FileSet, f *ast.FuncDecl, opts *functio return true } - calls, ok := callMap[name] - if !ok { + // Collect all matching calls (both qualified and unqualified names) + var allCalls functionCalls + if calls, ok := callMap[name]; ok { + allCalls = append(allCalls, calls...) + delete(callMapCheck, name) + } + + // Also check for unqualified name if this is a selector expression + if sel, isSel := expr.Fun.(*ast.SelectorExpr); isSel { + simpleName := sel.Sel.Name + if calls, ok := callMap[simpleName]; ok { + allCalls = append(allCalls, calls...) + delete(callMapCheck, simpleName) + } + } + + if len(allCalls) == 0 { return true } - if err := addFunctionCall(expr, calls); err != nil { + if err := addFunctionCall(expr, allCalls); err != nil { errInspect = err return false } - delete(callMapCheck, name) case *ast.CompositeLit: name, exist := exprName(expr.Type) @@ -708,13 +740,27 @@ func applyFunctionOptions(fileSet *token.FileSet, f *ast.FuncDecl, opts *functio return true } - structs, ok := structMap[name] - if !ok { + // Collect all matching structs (both qualified and unqualified names) + var allStructs functionStructs + if structs, ok := structMap[name]; ok { + allStructs = append(allStructs, structs...) + delete(structMapCheck, name) + } + + // Also check for unqualified name if this is a selector expression + if sel, isSel := expr.Type.(*ast.SelectorExpr); isSel { + simpleName := sel.Sel.Name + if structs, ok := structMap[simpleName]; ok { + allStructs = append(allStructs, structs...) + delete(structMapCheck, simpleName) + } + } + + if len(allStructs) == 0 { return true } - addStructs(fileSet, expr, structs) - delete(structMapCheck, name) + addStructs(fileSet, expr, allStructs) default: return true diff --git a/ignite/pkg/xast/function_test.go b/ignite/pkg/xast/function_test.go index b641f42e0f..3a3cf6aa3d 100644 --- a/ignite/pkg/xast/function_test.go +++ b/ignite/pkg/xast/function_test.go @@ -242,7 +242,7 @@ func process(x int) string { }`, 2), AppendFuncCode(`fmt.Println("Appended code.")`), AppendFuncCode(`Param{ - Baz: baz, + Baz: baz, Foo: foo, }`), NewFuncReturn("1"), @@ -363,7 +363,7 @@ func TestValidate(t *testing.T) { functionName: "TestValidate", functions: []FunctionOptions{ AppendFuncTestCase(`{ - desc: "valid genesis state", + desc: "valid genesis state", genState: GenesisState{}, }`), }, @@ -597,6 +597,123 @@ func anotherFunction() bool { return true } +// TestValidate test the validations +func TestValidate(t *testing.T) { + tests := []struct { + desc string + genState types.GenesisState + }{ + { + desc: "default is valid", + genState: types.DefaultGenesis(), + }, + { + desc: "valid genesis state", + genState: types.GenesisState{}, + }, + } + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + err := tc.genState.Validate() + require.NoError(t, err) + }) + } +}`, + }, + { + name: "add inside call modifications with qualified package name", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{ + AppendInsideFuncCall("bla.NewParam", "baz", 0), + AppendInsideFuncCall("bla.NewParam", "bla", -1), + AppendInsideFuncCall("CallSomething", strconv.Quote("test1"), -1), + }, + }, + want: `package main + +import ( + "fmt" +) + +// main function +func main() { + // print hello world + fmt.Println("Hello, world!") + // call new param function + New(param1, param2) +} + +// anotherFunction another function +func anotherFunction() bool { + // init param + p := bla.NewParam(baz, bla) + // start to call something + p.CallSomething("Another call", "test1") + // return always true + return true +} + +// TestValidate test the validations +func TestValidate(t *testing.T) { + tests := []struct { + desc string + genState types.GenesisState + }{ + { + desc: "default is valid", + genState: types.DefaultGenesis(), + }, + { + desc: "valid genesis state", + genState: types.GenesisState{}, + }, + } + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + err := tc.genState.Validate() + require.NoError(t, err) + }) + } +}`, + }, + { + name: "add inside call modifications with mixed qualified and unqualified names", + args: args{ + fileContent: existingContent, + functionName: "anotherFunction", + functions: []FunctionOptions{ + AppendInsideFuncCall("bla.NewParam", "ctx", 0), + AppendInsideFuncCall("NewParam", "baz", -1), + AppendInsideFuncCall("p.CallSomething", strconv.Quote("test1"), 0), + AppendInsideFuncCall("CallSomething", strconv.Quote("test2"), -1), + }, + }, + want: `package main + +import ( + "fmt" +) + +// main function +func main() { + // print hello world + fmt.Println("Hello, world!") + // call new param function + New(param1, param2) +} + +// anotherFunction another function +func anotherFunction() bool { + // init param + p := bla.NewParam(ctx, baz) + // start to call something + p.CallSomething("test1", "Another call", "test2") + // return always true + return true +} + // TestValidate test the validations func TestValidate(t *testing.T) { tests := []struct { @@ -632,8 +749,8 @@ import ( // anotherFunction another function func anotherFunction() bool { Param{ - Baz: baz, - Foo: foo, + Baz: baz, + Foo: foo, } Client{baz, foo} // return always true @@ -759,7 +876,7 @@ func TestValidate(t *testing.T) { functionName: "anotherFunction", functions: []FunctionOptions{AppendFuncAtLine(`fmt.Println("")`, 4)}, }, - err: errors.New("line number 4 out of range"), + err: errors.New("line number 4 out of range (max 2)"), }, { name: "invalid code for append at line", @@ -852,16 +969,16 @@ func main() { // Simple function call // print hello world fmt.Println("Hello, world!") - + // Call with multiple arguments server.Foo(param1, param2, 42) - + // Call with no arguments EmptyFunc() - + // Call with complex arguments ComplexFunc([]string{"a", "b"}, map[string]int{"a": 1}) - + // Multiple calls to the same function fmt.Println("First call") fmt.Println("Second call") @@ -895,16 +1012,16 @@ func main() { // Simple function call // print hello world fmt.Println("Modified output") - + // Call with multiple arguments server.Foo(param1, param2, 42) - + // Call with no arguments EmptyFunc() - + // Call with complex arguments ComplexFunc([]string{"a", "b"}, map[string]int{"a": 1}) - + // Multiple calls to the same function fmt.Println("Modified output") fmt.Println("Modified output") @@ -930,16 +1047,16 @@ func main() { // Simple function call // print hello world fmt.Println("Hello, world!") - + // Call with multiple arguments server.Foo(context.Background(), newParam, 123) - + // Call with no arguments EmptyFunc() - + // Call with complex arguments ComplexFunc([]string{"a", "b"}, map[string]int{"a": 1}) - + // Multiple calls to the same function fmt.Println("First call") fmt.Println("Second call") @@ -965,16 +1082,16 @@ func main() { // Simple function call // print hello world fmt.Println("Hello, world!") - + // Call with multiple arguments server.Foo(param1, param2, 42) - + // Call with no arguments EmptyFunc("new argument") - + // Call with complex arguments ComplexFunc([]string{"a", "b"}, map[string]int{"a": 1}) - + // Multiple calls to the same function fmt.Println("First call") fmt.Println("Second call") @@ -1000,16 +1117,16 @@ func main() { // Simple function call // print hello world fmt.Println("Hello, world!") - + // Call with multiple arguments server.Foo(param1, param2, 42) - + // Call with no arguments EmptyFunc() - + // Call with complex arguments ComplexFunc([]string{"x", "y", "z"}, map[string]int{"x": 10}) - + // Multiple calls to the same function fmt.Println("First call") fmt.Println("Second call") diff --git a/ignite/pkg/xast/import.go b/ignite/pkg/xast/import.go index dee06f9a9e..126303ec9f 100644 --- a/ignite/pkg/xast/import.go +++ b/ignite/pkg/xast/import.go @@ -93,3 +93,36 @@ func AppendImports(fileContent string, imports ...ImportOptions) (string, error) return buf.String(), nil } + +// RemoveImports removes import statements from the existing import block in Go source code content. +func RemoveImports(fileContent string, imports ...ImportOptions) (string, error) { + // apply global options. + opts := newImportOptions() + for _, o := range imports { + o(&opts) + } + + fileSet := token.NewFileSet() + + // Parse the Go source code content. + f, err := parser.ParseFile(fileSet, "", fileContent, parser.ParseComments) + if err != nil { + return "", err + } + cmap := ast.NewCommentMap(fileSet, f, f.Comments) + + // Remove import statements. + for _, importPath := range opts.imports { + astutil.DeleteNamedImport(fileSet, f, importPath.name, importPath.path) + } + + f.Comments = cmap.Filter(f).Comments() + + // Format the modified AST. + var buf bytes.Buffer + if err := format.Node(&buf, fileSet, f); err != nil { + return "", err + } + + return buf.String(), nil +} diff --git a/ignite/pkg/xast/import_test.go b/ignite/pkg/xast/import_test.go index 022efc7a25..2e23984281 100644 --- a/ignite/pkg/xast/import_test.go +++ b/ignite/pkg/xast/import_test.go @@ -255,3 +255,194 @@ func main() { }) } } + +func TestRemoveImports(t *testing.T) { + type args struct { + fileContent string + imports []ImportOptions + } + tests := []struct { + name string + args args + want string + err error + }{ + { + name: "remove single import statement", + args: args{ + fileContent: `package main + +import ( + "fmt" + "strings" +) + +func main() { + fmt.Println("Hello, world!") +}`, + imports: []ImportOptions{ + WithImport("strings"), + }, + }, + want: `package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, world!") +} +`, + }, + { + name: "remove multiple import statements", + args: args{ + fileContent: `package main + +import ( + "fmt" + "os" + "strconv" + st "strings" +) + +func main() { + fmt.Println("Hello, world!") +}`, + imports: []ImportOptions{ + WithNamedImport("st", "strings"), + WithImport("strconv"), + WithImport("os"), + }, + }, + want: `package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, world!") +} +`, + }, + { + name: "remove all imports", + args: args{ + fileContent: `package main + +import ( + "fmt" + "strings" +) + +func main() { + fmt.Println("Hello, world!") +}`, + imports: []ImportOptions{ + WithImport("fmt"), + WithImport("strings"), + }, + }, + want: `package main + +func main() { + fmt.Println("Hello, world!") +} +`, + }, + { + name: "remove non-existent import", + args: args{ + fileContent: `package main + +import "fmt" + +func main() { + fmt.Println("Hello, world!") +}`, + imports: []ImportOptions{ + WithImport("strings"), + }, + }, + want: `package main + +import "fmt" + +func main() { + fmt.Println("Hello, world!") +} +`, + }, + { + name: "remove named import", + args: args{ + fileContent: `package main + +import ( + "fmt" + st "strings" +) + +func main() { + fmt.Println("Hello, world!") +}`, + imports: []ImportOptions{ + WithNamedImport("st", "strings"), + }, + }, + want: `package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, world!") +} +`, + }, + { + name: "remove import from file with no imports", + args: args{ + fileContent: `package main + +func main() { + fmt.Println("Hello, world!") +}`, + imports: []ImportOptions{ + WithImport("fmt"), + }, + }, + want: `package main + +func main() { + fmt.Println("Hello, world!") +} +`, + }, + { + name: "remove empty file content", + args: args{ + fileContent: "", + imports: []ImportOptions{ + WithImport("fmt"), + }, + }, + err: errors.New("1:1: expected 'package', found 'EOF'"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := RemoveImports(tt.args.fileContent, tt.args.imports...) + if tt.err != nil { + require.Error(t, err) + require.Equal(t, tt.err.Error(), err.Error()) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +}