I wrote a linter!

Creating a linter in Go is surprisingly easy! I wrote one and integrated it with golangci-lint to diagnose "fat contexts". This post documents the process.

I wrote a linter!
Photo by Sam Carter / Unsplash

A few weeks ago, I published an article titled Context-induced performance bottleneck in Go, in which I discuss a misuse of Go's context.Context. I didn't find any linter already handling this in golangci-lint, our meta linter of choice, so I decided to create one!

πŸ‘ A new linter

πŸ—ΊοΈ Exploration

As it turns out, creating a linter is surprisingly easy. Since my intention was to add it to golangci-lint, I headed to their website and found they had published some guidance. This post from one of golangci-lint's maintainers was especially helpful and well written.

πŸ’‘
This post isn’t a tutorial. It documents my journey of writing and integrating a linter with golangci-lint.

After a few hours of tinkering, I ended up with fatcontext:

GitHub - Crocmagnon/fatcontext: detects nested contexts in loops
detects nested contexts in loops. Contribute to Crocmagnon/fatcontext development by creating an account on GitHub.

You can use it today, prebuilt binaries are available on each release.

The meat of the linter is in pkg/analyzer/analyzer.go and it fits in ~115 lines of code, including ~30 lines just to suggest a fix (which golangci-lint doesn't even support yet).

Thanks to go/analysis, with few efforts on my part, you can use fatcontext:

  • as a standalone binary,
  • with go vet,
  • with golangci-lint,
  • and probably others.

golangci-lint even requires writing new linters using go/analysis.

πŸ§ͺ Testing

Testing it was also simple (and built-in). You have to write a small test file which will trigger go/analysis test runner, and some source code as test cases. The test source should be annotated with special comments signaling to go/analysis/analysistest that we expect a report on a line. It will then ensure that your analysis is complete and precise:

  • complete: it didn't miss any expected report
  • precise: it didn't emit any false positive

Given how easy it is to write these tests, it's completely possible to create a linter with TDD, which I didn't do this time but would probably do next time.

🧺 Integration

So, now we have a proper linter, but it's not yet integrated with our toolchain of choice. From here, I had two possibilities: integrating this with golangci-lint using either their module plugin system or through Go's plugin system and keep it to ourselves, or integrating this in golangci-lint's public version. Given the issue it detects isn't specific to OVHcloud, I decided to take the public route.

πŸ’‘
The module plugin system seems relatively easy to approach, should you need it. I haven't tested it but for private linters it seems easier than the older β€œGo plugin system.”

Integrating with golangci-lint requires opening a pull request, following some advice found in the documentation:

feat: add fatcontext linter by Crocmagnon Β· Pull Request #4583 Β· golangci/golangci-lint
As described in a blog post I wrote (https://gabnotes.org/fat-contexts/), we’ve been bitten by contexts not shadowed inside loops. This linter addresses this issue. Github repo: https://github.com/…

I had a friendly and productive ping-pong with the maintainers who commented with a helpful checklist. It pushed me to add a GitHub Action workflow to run tests automatically and publish new releases of the linter, very easy thanks to goreleaser. I was fortunate enough to be available at the same time as the maintainers, so our exchange was fluid even if happening in the pull request discussion only.

A few days later, the PR was merged! I'm happy to report that fatcontext is available since golangci-lint@v1.58.0 πŸŽ‰

🌯 Conclusion

All in all, this was a very pleasant experience, full of learning and first times. If you also have an itch that can be solved by writing a linter, I highly suggest trying! At worst you'll probably learn some new stuff, at best you'll create a helpful tool for the Go community πŸ˜€

That's it for today, thank you for reading! See you in a future post 😊