Some checks failed
Docs Deploy / build_and_deploy (push) Has been cancelled
Generate Docs / cli (push) Has been cancelled
Generate Config Doc / cli (push) Has been cancelled
Go formatting / go-formatting (push) Has been cancelled
Check links / markdown-link-check (push) Has been cancelled
Integration / pre-test (push) Has been cancelled
Integration / test on (push) Has been cancelled
Integration / status (push) Has been cancelled
Lint / Lint Go code (push) Has been cancelled
Test / test (ubuntu-latest) (push) Has been cancelled
445 lines
11 KiB
Go
445 lines
11 KiB
Go
package modulecreate
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/format"
|
|
"go/parser"
|
|
"go/token"
|
|
"strings"
|
|
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/errors"
|
|
)
|
|
|
|
type addModuleAppConfigOptions struct {
|
|
skipConfig bool
|
|
runtimeFields []string
|
|
}
|
|
|
|
type AddModuleAppConfigOption func(*addModuleAppConfigOptions)
|
|
|
|
func SkipConfigEntry() AddModuleAppConfigOption {
|
|
return func(opts *addModuleAppConfigOptions) {
|
|
opts.skipConfig = true
|
|
}
|
|
}
|
|
|
|
// SpecifyModuleEntry allows to define to which field the module should be added in the app config.
|
|
// E.g. "PreBlockers", "InitGenesis", "BeginBlockers", "EndBlockers".
|
|
func SpecifyModuleEntry(fields ...string) AddModuleAppConfigOption {
|
|
return func(opts *addModuleAppConfigOptions) {
|
|
opts.runtimeFields = fields
|
|
}
|
|
}
|
|
|
|
// AddModuleToAppConfig appends a given module to the chain app config.
|
|
func AddModuleToAppConfig(content, moduleName string, opts ...AddModuleAppConfigOption) (string, error) {
|
|
options := addModuleAppConfigOptions{}
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
return AddModuleToAppConfigWithOptions(content, moduleName, options)
|
|
}
|
|
|
|
// AddModuleToAppConfigWithOptions appends a given module to the chain app config with options.
|
|
func AddModuleToAppConfigWithOptions(content, moduleName string, opts addModuleAppConfigOptions) (string, error) {
|
|
fileSet := token.NewFileSet()
|
|
file, err := parser.ParseFile(fileSet, "", content, parser.ParseComments)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
commentMap := ast.NewCommentMap(fileSet, file, file.Comments)
|
|
|
|
appConfigLit, err := findAppConfigCompositeLiteral(file)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
modulesField, err := findKeyValueByName(appConfigLit, "Modules")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
runtimeModuleLit, err := findRuntimeModuleCompositeLiteral(file, modulesField.Value, fileSet)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
fields := opts.runtimeFields
|
|
if len(fields) == 0 {
|
|
fields = []string{"InitGenesis", "BeginBlockers", "EndBlockers"}
|
|
}
|
|
|
|
for _, fieldName := range fields {
|
|
if err := appendModuleNameToRuntimeField(file, runtimeModuleLit, fieldName, moduleName, fileSet); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
if !opts.skipConfig {
|
|
if err := appendModuleConfigEntry(file, modulesField.Value, moduleName, fileSet); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
file.Comments = commentMap.Filter(file).Comments()
|
|
|
|
var buf bytes.Buffer
|
|
if err := format.Node(&buf, fileSet, file); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
formatted, err := format.Source(buf.Bytes())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return string(formatted), nil
|
|
}
|
|
|
|
func findAppConfigCompositeLiteral(file *ast.File) (*ast.CompositeLit, error) {
|
|
for _, decl := range file.Decls {
|
|
genDecl, ok := decl.(*ast.GenDecl)
|
|
if !ok || genDecl.Tok != token.VAR {
|
|
continue
|
|
}
|
|
|
|
for _, spec := range genDecl.Specs {
|
|
valueSpec, ok := spec.(*ast.ValueSpec)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for i, name := range valueSpec.Names {
|
|
if name.Name != "appConfig" && name.Name != "AppConfig" {
|
|
continue
|
|
}
|
|
if len(valueSpec.Values) == 0 {
|
|
return nil, errors.Errorf("%s has no value", name.Name)
|
|
}
|
|
|
|
valueIdx := i
|
|
if valueIdx >= len(valueSpec.Values) {
|
|
valueIdx = 0
|
|
}
|
|
|
|
return findCompositeLiteralByType(file, valueSpec.Values[valueIdx], "appv1alpha1", "Config")
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, errors.New("app config variable not found")
|
|
}
|
|
|
|
func findRuntimeModuleCompositeLiteral(
|
|
file *ast.File,
|
|
modulesExpr ast.Expr,
|
|
fileSet *token.FileSet,
|
|
) (*ast.CompositeLit, error) {
|
|
modulesLit, err := resolveCompositeLiteral(file, modulesExpr)
|
|
if err != nil {
|
|
return nil, errors.Errorf("resolve modules list: %w", err)
|
|
}
|
|
|
|
for _, elt := range modulesLit.Elts {
|
|
moduleConfigLit, err := resolveCompositeLiteral(file, elt)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
nameField, err := findKeyValueByName(moduleConfigLit, "Name")
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
nameValue, err := exprString(fileSet, nameField.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if nameValue != "runtime.ModuleName" {
|
|
continue
|
|
}
|
|
|
|
configField, err := findKeyValueByName(moduleConfigLit, "Config")
|
|
if err != nil {
|
|
return nil, errors.Errorf("runtime module config field not found: %w", err)
|
|
}
|
|
|
|
return findCompositeLiteralByType(file, configField.Value, "runtimev1alpha1", "Module")
|
|
}
|
|
|
|
return nil, errors.New("runtime module not found in app config")
|
|
}
|
|
|
|
func appendModuleNameToRuntimeField(
|
|
file *ast.File,
|
|
runtimeModuleLit *ast.CompositeLit,
|
|
fieldName, moduleName string,
|
|
fileSet *token.FileSet,
|
|
) error {
|
|
field, err := findKeyValueByName(runtimeModuleLit, fieldName)
|
|
if err != nil {
|
|
return errors.Errorf("%s field not found in runtime module: %w", fieldName, err)
|
|
}
|
|
|
|
listLit, err := resolveCompositeLiteral(file, field.Value)
|
|
if err != nil {
|
|
return errors.Errorf("resolve %s list: %w", fieldName, err)
|
|
}
|
|
|
|
moduleExprText := fmt.Sprintf("%smoduletypes.ModuleName", moduleName)
|
|
normalizedModuleExpr := normalizedExpr(moduleExprText)
|
|
|
|
for _, elt := range listLit.Elts {
|
|
existing, err := exprString(fileSet, elt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if normalizedExpr(existing) == normalizedModuleExpr {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
appendCompositeLiteralElement(fileSet, listLit, moduleExprText)
|
|
return nil
|
|
}
|
|
|
|
func appendModuleConfigEntry(
|
|
file *ast.File,
|
|
modulesExpr ast.Expr,
|
|
moduleName string,
|
|
fileSet *token.FileSet,
|
|
) error {
|
|
modulesLit, err := resolveCompositeLiteral(file, modulesExpr)
|
|
if err != nil {
|
|
return errors.Errorf("resolve modules list: %w", err)
|
|
}
|
|
|
|
moduleNameText := fmt.Sprintf("%smoduletypes.ModuleName", moduleName)
|
|
moduleNamePattern := normalizedExpr(fmt.Sprintf("Name:%s", moduleNameText))
|
|
|
|
for _, elt := range modulesLit.Elts {
|
|
existingExpr, err := exprString(fileSet, elt)
|
|
if err == nil && strings.Contains(normalizedExpr(existingExpr), moduleNamePattern) {
|
|
return nil
|
|
}
|
|
|
|
moduleConfigLit, err := resolveCompositeLiteral(file, elt)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
nameField, err := findKeyValueByName(moduleConfigLit, "Name")
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
existingName, err := exprString(fileSet, nameField.Value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if existingName == moduleNameText {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
newEntry := ast.NewIdent(fmt.Sprintf(
|
|
`{
|
|
Name: %smoduletypes.ModuleName,
|
|
Config: appconfig.WrapAny(&%smoduletypes.Module{}),
|
|
}`,
|
|
moduleName,
|
|
moduleName,
|
|
))
|
|
|
|
appendCompositeLiteralElement(fileSet, modulesLit, newEntry.Name)
|
|
return nil
|
|
}
|
|
|
|
func findCompositeLiteralByType(
|
|
file *ast.File,
|
|
expr ast.Expr,
|
|
pkgName, typeName string,
|
|
) (*ast.CompositeLit, error) {
|
|
lit := findCompositeLiteralByTypeExpr(file, expr, pkgName, typeName, map[string]struct{}{})
|
|
if lit == nil {
|
|
return nil, errors.Errorf("composite literal %s.%s not found", pkgName, typeName)
|
|
}
|
|
return lit, nil
|
|
}
|
|
|
|
func findCompositeLiteralByTypeExpr(
|
|
file *ast.File,
|
|
expr ast.Expr,
|
|
pkgName, typeName string,
|
|
visited map[string]struct{},
|
|
) *ast.CompositeLit {
|
|
switch typedExpr := expr.(type) {
|
|
case *ast.ParenExpr:
|
|
return findCompositeLiteralByTypeExpr(file, typedExpr.X, pkgName, typeName, visited)
|
|
case *ast.UnaryExpr:
|
|
return findCompositeLiteralByTypeExpr(file, typedExpr.X, pkgName, typeName, visited)
|
|
case *ast.CallExpr:
|
|
for _, arg := range typedExpr.Args {
|
|
if lit := findCompositeLiteralByTypeExpr(file, arg, pkgName, typeName, visited); lit != nil {
|
|
return lit
|
|
}
|
|
}
|
|
case *ast.CompositeLit:
|
|
if isSelectorType(typedExpr.Type, pkgName, typeName) {
|
|
return typedExpr
|
|
}
|
|
for _, elt := range typedExpr.Elts {
|
|
keyValue, ok := elt.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if lit := findCompositeLiteralByTypeExpr(file, keyValue.Value, pkgName, typeName, visited); lit != nil {
|
|
return lit
|
|
}
|
|
}
|
|
case *ast.Ident:
|
|
if _, ok := visited[typedExpr.Name]; ok {
|
|
return nil
|
|
}
|
|
visited[typedExpr.Name] = struct{}{}
|
|
|
|
valueExpr, err := findGlobalValueExpr(file, typedExpr.Name)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
return findCompositeLiteralByTypeExpr(file, valueExpr, pkgName, typeName, visited)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func resolveCompositeLiteral(file *ast.File, expr ast.Expr) (*ast.CompositeLit, error) {
|
|
switch typedExpr := expr.(type) {
|
|
case *ast.CompositeLit:
|
|
return typedExpr, nil
|
|
case *ast.ParenExpr:
|
|
return resolveCompositeLiteral(file, typedExpr.X)
|
|
case *ast.UnaryExpr:
|
|
return resolveCompositeLiteral(file, typedExpr.X)
|
|
case *ast.Ident:
|
|
valueExpr, err := findGlobalValueExpr(file, typedExpr.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resolveCompositeLiteral(file, valueExpr)
|
|
default:
|
|
return nil, errors.Errorf("unsupported composite literal expression %T", expr)
|
|
}
|
|
}
|
|
|
|
func findGlobalValueExpr(file *ast.File, name string) (ast.Expr, error) {
|
|
for _, decl := range file.Decls {
|
|
genDecl, ok := decl.(*ast.GenDecl)
|
|
if !ok || genDecl.Tok != token.VAR {
|
|
continue
|
|
}
|
|
|
|
for _, spec := range genDecl.Specs {
|
|
valueSpec, ok := spec.(*ast.ValueSpec)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for i, varName := range valueSpec.Names {
|
|
if varName.Name != name {
|
|
continue
|
|
}
|
|
|
|
if len(valueSpec.Values) == 0 {
|
|
return nil, errors.Errorf("global variable %q has no value", name)
|
|
}
|
|
|
|
valueIdx := i
|
|
if valueIdx >= len(valueSpec.Values) {
|
|
valueIdx = 0
|
|
}
|
|
|
|
return valueSpec.Values[valueIdx], nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, errors.Errorf("global variable %q not found", name)
|
|
}
|
|
|
|
func findKeyValueByName(compLit *ast.CompositeLit, name string) (*ast.KeyValueExpr, error) {
|
|
for _, elt := range compLit.Elts {
|
|
keyValue, ok := elt.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
key, ok := keyValue.Key.(*ast.Ident)
|
|
if ok && key.Name == name {
|
|
return keyValue, nil
|
|
}
|
|
}
|
|
|
|
return nil, errors.Errorf("field %q not found", name)
|
|
}
|
|
|
|
func isSelectorType(expr ast.Expr, pkgName, typeName string) bool {
|
|
selector, ok := expr.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
pkgIdent, ok := selector.X.(*ast.Ident)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
return pkgIdent.Name == pkgName && selector.Sel.Name == typeName
|
|
}
|
|
|
|
func exprString(fileSet *token.FileSet, expr ast.Expr) (string, error) {
|
|
var buf bytes.Buffer
|
|
if err := format.Node(&buf, fileSet, expr); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return buf.String(), nil
|
|
}
|
|
|
|
func normalizedExpr(expr string) string {
|
|
expr = strings.ReplaceAll(expr, " ", "")
|
|
expr = strings.ReplaceAll(expr, "\n", "")
|
|
expr = strings.ReplaceAll(expr, "\t", "")
|
|
return expr
|
|
}
|
|
|
|
func appendCompositeLiteralElement(fileSet *token.FileSet, compLit *ast.CompositeLit, code string) {
|
|
file := fileSet.File(compLit.Pos())
|
|
maxOffset := file.Offset(compLit.Rbrace)
|
|
for _, elt := range compLit.Elts {
|
|
if pos := elt.End(); pos.IsValid() {
|
|
offset := file.Offset(pos)
|
|
if offset > maxOffset {
|
|
maxOffset = offset
|
|
}
|
|
}
|
|
}
|
|
|
|
insertPos := file.Pos(maxOffset)
|
|
value := ast.NewIdent(code)
|
|
value.NamePos = insertPos
|
|
|
|
compLit.Elts = append(compLit.Elts, value)
|
|
compLit.Rbrace += token.Pos(1)
|
|
|
|
if len(compLit.Elts) > 0 {
|
|
last := compLit.Elts[len(compLit.Elts)-1]
|
|
if file.Line(compLit.Rbrace) == file.Line(last.End())-1 {
|
|
file.AddLine(file.Offset(compLit.Rbrace))
|
|
compLit.Rbrace += token.Pos(1)
|
|
}
|
|
}
|
|
}
|