Skip to content

Errcheck Linter Implementation

Recently, I'm going to develop a static tool to analyze our self-defined framework syntax inside our company. Hence, I investigated a lot around the open source linters to find some ideas. This blogs talks about the implementation of errcheck. It's based on AST and relies on the node types to handle the different cases.

Usage and Shortage

Errcheck aims to check the usage of unchecked error. However, due to its implementation, errcheck is not perfect for every case because it simply utilizes the AST node types for inspection. For example, only the function calls of a() in the code below could be figured out by the error check.

package demo

func a() error {
    return nil
}

func A() {
    a() // figured out by error check
    go a() // figured out by error check
    defer a() // figured out by error check
    _ = a()
    e := a()
    _ = e

    e = a()
    e = a()
}

The output is shown below, and later we will see the code implementation to explain why.

demo/demo.go:8:3:       a()
demo/demo.go:9:6:       go a()
demo/demo.go:10:9:      defer a()

AST Node Types and Errcheck Implementation

Do we need to find out all returned errors and the track them to figure out usages? Not really, because ercheck will only inspects all the function calls and see whether the returned values contained errors and whether the returned values are received.

At the first glance, you may think about the way to find the function call without receiving its return value, as a(), go a() or defer a() shown above. A further step is to distinguish this kind of usage from the normal case like e := a(). However, this is not necessary because AST node types already analyze it for you.

Here, Go has expression statement, go statement and defer statement to these cases.

Here I explain the expression statement a bit. The expressions are calculations for result, and statements are instructions to control. The expression statement refers an expression performs as a statement, where the returned value doesn't matter.

img.png

Hence, by utilizing the go tools, we can easily distinguish these cases. That's why the implementation of visitor#Visit is fairly simple. All we need is to check whether the function returns an error or not and this is done by visitor#errorsByArg.

This requires us to check the return values(named value, pointer or tuple) to figure out the error. One thing that is worth to mention is that we need to consider the returned pointers where they satisfy the error interface.

func (v *visitor) errorsByArg(call *ast.CallExpr) []bool {
    switch t := v.typesInfo.Types[call].Type.(type) {
    case *types.Named:
        // Single return
        return []bool{isErrorType(t)}
    case *types.Pointer:
        // Single return via pointer
        return []bool{isErrorType(t)}
    case *types.Tuple:
        // Multiple returns
        s := make([]bool, t.Len())
        for i := 0; i < t.Len(); i++ {
            switch et := t.At(i).Type().(type) {
            case *types.Named:
                // Single return
                s[i] = isErrorType(et)
            case *types.Pointer:
                // Single return via pointer
                s[i] = isErrorType(et)
            default:
                s[i] = false
            }
        }
        return s
    }
    return []bool{false}
}

Note that the assignment is ignored, I think I will add details about it