Unused parameters in Go code

27 June 2018

Daniel Martí

Let's define an unused parameter

Sometimes parameters were never needed, or are no longer needed.

func NewTemplate(name, body, footer string) Template {
    return Template{name: name, body: body}
}

--

Sometimes a parameter should be used but isn't.

func (t Template) Execute(w io.Writer) error {
    _, err := fmt.Fprintf(os.Stdout, "%s %s", t.name, t.body)
    return err
}
2

Let's write a linter for this!

Enter our first tricky case:

func NewTemplate(name, body string) Template {
    body = sampleBody
    return Template{name: name, body: body}
}

go/ast is just the syntax, it has no semantics.

3

Let's add go/types

First algorithm in pseudo-Go:

for _, fn := range allFuncs {
    for _, param := range fn.Params {
        used := false
        for _, node := range fn.NodesRead() {
            if types.Is(node, param) {
                used = true
                break
            }
        }
        if !used {
            warnf("unused param: %v", param)
        }
    }
}
4

Limitations of go/ast and go/types

Enter our first tricky case, again:

func NewTemplate(name, body string) Template {
    body = sampleBody // not a use of the body variable!
    return Template{name: name, body: body}
}

Too many ways to define a variable:

var (t Template)
t.body = someValue

t := Template{}
t.body = someValue

t := Template{body: someValue}
5

We can make it much simpler with x/tools/go/ssa

New algorithm in pseudo-Go:

for _, param := range fn.Params {
    if len(param.Referrers()) == 0 {
        warnf("unused param: %v", param)
    }
}

Because of SSA, our first tricky case becomes something like:

func NewTemplate(name, body string) Template {
    body2 := sampleBody
    return Template{name: name, body: body2}
}
6

Common sources of false positives

Dummy or stub functions:

func (d devNull) WriteString(s string) error {
    return nil
}
func (s storeImpl) CreateFoo(f Foo) error {
    return fmt.Errorf("unimplemented")
}

Public API with compatibility guarantees:

func NewClient(ctx context.Context, timeout time.Duration) Client {
    // no longer using timeout, use context.WithTimeout instead
    return Client{ctx: ctx, ...}
}
7

Go has more potential for false positives

Funcs that satisfy an interface or func signature:

func pingHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("pong"))
}
--
os.Expand(s, func(name string) string {
    return "SameValue"
})
8

Enter our last package: x/tools/go/callgraph

Variance in time cost and accuracy:

A linter must be conservative and reasonably fast, so let's choose CHA.

9

Last version of our program:

anyStrongCall will inspect if any of the calls to fn "locks in" its signature.

if anyStrongCall(graph.Inbound[fn]) {
    continue
}
for _, param := range fn.Params {
    if len(param.Referrers()) == 0 {
        warnf("unused param: %v", param)
    }
}

Examples of these calls:

// any func passed as handler cannot change signature
func HandleRequest(handler Handler, req Request) {
    handler(req)
}

// we'd need to change otherFn too, which may not be possible
fn(otherFn())
10

Summary

You can try the end result:

go get -u mvdan.cc/unparam

Same idea, just with more code to handle endless edge cases.

11

Thank you

Use the left and right arrow keys or click the left and right edges of the page to navigate between slides.
(Press 'H' or navigate to hide this message.)