Properly configure `typos` in `pre-commit-hooks.nix` (now `git-hooks.nix`)

Published by Philipp Schuster on

2024-04: Ongoing Upstream Discussion

There is an ongoing upstream discussion for a pull request of mine that tries to mitigate this issues of the typos integration with pre-commit-hooks.nix I present in the following. I don’t know if it is ever getting merged – many opinions, limited time, stressed maintainers, complicated edge cases, different valid variants of usage of typos, stressed me – not blaming anyone!. However, IMHO, you should configure typos as follows, and you are good to go:

Terminology & Background

  • typos is the new shining star in the sky of source code spell checkers.
  • pre-commit-hooks.nix is a seamless integration of https://pre-commit.com git hooks with Nix. (It was recently renamed to git-hooks.nix upstream.)
  • pre-commit is the binary used to combine all linters, formatters, and other checkers in a single command using a .pre-commit-config.yml. This is the tool invoked by the git hooks, however, it is a standalone component.

pre-commit doesn’t need to be used with git hooks at all. In fact, pre-commit can be used as single entry into all checks without ever being used as a git hook. So, $ pre-commit run --all-files just run all checks on the repository. I use it that way and that’s perfectly valid and productive.

My Expectation

IMHO, $ pre-commit run typos [--all-files] must behave exactly like $ typos without any doubt or exception. typos is the base, pre-commit the additional convenience. Not the other way around!

Problems

  • pre-commit is meant to pass all files that should be checked to the tools as argument, such as $ some-checker-tool src/foo.c src/bar.c include/bar.h (this information comes from .pre-commit-config.yaml)
  • typos is supposed to be invoked on the tree, i.e., $ typos
    • Using a .typos.toml file one can exclude certain files or directories.
    • This is the setup I’ve seen in dozens of projects.
    • But yes, you can also invoke typos like this: $ typos src/foo.c src/bar.c include/bar.h, but this feels wrong.
  • typos has a massive memory and CPU usage when thousands of files are passed to it as argument, even when the typos-flag --force-excluded is used
  • pre-commit-hooks.nix is structured in a way that it can’t use the excludes of the .typos.toml file (my PR is trying to fix that)
  • Just setting pass_filenames = false as default got negative feedback upstream
  • typos exclude rules uses globs whereas pre-commit uses regex. This makes the implementation harder.
  • I have a project where over 70.000 files in a folder are excluded in .typos.toml. However, pre-commit-hooks.nix creates a .pre-commit-config.yaml file that instructs pre-commit to pass ALL OF THEM to typos which let my computer explode
  • Again, I do not use the git hooks but pre-commit as centralized binary to run all checks. With the current default upstream definitions how pre-commit should invoke typos, I can’t run pre-commit run [typos] --all-files without additional configuration.

But again. It is entirely unacceptable and counter-intuitive that $ pre-commit typos --all-files behaves differently from running just $ typos with a proper .typos.toml.

Conclusion, Outlook, and Solutions

There is no one to blame for the stuck upstream discussion, just an unfortunate situation and people that use typos in different ways. I don’t think it is worth it spending more time in this time sink upstream. (Locally applicable) solutions to this problem are:

  • The typos utility should be fixed to not explode your PC when it receives that many arguments (I never investigated this, and I do not have the capacity to, unfortunately)
  • Setting hooks.typos.pass_filenames = false is perfectly fine IMHO. Just let typos figure out the excludes by itself by reading its configuration file.
  • Otherwise, you can set hooks.typos.excludes = (builtins.fromTOML (builtins.readFile ../.typos.toml).files or { }).extend-exclude or [ ]); as long as your excludes do not use globs (*).

So the following code snippet shows my solution (broken down to what’s relevant). For simplicity, the code snipped uses a Nix channel to fetch nixpkgs:

let
  pkgs = import <nixpkgs> { };
  # Commit ID from 2024-01-08
  pre-commit-hooks = import (builtins.fetchTarball "https://github.com/cachix/pre-commit-hooks.nix/tarball/ea07fa07f222a5c4baacbcdbf529276ef0ddc6ca");
in
# Configured with the module options defined in `modules/pre-commit.nix`:
pre-commit-hooks.run {
  src = ./.;

  # Set the pkgs to get the tools for the hooks from.
  tools = pkgs;

  hooks = {
    typos = {
      enable = true;
      pass_filenames = false;
      # As this is a string and not a Nix Path upstream, the excludes can
      # never be evaluated by pre-commit-hooks.nix.
      settings.configPath = ".typos.toml";
    };
  };
}

It is unfortunate that such a (IMHO) basic thing became such a time sink. However, I hope I could help you.


Philipp Schuster

Hi, I'm Philipp and interested in Computer Science. I especially like low level development, making ugly things nice, and de-mystify "low level magic".

0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *