Some checks are pending
Docs Deploy / build_and_deploy (push) Waiting to run
Generate Docs / cli (push) Waiting to run
Generate Config Doc / cli (push) Waiting to run
Go formatting / go-formatting (push) Waiting to run
Check links / markdown-link-check (push) Waiting to run
Integration / pre-test (push) Waiting to run
Integration / test on (push) Blocked by required conditions
Integration / status (push) Blocked by required conditions
Lint / Lint Go code (push) Waiting to run
Test / test (ubuntu-latest) (push) Waiting to run
359 lines
8.7 KiB
Go
359 lines
8.7 KiB
Go
// Package goanalysis provides a toolset for statically analysing Go applications
|
|
package goanalysis
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/format"
|
|
"go/parser"
|
|
"go/token"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
|
|
"golang.org/x/mod/modfile"
|
|
|
|
"github.com/ignite/cli/v29/ignite/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
mainPackage = "main"
|
|
goFileExtension = ".go"
|
|
toolsBuildTag = "//go:build tools\n\n"
|
|
)
|
|
|
|
// ErrMultipleMainPackagesFound is returned when multiple main packages found while expecting only one.
|
|
var ErrMultipleMainPackagesFound = errors.New("multiple main packages found")
|
|
|
|
// DiscoverMain finds main Go packages under path.
|
|
func DiscoverMain(path string) (pkgPaths []string, err error) {
|
|
uniquePaths := make(map[string]struct{})
|
|
|
|
err = filepath.Walk(path, func(filePath string, f os.FileInfo, err error) error {
|
|
if f.IsDir() || !strings.HasSuffix(filePath, goFileExtension) {
|
|
return err
|
|
}
|
|
|
|
parsed, err := parser.ParseFile(token.NewFileSet(), filePath, nil, parser.PackageClauseOnly)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if mainPackage == parsed.Name.Name {
|
|
dir := filepath.Dir(filePath)
|
|
uniquePaths[dir] = struct{}{}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for path := range uniquePaths {
|
|
pkgPaths = append(pkgPaths, path)
|
|
}
|
|
|
|
return pkgPaths, nil
|
|
}
|
|
|
|
// DiscoverOneMain tries to find only one main Go package under path.
|
|
func DiscoverOneMain(path string) (pkgPath string, err error) {
|
|
pkgPaths, err := DiscoverMain(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
count := len(pkgPaths)
|
|
if count == 0 {
|
|
return "", errors.New("main package cannot be found")
|
|
}
|
|
if count > 1 {
|
|
return "", ErrMultipleMainPackagesFound
|
|
}
|
|
|
|
return pkgPaths[0], nil
|
|
}
|
|
|
|
// FuncVarExists finds a genesis variable goImport into the go file.
|
|
func FuncVarExists(f *ast.File, goImport, methodSignature string) bool {
|
|
var (
|
|
importAlias = ""
|
|
imports = FormatImports(f)
|
|
)
|
|
for alias, imp := range imports {
|
|
if imp == goImport {
|
|
importAlias = alias
|
|
}
|
|
}
|
|
if importAlias == "" {
|
|
return false
|
|
}
|
|
methodDecl := importAlias + "." + methodSignature
|
|
|
|
for _, d := range f.Decls {
|
|
if declVarExists(d, methodDecl) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// declVarExists find a variable declaration into a ast.Decl.
|
|
func declVarExists(decl ast.Decl, methodDecl string) bool {
|
|
switch d := decl.(type) {
|
|
case *ast.FuncDecl:
|
|
for _, stmt := range d.Body.List {
|
|
switch v := stmt.(type) {
|
|
case *ast.DeclStmt:
|
|
if declVarExists(v.Decl, methodDecl) {
|
|
return true
|
|
}
|
|
case *ast.AssignStmt:
|
|
if len(v.Rhs) == 0 {
|
|
continue
|
|
}
|
|
decl, err := getCallExprName(v.Rhs[0])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if decl == methodDecl {
|
|
return true
|
|
}
|
|
case *ast.IfStmt:
|
|
stmt, ok := v.Init.(*ast.AssignStmt)
|
|
if !ok || len(stmt.Rhs) == 0 {
|
|
continue
|
|
}
|
|
decl, err := getCallExprName(stmt.Rhs[0])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if decl == methodDecl {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
case *ast.GenDecl:
|
|
decls, err := getGenDeclNames(d)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if slices.Contains(decls, methodDecl) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// getGenDeclNames returns a list of the method declaration inside the ast.GenDecl.
|
|
func getGenDeclNames(genDecl *ast.GenDecl) ([]string, error) {
|
|
if genDecl.Tok != token.VAR {
|
|
return nil, errors.Errorf("genDecl is not a var token: %v", genDecl.Tok)
|
|
}
|
|
var decls []string
|
|
for _, spec := range genDecl.Specs {
|
|
valueDecl, ok := spec.(*ast.ValueSpec)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, id := range valueDecl.Names {
|
|
vSpec, ok := id.Obj.Decl.(*ast.ValueSpec)
|
|
if !ok || len(vSpec.Values) == 0 {
|
|
continue
|
|
}
|
|
|
|
cursorDecl, err := getCallExprName(vSpec.Values[0])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
decls = append(decls, cursorDecl)
|
|
}
|
|
}
|
|
if len(decls) == 0 {
|
|
return nil, errors.Errorf("empty method declarations")
|
|
}
|
|
return decls, nil
|
|
}
|
|
|
|
// getCallExprName returns the method declaration inside the ast.Expr.
|
|
func getCallExprName(expr ast.Expr) (string, error) {
|
|
call, ok := expr.(*ast.CallExpr)
|
|
if !ok {
|
|
return "", errors.Errorf("expression is not a *ast.CallExpr: %v", expr)
|
|
}
|
|
sel, ok := call.Fun.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return "", errors.Errorf("expression function is not a *ast.SelectorExpr: %v", call.Fun)
|
|
}
|
|
|
|
x, ok := sel.X.(*ast.Ident)
|
|
if !ok {
|
|
return "", errors.Errorf("selector expression function is not a *ast.Ident: %v", sel.X)
|
|
}
|
|
|
|
return x.String() + "." + sel.Sel.String(), nil
|
|
}
|
|
|
|
// FindBlankImports find all blank imports ('_') into a file.
|
|
func FindBlankImports(node *ast.File) []string {
|
|
// Iterate through the import declarations and find the blank imports.
|
|
blankImports := make([]string, 0)
|
|
for _, imp := range node.Imports {
|
|
if imp.Name != nil && imp.Name.Name == "_" {
|
|
blankImports = append(blankImports, strings.ReplaceAll(imp.Path.Value, `"`, ""))
|
|
}
|
|
}
|
|
return blankImports
|
|
}
|
|
|
|
// FormatImports translate f.Imports into a map where name -> package.
|
|
// Name is the alias if declared, or the last element of the package path.
|
|
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 && imp.Name.Name != "_" && imp.Name.Name != "." {
|
|
importName = imp.Name.Name
|
|
} else {
|
|
importParts := strings.Split(imp.Path.Value, "/")
|
|
importName = importParts[len(importParts)-1]
|
|
}
|
|
|
|
name := strings.Trim(importName, "\"")
|
|
m[name] = strings.Trim(imp.Path.Value, "\"")
|
|
}
|
|
return m
|
|
}
|
|
|
|
// AddOrRemoveTools helper function to add or remove tools from the go.mod file.
|
|
func AddOrRemoveTools(f *modfile.File, writer io.Writer, importsToAdd, importsToRemove []string) error {
|
|
for _, imp := range importsToAdd {
|
|
_ = f.AddTool(imp)
|
|
}
|
|
|
|
for _, imp := range importsToRemove {
|
|
_ = f.DropTool(imp)
|
|
}
|
|
|
|
data, err := f.Format()
|
|
if err != nil {
|
|
return errors.Errorf("failed to format go.mod file: %w", err)
|
|
}
|
|
|
|
_, err = writer.Write(data)
|
|
return err
|
|
}
|
|
|
|
// ReplaceCode replace a function implementation into a package path. The method will find
|
|
// the method signature and re-write the method implementation based in the new function.
|
|
func ReplaceCode(pkgPath, oldFunctionName, newFunction string) (err error) {
|
|
absPath, err := filepath.Abs(pkgPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fileSet := token.NewFileSet()
|
|
all, err := parser.ParseDir(fileSet, absPath, func(os.FileInfo) bool { return true }, parser.ParseComments)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, pkg := range all {
|
|
for _, f := range pkg.Files {
|
|
found := false
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
if funcDecl, ok := n.(*ast.FuncDecl); ok {
|
|
// Check if the function has the name you want to replace.
|
|
if funcDecl.Name.Name == oldFunctionName {
|
|
// Replace the function body with the replacement code.
|
|
replacementExpr, err := parser.ParseExpr(newFunction)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
funcDecl.Body = &ast.BlockStmt{List: []ast.Stmt{
|
|
&ast.ExprStmt{X: replacementExpr},
|
|
}}
|
|
found = true
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
if !found {
|
|
continue
|
|
}
|
|
filePath := fileSet.Position(f.Package).Filename
|
|
outFile, err := os.Create(filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Format and write the modified AST to the output file.
|
|
if err := format.Node(outFile, fileSet, f); err != nil {
|
|
return err
|
|
}
|
|
if err := outFile.Close(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HasAnyStructFieldsInPkg finds the struct within a package folder and checks
|
|
// if any of the fields are defined in the struct.
|
|
func HasAnyStructFieldsInPkg(pkgPath, structName string, fields []string) (bool, error) {
|
|
absPath, err := filepath.Abs(pkgPath)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
fileSet := token.NewFileSet()
|
|
all, err := parser.ParseDir(fileSet, absPath, nil, parser.ParseComments)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
fieldsNames := make(map[string]struct{})
|
|
for _, field := range fields {
|
|
fieldsNames[strings.ToLower(field)] = struct{}{}
|
|
}
|
|
|
|
exist := false
|
|
for _, pkg := range all {
|
|
for _, f := range pkg.Files {
|
|
ast.Inspect(f, func(x ast.Node) bool {
|
|
typeSpec, ok := x.(*ast.TypeSpec)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
if _, ok := typeSpec.Type.(*ast.StructType); !ok ||
|
|
typeSpec.Name.Name != structName ||
|
|
typeSpec.Type == nil {
|
|
return true
|
|
}
|
|
|
|
// Check if the struct has fields.
|
|
structType, ok := typeSpec.Type.(*ast.StructType)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
// Iterate through the fields of the struct.
|
|
for _, field := range structType.Fields.List {
|
|
for _, fieldName := range field.Names {
|
|
if _, ok := fieldsNames[strings.ToLower(fieldName.Name)]; !ok {
|
|
continue
|
|
}
|
|
exist = true
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
}
|
|
return exist, nil
|
|
}
|