Clojure formatting with cljstyle
October 23, 2020Today I would like to show an approach to organize auto-formatting on a Clojure project from a practical point of view. I will describe the reasons behind choosing the tool and configuration for two different formatting styles.
Requirements for formatting tool
In my experience having forced auto-formatting on a project leads to keeping code in consistent good-looking shape, reduces some of redundant discussions about code style during code review, and eventually better code understanding for a whole team.
To have all that benefits and not spend too much time every day to keep it working the tool and approach, in general, should meet certain requirements. For instance, we should be able to:
- run formatting locally on every commit or push - so it should be fairly fast to prevent consuming our time;
- run format checking in CI on every commit;
- have exactly same results in any environment;
- configure formatting for a new project in a short time as possible.
Existing tools
For now, as I see there are several mature tools for Clojure formatting which are:
cljfmt
- a popular and really reliable choice;zprint
- a bit less popular but is mature too even offer some more features.
cljfmt
is really great I used to use it a lot, but it doesn't have a supported binary.
For me, it is one of the main points because I would like to format code often
without a need to keep REPL running.
zprint
has binary but the configuration seems a bit unintuitive to me.
Cljstyle
There is one more tool that meets almost all my requirements: cljstyle
.
Despite it is not so popular as the tools described above It has a mature
foundation cause based on the cljfmt
code. cljstyle
has a little
different rules' logic but it doesn't affect the quality of results.
It keeps all the good parts of cljfmt
and even adds a few more improvements.
The main of them are:
- standalone binary;
- can keep count of empty lines between blocks as you configure it (two by default);
- can break var's and function's definitions by lines;
- can remove trailing whitespaces and add new lines at the end of a file.
About formatting rules
For the rule's configuration, we have several options.
First, keeping default rules which mostly follow
Clojure Style Guide
except single space indentation for several forms (which is configurable too).
It is a more widespread and almost standard approach for now. It's useful for large projects which partially already have a near style like that. Because it will be faster to add rules with minor changes of a legacy codebase. Or for those who just like that formatting style.
But there is an alternative: universal Clojure style
formatting. which is described in the article "Better Clojure formatting".
I like the idea because it has a few rules, the same logic to treat
different forms (actually, there could be one exception: imports) and it is not broken
if once you will decide to add some new arbitrary macro.
Also, you don't need to reformat a body of threading macros each time you change it
to another one, for instance: ->
to some->
.
Here are the rules:
- format with two space indentation for every multi-line list starting with symbol;
- format regular multi-line other sequences by a first arg.
Possible exception for ns formatting (because it is too common):
- requires are indented by the first arg.
Cljstyle rules for universal formatting
To change default behavior and configure cljstyle
(for version >= 0.14.0
)
please place file .cljstyle
to a root of a project with content:
{:rules
{:indentation
{:indents
{#"^:?require$" [[:block 0]],
#"^:?import$" [[:block 0]],
#"^:?use$" [[:block 0]],
#"^[^ \[]" [[:inner 0]]}},
:vars {:enabled? false},
:namespaces {:enabled? false}}}
I turned off the line breaking for vars because sometimes it is convenient to have var definition in the same line with its value. Also, rewriting of namespaces has been switched off because it conflicts with custom rules and it will be done with additional rules.
The key part is under :indents
keyword. I replaced default indentation
rules with of custom ones which as I supposed ideally
should not be changed (at least too often). Let's describe the idea shortly:
#"^[^ \[]" [[:inner 0]]
- enable two spaces indentation for all multi-line lists starting from any character except opening square bracket[
- to fix case with multi-arity functions like:(defn new-server ([app] (new-server app {})) ([app options] (map->Server {:app app :options options)})))
#"^:?require$" [[:block 0]]
- other rules forrequire
,import
anduse
for formatting imports by first arg.(ns project.core (:require [clojure.string :as str] [clojure.xml :as xml] [compojure.route :as route]))
Empty lines between code blocks
By default, cljstyle
forces two empty lines between code blocks and forms.
I think it is reasonable because in my opinion, it improves readability a bit
and makes it easier for eyes to distinct different functions.
But you could disable empty line editing at all or choose some custom settings for that. For example, if you prefer a single empty line between code blocks just add the following lines to the config map next to the last keyword:
{...
:max-consecutive-blank-lines 1
:padding-lines 1}
Run formatting
Let's try to run formatting. To do that we could download the prepared binary from the releases page and run:
cljstyle check --report src
The output could be something like:
--- a/src/project/server.clj
+++ b/src/project/server.clj
@@ -4,7 +4,7 @@
(defrecord Server
- [app options server]
+ [app options server]
component/Lifecycle
Checked 8 files in 291 ms
7 correct
1 incorrect
Resulting diff has 2 lines
1 files formatted incorrectly
Now we can fix it:
cljstyle fix --report src
Checked 8 files in 279 ms
8 correct
There are some cases when you want (or need) to run formatting in a docker container.
For example, in CI. Or to use a single way to run formatting locally by hand,
on git hook, and in CI. You can simply build
on your own or use the one I published on Dockerhub
:
docker run -v $PWD:/app --rm abogoyavlensky/cljstyle cljstyle check --report src
Or with simple docker-compose file:
version: "3.8"
services:
fmt:
image: abogoyavlensky/cljstyle
command: cljstyle check --report src
volumes:
- .:/app
Also, for convenience we could move command to bash
-script or Makefile
(with some logic for choosing fmt action)
to have single "source of true" for that and avoid duplication it.
Caveats
The tool is still being evolving and has some minor drawbacks which seem not so critical to me. I noticed the following:
- multi-line lists as data structure
'()
or()
insidecase
formatted with two spaces;- solution: use if possible
(list ...)
or ignore expression with metadata:^:cljstyle/ignore
;
- solution: use if possible
the formatter throw an exception when auto-resolve namespace for a map is used#::{}
;solution: use full qualified map name#:some-module{}
;- UPD: since version
>= 0.15.0
the issue has been fixed.
One thing yet which is not actually a downside, but sometimes I miss it:
- ability to keep strict line length and align code to it.
Editors
In the official repository, there is a page
about using cljstyle
with vim
and emacs
.
I will show simple configs to format a current file or the whole project
on a keypress in VS Code
and IDEA
.
VS Code
To add fmt task you could create file .vscode/tasks.json
at the root of the project with content:
{
"version": "2.0.0",
"tasks": [
{
"label": "Run fmt",
"type": "shell",
"command": "cljstyle fix ${file}",
"presentation": {
"reveal": "silent"
}
}
]
}
If you would like format the whole project just replace ${file}
to list of dirs
whatever you want, for example: cljstyle fix src test dev
.
Then you could add a shortcut to keybindings.json
:
{
"key": "<your key sequence>",
"command": "workbench.action.tasks.runTask",
"args": "Run fmt"
}
IDEA
In IDEA
we have an option to add external tool in Settings -> Tools -> External tools -> +
.
Then configure the fmt tool as shown in the picture:
As before you could replace $FilePath$
to static dir list src dev ...
.
Then add appropriate key bindings in Settings -> Keymap
.
Check formatting on a git commit/push
The simplest possible way is to create file .git/hooks/pre-commit
at the root of project dir containing:
#!/bin/bash
set -e
cljstyle check --report src
Or you could use a bit more powerful tools like pre-commit
or Lefthook
. An example for Lefthook
:
pre-commit:
commands:
fmt:
glob: "*.{clj,cljs,cljc,edn}"
run: cljstyle check --report {staged_files}
Recap
So I described an approach for formatting Clojure code which gives an ability to choose between two different formatting styles at the same time with execution speed in any environment. We didn't touch CI configs but the idea is similar and you could pick any of CI systems and apply the fmt command there.
Thanks for reading!