The Elixir ecosystem has a number of tools and third party packages to aid in keeping a code base in tip top shape. Setting up some code hygiene tools is one of the first things I do on any new project. In this series of posts we’ll go over the tools I typically use.
I noticed an interesting post on the Elixir Forum regarding the usage of
GitHooks within a project. Reading it made me really rethink the way I’ve typically setup
GitHooks on a project. It’s worked well in the past… but that was with a team of two where we had very similar dev workflows and it was easy to discuss changes to the way we wanted to structure any hooks.
As many of the posters in the above thread point out there can be legimate reasons to want to check in code that doesn’t compile or pass various checks. The most obvious being when you are stuck on something and want to check in some code so a team member can take a look. Another thing to consider is using
GitHooks imposes a particular workflow on everyone and doesn’t make concessions for the different ways people work. In my current workplace they don’t have
GitHooks set up for formatting, running test coverage etc. and I think that’s what I’ll be doing going forward as well. Leaving this kind of thing to a
CI setup and handling outdated dependencies with Dependabot seems like a reasonable and flexible solution. So… you might want to consider ignoring the
GitHook specific parts of these posts… with that caveat out of the way… back to our regularly scheduled program!
Create a sample project
We’ll create a sample project to demonstrate our code hygiene setup… so let’s create the project.
We’ll follow the instructions from the
mix phx.new output and make sure everything seems to be running.
Checking out http://localhost:4000/ everything looks as it should!
At this point I would typically make the first commit to the repo, i.e.
Like many people, I use asdf as a version manager, so the next step I would take is to add a .tool-versions file to the project indicating which version of Elixir and Erlang the project is using, i.e.
Again I would make a commit.
And now we are ready to add some code hygiene!
Add code hygiene tools
I typically add the following to any new project:
- Credo - Credo provides static code analysis and flags up potential issues and refactoring opportunities. It’s a great tool and provides many useful insights.
- ExCoveralls - ExCoveralls provides code coverage statistics. I find it’s a great way to guard against forgetting to add tests for a new piece of functionality.
- ExDoc - this is the standard Elixir documentation generation tool.
- Dialyxir - provides static code analysis and validation of Typespecs; simplifies the use of Dialyzer in Elixir projects.
- Boundary - helps manage and restrain cross-module dependencies in Elixir projects.
- GitHooks - GitHooks let’s us run tasks prior to performing a Git action such as a
push. This is helpful in avoiding pushing code that doesn’t conform to the checks we’ve put in place. It also prevents pushing code that will just fail a CI pipeline (as part of this post we’ll set up some GitHub Actions to act as a CI pipeline).
Many of these tools are pretty unobtrusive (
ExDoc) and I can’t see any reason to not include them when starting up a new project. Things are a little more open ended with
Boundary. Dialyzer can sometimes be a little flaky / annoying with Elixir projects, and for a small project Boundary might be overkill.
In anycase, let’s add the mix dependencies for the above tools.
… and grab the new dependencies…
We need to add some configuration for our newly added packages, so let’s take care of that next.
We need to make a few changes to the
mix.exs file in order for
Boundary to work.
We need to update the
project definition to include
Boundary in the
compilers attribute and
ExCoveralls requires us to add a
Next let’s set up the
We’ll create a credo config file in the root of the project, this can be accomplished via:
This creates a
.credo.exs file. There are a number of customizations that can be made to
Credo in terms of what checks get performed (see the enabled and disabled section of the configuration file) and you can even make your own custom checks.
Typically I find the default settings are sufficient, the only change I usually make is to not check for
TODO tags. You can either move this check into the disabled section of the configuration file, or set the check to false, i.e.
Note: setting the exit_status value to 0 as per the comment in the config file will still result in a warning when running
mix credo --strict… which is why we are setting the check to false instead.
With the configuration set up let’s see what our credo checks reveal.
Credo has flagged up some moduledoc warnings and some warnings related to test files. I usually disable checks on the test directory, so let’s do that by commenting out the test directory in the
For the moduledoc warnings we’ll add a
@moduledoc false attribute to the flagged modules as we don’t care about adding
moduledocs for these particular files.
Now if we run credo again?
Time to cofigure
Although not necessary, I find a custom coveralls.json file is useful when using
In the configuration we’ll skip files we don’t want included in our coverage statistics such as our test files and some of the default files generated when a new Phoenix project is created.
We’ve also set our
minimum_coverage value to 100. This is something I’ve been experimenting with. Having 100% coverage certainly does not indicate bug free code, nor is it necessarily desirable. The way I’ve been using this is to not actually enforce 100% coverage but to force any coverage gaps to be very intentional via the ignore line comments provided by
excoveralls that you can place directly into your code files, i.e.
This means I need to explicitly indicate something isn’t covered by a test. I’m still undecided on whether I like this approach or not… and it certainly might not be the right one for you.
In anycase, let’s see what coveralls has to tell us.
All good! Let’s look at
We’ll go into a more detailed set up of
Boundary in our next post so don’t worry if the below seems a little nebulous; today we’ll just get things set up with no
Boundary warnings. If we compile the project as it stands, we’ll see we’re not there yet!
To clear these up we need to add a few boundary statements throughout the code.
Adding the above
use statement establishes our main backend
code_hygiene.ex module as a boundary.
Now we need to deal with the web layer.
Here we are establishing
code_hygiene_web.ex as another boundary. We’re also specifying
CodeHygieneWeb can access our main backend module;
CodeHygiene (via the deps attribute); and we export the
Endpoint module via the exports attribute.
Finally we need to update
application.ex and indicate it is a top level boundary. It depends on both the
CodeHygieneWeb boundaries. You’ll notice that
endpoint.ex, thus why we needed to export it in our
code_hygiene_web.ex boundary configuration.
In practical terms the above boundary setup means we can access any function in
code_hygiene.ex from our Web layer. Essentially we are setting up
code_hygiene.ex as our core backend
API which all our web requests to the backend will go thru. Again we’ll go over this more in the next post.
For now, let’s see what happens if we compile the project again.
We’re going to leave our
ExDoc setup to a subsequent post where we’ll customize the output a little bit. You can generate documents right out the box however without any special configuration via:
If you navigate to the
Modules tab you’ll see documentation for the various modules in our project.
Our final bit of configuration for the day is getting GitHooks setup.
We don’t need a specific configuration file for
GitHooks, instead we just need to enhance our existing
config.exs file. We’ll add the
GitHooks configuration directly after the
We’re adding some hooks that will run before we
commit and also before we
Commits will fail if the compiler, formatter or credo pop any errors or warnings. On a
push we do the same checks but add in a coveralls check and also a check for outdated dependencies, dialyzer errors and doc errors.
The outdated dependency check (i.e.
hex.outdated) is something you might not want to include, and you will definitely want to discuss this with your team so they don’t murder you. This dependency check can be pretty annoying as it prevents a check-in when a dependency falls out of date. Similar to the 100% coverage setting in
ExCoveralls, this is something I am experimenting with and haven’t reached a definitive conclusion as to whether I like it or not. It is certainly something that you wouldn’t want around once a project becomes more mature and is deployed. When a project is first getting started I kind of like having the check, as it keeps everything completely up to date.
Anyway, let’s ensure the hook are working, we can run them locally as below.
All good, next we’ll try the
push hooks (Note: this will take a bit of time as the first run of
dialyzer is somewhat snail like… subsequent runs are faster).
While writing this post I ran into an example of the
hex.outdated check, we can see
credo has an update available and thus an attempt to push our code would fail.
We would resolve this by updating credo.
And now all is good.
Again, you’ll want to really think about whether you want
hex.outdated included as part of your checks.
A mix task to check…
all the things
Finally, I like to add a mix task as a convenience to run all the hygiene checks. We’ll make another update to
mix.exs, adding a new alias.
This way we can just run
mix check from the terminal.
All good, one more thing before we’re done… let’s add some GitHub Actions to run our checks when we push our code.
Before proceeding I’d tend to do a commit.
… and now let’s get to those actions!
Compile, test, lint etc.
We need to add a new
YAML file for the workflow in a
.github/workflows directory. We’ll name the file
build_lint_test.yml… ‘cause well that’s what it does.
This file is largely based on the examples from the setup-beam repository.
So what’s going on here?
On any push to GitHub we’ll compile, check for outdated deps (again just like with the GitHooks this is something you might not want to do) lint, run credo, and finally run our tests via coveralls. If any of these steps fail, we’ll get a notification from GitHub to indicate our push caused errors… nice!
We have GitHooks running locally, so in theory we should never hit a situation where the GitHub action fails… we’d expect the push to GitHub itself to fail locally. However, I think it’s very beneficial having these checks as part of the CI process. You don’t want to depend on local dev setups to enforce your project’s code hygiene.
We’ll also create a workflow for running
Dialyzer, we’ll only run it on the main branch since it can be a little slow.
This is a pretty simple workflow, I believe I originally sourced this from the excellent post by Trevor Brown. I’d recommend checking out his post for a more detailed explanation of this workflow.
In a subsequent post we’ll also add a GitHub action to generate our docs… but for today we’re done and dusted. Let’s check in these changes.
Now if we set up a repo in GitHub and push our code, we’ll see our workflows run… and hopefully succeed!
So that’s it for today, next time out we’ll take a closer look at
Boundary and get a better idea of the benefits it provides.
If you want to retrieve the GitHub commit that corresponds to today’s code:
You should now see the following git history.
Thanks for reading and I hope you enjoyed the post!