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.
