Static Analysis with Haskell

As a project maintainer, I need to have confidence that new patches are appropriate both in terms of logic and style, especially when I did not write those patches. Naturally, I spend the majority of my brain power on the former. Static analysis tools can greatly reduce the work necessary for dealing with the latter. When I talk about style, I refer to typical issues like indentation, white space and alignment but also choices regarding libraries, language idioms and patterns.

With some analysis in place, patch reviews are more focused on how the problem is solved and maintainers can avoid appearing like pedantic lunatics when they deluge the contributor with style issues. The source reads uniformly across the entire project and this helps contributors learn quickly what is the expected style for their contribution. This also means that there is no need to write a style guide.

In this post, I outline how I integrate static analysis tools with my Haskell projects. I usually drive integration from a Makefile and a continuous integration setup which fails my build if one of the tools reports an issue. This may seem a bit demanding at first, and setting this up normally is, but once put in place it reduces the work load for all thereafter.

I normally use HLint and stylish-haskell. There's also hindent and brittany which have quite aggressive formatting guidelines. If I need to integrate a formatting tool into a project later in the development, I normally go with stylish-haskell, in order to preserve the usefulness of git-blame.

I typically use Travis CI for my continuous integration builds. Travis provides a useful matrix feature which allows running parallel builds. An example of this in a .travis.yml would be:

  - env: BUILD=stack
    compiler: "GHC-8.0.2"
    addons: {apt: {packages: [libgmp-dev]}}

  - env: BUILD=hlint
    compiler: "GHC-8.0.2"
    addons: {apt: {packages: [libgmp-dev]}}

  - env: BUILD=stylish-haskell
    compiler: "GHC-8.0.2"
    addons: {apt: {packages: [libgmp-dev]}}

I run different tools based off the $BUILD variable. That allows each matrix to deal with a single task, in parallel, which makes the build faster. This also keeps the Travis configuration simple:

- |
  set -ex
  case "$BUILD" in
      make test
      make hlint
      make stylish_haskell
  set +ex

When building the project, I ask GHC to fail compilation with any warning it can find:

- "-Wall"

I then create a HLint.hs configuration file:

$ hlint --default > .hlint.yaml

And add the following Make targets:

    stack install hlint
.PHONY: hlint_install

    hlint library/ test/
.PHONY: hlint

HLint will return an exit code of 1 if it reports any warnings, causing a build failure. When this does happen and if I agree with the proposed hint, I can run the following target to automatically apply it:

hlint_apply_refact: hlint_install
    stack install apply_refact
.PHONY: hlint_apply_refact

HLINT=hlint --refactor --refactor-options -i {} \;
hlint_refactor: hlint-apply-refact
    find library/ test/ -name "*.hs" -exec $(HLINT)
.PHONY: hlint_refactor

Otherwise, I will add an ignore statement to the configuration file. This makes it convenient to get HLint out of the way. In my experience, HLint is a useful tool and more often than not, I follow the suggestions.

I generate a stylish-haskell configuration file with:

$ stylish-haskell --defaults > .stylish-haskell.yaml

And add the following targets:

    stack install stylish-haskell
.PHONY: stylish_haskell_install

STYLISH=stylish-haskell -i {} \;
stylish_haskell_check: stylish_haskell_install
    find library/ test/ -name "*.hs" -exec $(STYLISH) \
    && git diff --exit-code
.PHONY: stylish_haskell

Using the -i flag allows stylish-haskell to make changes to source. If any are made, that means I have some outstanding issue which forces the git diff to return an exit code of 1 and fail the build.

That's it! To see all this integrated with a project, I've added it in tasty-discover.

Something wrong? Please raise an issue.