go/ast y go/parser en Go: analizar código fuente, recorrer el AST y construir herramientas

El paquete go/ast expone el árbol de sintaxis abstracta de un programa Go y go/parser lo parsea a partir del código fuente. Con estas dos piezas puedes construir herramientas que analizan, transforman o generan código Go, que es exactamente lo que hacen gofmt, gopls y la mayoría de linters.

Parsear un fichero con parser.ParseFile()

package main

import (
    "fmt"
    "go/parser"
    "go/token"
)

func main() {
    src := `package main

import "fmt"

func hola(nombre string) string {
    return fmt.Sprintf("Hola, %s", nombre)
}

func main() {
    fmt.Println(hola("mundo"))
}`

    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, "ejemplo.go", src, parser.AllErrors)
    if err != nil {
        fmt.Println("Error de parse:", err)
        return
    }
    fmt.Println("Paquete:", f.Name.Name)
    fmt.Println("Declaraciones:", len(f.Decls))
}

El token.FileSet mantiene la información de posición (línea y columna) de cada nodo del AST. Es imprescindible para reportar errores con ubicación real.

Recorrer el AST con ast.Inspect()

ast.Inspect hace un recorrido depth-first del árbol. La función de callback recibe cada nodo; si devuelve true, continúa descendiendo:

import (
    "go/ast"
    "go/parser"
    "go/token"
)

func main() {
    fset := token.NewFileSet()
    f, _ := parser.ParseFile(fset, "mi.go", nil, 0)

    ast.Inspect(f, func(n ast.Node) bool {
        switch x := n.(type) {
        case *ast.FuncDecl:
            fmt.Printf("Función: %s en línea %dn",
                x.Name.Name, fset.Position(x.Pos()).Line)
        case *ast.CallExpr:
            if sel, ok := x.Fun.(*ast.SelectorExpr); ok {
                fmt.Printf("  Llamada: %s.%sn",
                    sel.X, sel.Sel.Name)
            }
        }
        return true
    })
}

Extraer todas las funciones de un paquete

func extraerFunciones(dir string) ([]string, error) {
    fset := token.NewFileSet()
    pkgs, err := parser.ParseDir(fset, dir, nil, 0)
    if err != nil {
        return nil, err
    }

    var funciones []string
    for _, pkg := range pkgs {
        for _, f := range pkg.Files {
            for _, decl := range f.Decls {
                if fn, ok := decl.(*ast.FuncDecl); ok {
                    funciones = append(funciones, fn.Name.Name)
                }
            }
        }
    }
    return funciones, nil
}

Detectar llamadas a una función concreta

Un caso de uso real: detectar todos los usos de fmt.Println en un codebase para sustituirlos por un logger estructurado:

func detectarPrintln(filename string) {
    fset := token.NewFileSet()
    f, _ := parser.ParseFile(fset, filename, nil, 0)

    ast.Inspect(f, func(n ast.Node) bool {
        call, ok := n.(*ast.CallExpr)
        if !ok {
            return true
        }
        sel, ok := call.Fun.(*ast.SelectorExpr)
        if !ok {
            return true
        }
        if fmt.Sprint(sel.X) == "fmt" && sel.Sel.Name == "Println" {
            pos := fset.Position(call.Pos())
            fmt.Printf("%s:%d: uso de fmt.Printlnn", pos.Filename, pos.Line)
        }
        return true
    })
}

Inspeccionar interfaces de un paquete

func listarInterfaces(src string) {
    fset := token.NewFileSet()
    f, _ := parser.ParseFile(fset, "src.go", src, 0)

    for _, decl := range f.Decls {
        genDecl, ok := decl.(*ast.GenDecl)
        if !ok {
            continue
        }
        for _, spec := range genDecl.Specs {
            typeSpec, ok := spec.(*ast.TypeSpec)
            if !ok {
                continue
            }
            iface, ok := typeSpec.Type.(*ast.InterfaceType)
            if !ok {
                continue
            }
            fmt.Printf("Interface %s con %d métodos:n",
                typeSpec.Name.Name, iface.Methods.NumFields())
            for _, m := range iface.Methods.List {
                fmt.Printf("  - %sn", m.Names[0].Name)
            }
        }
    }
}

Construir una herramienta real: linter de errores ignorados

Esta herramienta detecta asignaciones de error que se descartan con _:

ast.Inspect(f, func(n ast.Node) bool {
    assign, ok := n.(*ast.AssignStmt)
    if !ok {
        return true
    }
    for i, lhs := range assign.Lhs {
        ident, ok := lhs.(*ast.Ident)
        if ok && ident.Name == "_" {
            pos := fset.Position(assign.Pos())
            fmt.Printf("WARN %s:%d: posible error ignorado (posición %d)n",
                pos.Filename, pos.Line, i)
        }
    }
    return true
})

Para herramientas más avanzadas que necesiten información de tipos puedes complementar con go/types y golang.org/x/tools/go/analysis, que es el framework que usa el propio equipo de Go para escribir los checks de go vet.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP