Announcing weeder-nix

Date 2024-04-27

This post announces the weeder-nix library for running weeder on a set of Haskell packages in Nix.

Why weed?

Over the years, your code will experience positive pressures. That is to say there will be pressure to add more code to it. In order to keep the code simple, there also needs to be negative pressure, i.e. pressure to remove code. Otherwise you will end up with hundreds of lines of code that are not, or no longer, used.

This unused code is useless but still needs to be maintained and compiled. Getting rid of it can save you maintenance overhead and reduce your compile times.

Weeding without weeder-nix

To get Weeder to weed your code, you first need to tell GHC to spit out .hie files using the -fwrite-ide-info flag. Then you just run weeder and see your weeds.

Easy, right? So why do you need weeder-nix?

Weeding with weeder-nix

Calling weeder from nix comes with some extra complications. Just like without weeder-nix, you need to activate -fwrite-ide-info for each of your packages, and collect the .hie files. In order to also find cross-package weeds, you also need to make sure that your packages use the versions of their dependencies with -fwrite-ide-info turned on. Otherwise you run into some false positives issues

I packaged up all the learnings of how to do that correctly in weeder-nix so that you can benefit from that. To make a weeder check, just add github:NorfairKing/weeder-nix to your flake, and call makeWeederCheck:

weeder-check = pkgs.weeder-nix.makeWeederCheck {
  weederToml = ./weeder.toml;
  packages = builtins.attrNames pkgs.haskellPackages.intrayPackages;
};

There is reference documentation about how to use weeder-nix in the README.

False-positives

Weeder will unfortunately still find some false positives. Most notably it will tell you about weeds that aren't weeds involving Template Haskell spliced functions, or syntax-related type-class instances.

This means that we need to be able to ignore those false positives in order to be able to use the weeder-check in CI.

You can add false positives in your weeder.toml configuration file. Mine usually looks something like this:

unused-types = true
type-class-roots = false
roots = [
  # General
  "^Main.main$",
  # Generated
  "^Paths_.*",
  "mkStatic",
  "widgetFile",
]
root-instances = [
  # False positives
  {class = 'IsString'},
  {class = 'IsList'},
  {instance = 'Lift Day'},
  # Generated
  ## DB
  {instance = 'AtLeastOneUniqueKey .*', module = "FooBar.DB"},
  {instance = 'OnlyOneUniqueKey .*', module = "FooBar.DB"},
  {instance = '^SymbolToField .*', module = "FooBar.DB"},
  {instance = '^ToBackendKey .*', module = "FooBar.DB"},
  {instance = '^SafeToInsert .*', module = "FooBar.DB"},
  {instance = '^PersistFieldSql .*$', module = "FooBar.DB"},
  {instance = '^PathPiece \(Key .*\)$'},
  {instance = '^ToHttpApiData \(Key .*\)$'},
  {instance = '^FromHttpApiData \(Key .*\)$'},
  {instance = '^PersistFieldSql \(Key .*\)$'},
  ## Routes
  {instance = 'RouteAttrs', module = "FooBar.Foundation"},
  {instance = 'ParseRoute', module = "FooBar.Foundation"},
]

Self-weeding

If you have been using weeder for a while, your weeder.toml file will probably have started growing weeds itself. There is an open issue about making weeder self-weeding. This would be an excellent place to start if you want to jump in and contribute.

Conclusion

Thanks to weeder-nix, I've finally been able to weed my own projects. I've removed over 5k LOC in total across over 20 projects. Now that the weeder-check is part of CI, weeds can no longer creep in over the years. Each of my Haskell templates now also comes with a weeder-check. Less code to maintain and faster compile-times, double win!

If you have a flake, we have CI ready for you

Zero-config, locally reproducible

Nix CI
Next
Getting your Haskell executable statically linked with Nix