# 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!"

<!--more-->

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.**

{{< admonition type=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.
{{< /admonition >}}

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](https://pre-commit.com/) 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.

{{< gh-repo-card-container >}}
  {{< gh-repo-card repo="pre-commit/pre-commit" >}}
  {{< gh-repo-card repo="pre-commit/pre-commit-hooks" >}}
{{< /gh-repo-card-container >}}

#### Pre-commit linters ("hooks")

{{< admonition >}}

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

{{< /admonition >}}


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:

{{< gh-repo-card-container >}}
  {{< gh-repo-card repo="tekwizely/pre-commit-golang" >}}
  {{< gh-repo-card repo="adrienverge/yamllint" >}}
  {{< gh-repo-card repo="syntaqx/git-hooks" >}}
  {{< gh-repo-card repo="hadolint/hadolint" >}}
  {{< gh-repo-card repo="python-jsonschema/check-jsonschema" >}}
  {{< gh-repo-card repo="ibm/detect-secrets" >}}
  {{< gh-repo-card repo="Lucas-C/pre-commit-hooks" >}}
  {{< gh-repo-card repo="terraform-docs/terraform-docs" >}}
{{< /gh-repo-card-container >}}

Pre-commit hooks are also [surprisingly easy to create](https://pre-commit.com/#new-hooks).

## When do we lint?

{{< admonition type=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.

{{< /admonition >}}

I always lint before pushing -- setting up a [pre-push
hook](https://git-scm.com/docs/githooks#_pre_push) 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.

```sh
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

{{< admonition type=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.

{{< /admonition >}}

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?

{{< admonition type=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.

{{< /admonition >}}

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 lazy[^lazy] if you don't.

[^lazy]: I mean, come on.  This isn't the good type of lazy -- this is the bad
    type that [imperils your Hubris](https://thethreevirtues.com/).

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

{{< admonition type=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.

{{< /admonition >}}

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._


---

> Author: Chris Weyl  
> URL: https://weyl.io/2024/10/linting-thoughts/  

