Automating static analysis for Haskell projects.

| Subscribe via RSS.

I am a big believer in the maintenance benefit of using static analysis tools.

As a 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 time on the former. Static analysis tools can greatly reduce the manual work necessary for the latter.

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

As highlighted in the ‘Software project maintenance is where Haskell shines’ article, Haskell provides great guarantees in the long run of a project. Testability is important:

Static analysis: It may go without saying, but Haskell’s static type system brings substantial potential for eliminating whole classes of bugs, and maintaining invariants while changing software, as a continuous feedback to the developer.

Yes, Haskell’s type system has many powerful features but I am also interested in looking to the library ecosystem for complementary tooling. In this post, I outline my current configuration for providing more guarantees.

I usually drive all of this from a Makefile and have my build fail 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 use HLint and stylish-haskell. They’re reliable tools. There’s also hindent, which is great but I prefer the ‘hands off’ approach of stylish-haskell. 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 would be:

matrix:
  include:
  - 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:

script:
- |
  set -ex
  case "$BUILD" in
    stack)
      make test
      ;;
    hlint)
      make hlint
      ;;
    stylish-haskell)
      make stylish_haskell
      ;;
  esac
  set +ex

When building the project, I ask GHC to annoy me about all the warnings it can find. I pass the --pedantic flag to the build command so that a warning causes a failure:

# In my package.yaml (https://github.com/sol/hpack)
ghc-options:
- "-Wall"

In order to setup HLint, I create a HLint.hs configuration file:

module HLint.HLint where

import "hint" HLint.Default
import "hint" HLint.Builtin.All

ignore "Redundant do"
ignore "Eta reduce"
ignore "Use fmap"

And then add the following Make targets:

hlint_install:
	stack install hlint
.PHONY: hlint_install

hlint: 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 very 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:

stylish_haskell_install:
	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, and if any are made, that means I have some unsolved issue which forces the differential to return an exit code of 1, and the build fails.

That’s it! For an example of all this up and running in a project, check out tasty-discover.