Linting Thoughts

Linting is an one of those things that can be weirdly controversial. I say weirdly as I cannot quite understand the objections to it – it’s not like we’re all perfect typists, after all. Generally the objections range from “My editor does it for me” to “I forget to run them before pushing!” to my favorite, “They always fail in CI!”

None of those are legitimate reasons to avoid linting. We call it “linting” because it’s like the lint on your clothes: small, annoying, seems to spontaneously self-incarnate, and is trivial to remove. Linting is one of the things computers excel at.

What is Linting?

Simply put: Linting is any automated process that enforces a deterministic, agreed-on set of coding, format, style, or other technical standards.

Tip
The content you’re linting can have a great impact on the tools you use to lint, and what you can lint. For example, a statically typed language like Go can be linted in ways that a dynamically typed language like Perl cannot.

Linting is:

  • Spell-checking
  • Validating data file formats
  • Validating end-of-file and other platform issues
  • Static code checkers / analysis
  • Code style checks
  • Ensuring license headers are applied everywhere
  • Other fully automated checks for formatting, style, etc, that can be run without human intervention

Linting is not:

  • Code reviews
  • Manual testing
  • User acceptance testing
  • Anything requiring human intervention

Linting may be:

  • Running unit tests
  • Building binaries (to validate it actually builds, not deployment)

…that’s really up to you.

Linters may also suggest fixes that can then be evaluated and applied, if appropriate. This is not necessary, but is very convenient.

How do we lint?

All effective linting implementations I’ve seen have the following characteristics:

  • They use a linting framework
  • They can be run locally
  • They are run and enforced in CI/CD

Linting Frameworks

It’s certainly possible to “roll your own” approach here, but why? While there aren’t a huge number of linting frameworks, all you really need is one good, general-purpose one. (Which is why there aren’t a huge number of them.)

Pre-commit is an excellent example of a linting framework. It is Open Source, is widely used, has a large number of pre-built hooks, and is trivially extensible. Additionally, it is well-documented, well-supported, and actively maintained. It’s name implies a certain workflow: it is designed to be run before each commit. However, this is not the case. It is a general-purpose linting framework with built-in support for Git hooks, but it can be run at any time.

A framework for managing and maintaining multi-language pre-commit hooks.

Python 13.1k 827

Some out-of-the-box hooks for pre-commit

Python 5.4k 719

Pre-commit linters (“hooks”)

Note
Pre-commit “hooks” are the individual linters that are run by the pre-commit framework; they should not be confused with Git hooks.

I’ve included the base, general pre-commit-hooks repository above. Note that this is not the only repository of hooks available – there are many others, including some that are specific to a particular language or tool.

Here’s a few examples of pre-commit hooks that I’ve found useful:

Pre-commit hooks for Golang with support for monorepos, the ability to pass arguments and environment variables to all hooks, and the ability to invoke custom go tools.

Shell 293 36

A linter for YAML files.

Python 2.9k 279

A collection of git hooks for use with pre-commit

Shell 34 17

Dockerfile linter, validate inline bash, written in Haskell

Haskell 10.5k 422

A CLI and set of pre-commit hooks for jsonschema validation with built-in support for GitHub Workflows, Renovate, Azure Pipelines, and more!

Python 223 40

Fork from Yelp/detect-secrets

An enterprise friendly way of detecting and preventing secrets in code.

Python 74 45

git pre-commit hooks

Python 124 50

Generate documentation from Terraform modules in various output formats

Go 4.3k 556

Pre-commit hooks are also surprisingly easy to create.

When do we lint?

Tip
Generally speaking, at a minimum one should lint before submitting a changeset for review (that is, creating a pull request). This is a minimum level of courtesy to your peers.

I always lint before pushing – setting up a pre-push hook tends to make this impossible to forget. Others have other preferences, and that’s fine: running the linters in CI/CD ensures that they cannot be forgotten. (…and that the reviewer does not need to enforce them.)

A successful linting workflow tends to run like this:

  • Local
    • Lint at some point before pushing
  • CI
    • Lint as the first pipeline job on every push
    • Fail the entire pipeline early if linting fails

Linting Locally

Running locally is both critical and not essential. It’s critical in that if you push a change that fails linting, your changeset will fail CI. Similarly, it is not essential as you can always push a change, wait for CI to fail and then fix it. Your call.

As Pre-commit’s name implies, it was originally designed to be run before you make a commit – e.g. as a pre-commit hook. However, with modern Git workflows this can be quite time-consuming. (Imagine having to wait for your linters to run every time you create a fixup! commit – it gets old fast.) A happy balance between “every time” and “never” is to run it before you push, typically as a pre-push hook.

1
pre-commit install --install-hooks --hook-type pre-push

Regardless of the tool used to lint, leveraging a pre-push hook helps ensure that you catch any linting errors before anyone else sees them.

Lint in CI

Warning
I cannot emphasize this enough: Enforce your lint rules in CI. People didn’t stop littering because it was the right thing to do. They stopped because they were fined for it.

It is imperative that you lint in CI. Not linting in CI defeats the whole purpose of linting. We lint to catch the (typically) trivial mistakes we may make and we run CI to automate testing for mistakes. If we believed every changeset to be perfect, we wouldn’t have CI in the first place.

If you find your CI/CD pipelines failing frequently due to linting errors, you may want to examine some of your own practices. (Like, say, setting up a pre-push hook?)

If you do not enforce your linting rules in CI, you’re effectively forcing other people to do it for you. This is not only rude, but it’s also a waste of time and resources.

What do we lint?

Tip
Linting is not just for code. It can be used to validate data files, check your spelling, and even ensure that your documentation is up-to-date.

Simple answer? We lint everything we can.

If you can lint it, you should. Linting is much like brushing your teeth or washing your hands – it’s a simple, effective way to prevent problems. Also, it’s really gross, obvious to even the most casual of observers, and just downright lazy1 if you don’t.

If you’re using pre-commit, there are a huge number of pre-built hooks you can use as well as a large number of third-party hooks. It is also easy to write your own, should the need arise.

Important things to lint include:

  • platform-specific line and file endings are consistent
  • data files (e.g. JSON, YAML, TOML) are not malformed
  • code is legal and styled consistently
  • the project is buildable (e.g. it is “complete” and coherent)
  • secrets have not been committed

You have to start somewhere

Tip
Take the Boy Scout approach to linting: Every time you find lint, leave the project a little cleaner (and the linter a little smarter) than you found it.

It’s not often to possible to know all the things you need to lint from the start. Much as with testing, you’ll find additional things to lint for as your project moves forward – especially as additional people, perhaps using different operating systems, editors, or tooling, begin to contribute. I’ve found it useful to treat linting like other testing processes:

  • Lint (test) all the obvious things right off the bat; and
  • Add additional linting as you find issues.

For example, it’s fairly obvious in a Go project that running gofmt is a good idea. It is perhaps less obvious that you should lint your Markdown README.md to ensure it renders correctly – until someone pushes one that does not. Similarly, it’s fairly obvious that one should lint YAML to ensure it’s valid, but less obvious that you should enforce a consistent style of indenting lists.

Finally…

Linting is a simple, effective way to ensure that your project is clean and consistent. We created computers to help us with automated, repetitive, deterministic tasks like this – tasks that humans have a tendency to forget or overlook. Automated, enforced linting is a simple and effective way to instantly raise and ensure the overall hygiene and quality of any project.

Just do it! Linting is only ever a big deal when it is not done.


  1. I mean, come on. This isn’t the good type of lazy – this is the bad type that imperils your Hubris↩︎

0%