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.
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.
Pre-commit linters (“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.
Dockerfile linter, validate inline bash, written in Haskell
A CLI and set of pre-commit hooks for jsonschema validation with built-in support for GitHub Workflows, Renovate, Azure Pipelines, and more!
Fork from Yelp/detect-secrets
An enterprise friendly way of detecting and preventing secrets in code.
Generate documentation from Terraform modules in various output formats
Pre-commit hooks are also surprisingly easy to create.
When do we lint?
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.
|
|
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
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?
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
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.
-
I mean, come on. This isn’t the good type of lazy – this is the bad type that imperils your Hubris. ↩︎