Josh Goldberg
TODO

Why I Don't Lint in Git Hooks

Nov 22, 202410 minute read

TODO.

Git hooks are a great way to run small automations on your code before it gets packaged into commits and/or pushed to a server. I’m a big fan of Husky and lint-staged for connecting Git hooks to standard web dev scripts such as formatting and secrets detection. All my repository templates use Git commit hooks to at least format code with Prettier.

In addition to formatting, a lot of other repositories additionally run linting in their commit hooks. At first thought it makes sense to also lint code, especially in --fix mode to autofix complaints when possible. Heck, the tool is named lint-staged, right?

Unfortunately, modern linting practices with make linting on Git commit hooks much less desirable than they used to be. If you use any kind of cross-file linting, such as typed linting, then your commit hooks will likely be drastically slower and less comprehensive than you’d think.

Recap: Cross-File Linting

Traditional JavaScript linters -i.e. ESLint- were built on the assumption that each lint rule only looks at one file at a time. But modern uses of linting for JavaScript and TypeScript code have lint rules that need to understand other files in the project to lint each file. For example, typescript-eslint’s typed linting uses TypeScript APIs that pull in information from potentially many other files.

Cross-file linting, especially typed linting, changes how lint rules behave in two significant ways:

Because of those two significant changes, I no longer believe it worthwhile for most JavaScript or TypeScript projects to run a full lint pass on Git commit hooks. The rest of this article will dig into the details.

Performance Gaps

Typed linting is powerful but slow. Running the TypeScript APIs as part of linting necessitates running much of the TypeScript type checker. TypeScript’s performance scales linearly with the number of files in your project. Projects with several hundred files can often take many seconds to type check with TypeScript.

Most web developers -myself included- are accustomed to Git operations being very quick. I find it aggravating when a commit or push operation takes multiple seconds. I’ve seen many developers -again, myself included- grow accustomed to running operations with --no-verify to skip long tasks.

Comprehensiveness Gaps

Because of cross-file linting, changes to one file may change many other files. Any commit hook that runs a linter only on changed files might not be linting all impacted files.

That means running a linter on commit must act in one of three ways:

Most projects in the wild use the quick strategy for simplicity’s sake. But it’s not comprehensive, and I’ve seen developers be annoyed and confused from it missing important linting points.

The full strategy becomes aggravatingly slow with typed linting on large projects, per Performance Gaps. I’ve never seen a team happily choose to stick with it.

Similarly, the targeted strategy becomes slow because you need a tool such as TypeScript to understand which files impact which other files. TypeScript allows global augmentations and other strategies that make it impossible to fully analyze dependency graphs purely based on import and export statements.

Example: Floating Promise Detection

Suppose

// action.ts
export function action() {
	// ...
}
// index.ts
import { action } from "./action";

action();

Alternatives to Full Linting on Commit Hooks

CI Checks

Hybrid Linting Strategies


Liked this post? Thanks! Let the world know: