Search code examples
parsinggotypesgenerated-code

check if an expression is a custom type using "go/parser"


Situation

writing a code generator that checks fields of a struct and add validation functions using struct tags

Problem

Here I need to check if the type field is a custom type or not

ie,

Following are not custom types

int, []int,*int,[]Integer,map[string]PhoneNumber 

But the following are custom type

Integer,PhoneNumber,*PhoneNumber

I think I can do it using functions like the following that looks for exact match and may add map,[] support

func isBuiltInType(typ string) bool {
    switch typ {
            case "bool", "byte", "complex128", "complex64", "error": 
            case "float32", "float64":
            case "int", "int16", "int32", "int64", "int8":
            case "rune", "string":
            case "uint", "uint16", "uint32", "uint64", "uint8", "uintptr":
            default:
                return false
    }
    return true
}

But is there a better way to do it using parse.ParseExpr etc


Solution

  • For any kind of reliable result you will want to involve Go's type checker using the go/types package. It is not trivial to use, but there is a helpful introductory article at https://golang.org/s/types-tutorial.

    I threw together an example program, so you can see what to expect. The important bit is the isBasic function, the rest is just boilerplate to make it executable. In particlar, the AST traversal is hardcoded for the specific sample source code. I presume you already have your own code in place for that.

    The key point is that the types.Info structure contains all the type information you need to implement your own logic.

    I found github.com/fatih/astrewrite and golang.org/x/tools/go/loader helpful when dealing with code generation and/or parsing (the loader package is kind of required for complete type information).

    https://play.golang.org/p/A9hdPy-Oy-

    package main
    
    import (
        "bufio"
        "fmt"
        "go/ast"
        "go/parser"
        "go/token"
        "go/types"
        "log"
        "strings"
    )
    
    var src = strings.TrimSpace(`
    package main
    
    type T struct{}
    
    func f() {
        var _ T
        var _ *T
    
        var _ int
        var _ *int
        var _ **int
        var _ []int
        var _ []T
        var _ map[string]int
        var _ map[string]T
    }
    `)
    
    func main() {
        // Parse source
        fset := token.NewFileSet()
        f, err := parser.ParseFile(fset, "src.go", src, 0)
        if err != nil {
                log.Fatal(err)
        }
    
        // Run type checker
        info := types.Info{Types: make(map[ast.Expr]types.TypeAndValue)}
    
        _, err = (&types.Config{}).Check("mypkg", fset, []*ast.File{f}, &info)
        if err != nil {
                log.Fatal(err)
        }
    
        printSrc()
    
        // Inspect variable types in f()
        for _, varDecl := range f.Decls[1].(*ast.FuncDecl).Body.List {
                value := varDecl.(*ast.DeclStmt).Decl.(*ast.GenDecl).Specs[0].(*ast.ValueSpec)
    
                pos := fset.Position(value.Type.Pos())
                typ := info.Types[value.Type].Type
    
                fmt.Println(pos, "basic:", isBasic(typ))
        }
    }
    
    func isBasic(t types.Type) bool {
        switch x := t.(type) {
        case *types.Basic:
                return true
        case *types.Slice:
                return true
        case *types.Map:
                return true
        case *types.Pointer:
                return isBasic(x.Elem())
        default:
                return false
        }
    }
    
    func printSrc() {
        s := bufio.NewScanner(strings.NewReader(src))
        for i := 1; s.Scan(); i++ {
                fmt.Printf("%2d: %s\n", i, s.Text())
        }
        fmt.Println("")
    }