Consistent Clojure Formatting
Editor's note: This post was originally published in July 2020. Since then, Atomist has evolved and updated its platform and product offerings. For up-to-date information, check out the Atomist product page.
The clojure developers on our team use a fairly diverse set of editors. Emacs, intellij, and vim are all in the mix. Vscode is now starting to show up too. For us, it has never been a goal to define a standard. Our developer environments change as we experiment with new tools. However, we do standardize our code formatting. There's a practical advantage to having code that it's easier to read and review. Plus, our community already has the benefit of a great clojure style guide.
Cljfmt is a trusted code formatting tool in the Clojure community. And it doesn't just recognize unformatted code, it also fixes any problems it discovers. Most of us add it to our environments by including a :cljfmt
alias in our deps.edn
files, or adding a plugin to our Leiningen project.clj
files, or integrating it into our editors, or running it from a git hook. See the editor support in the cljfmt README for examples.
However, what if an unformatted commit still manages to slip through the cracks? We employ a background workflow to catch any unformatted commits, and fix them. We want users to be aware any time white space is being carelessly flung into their lives.
We call these background workflows Atomist Skills, and they are designed to be shared. Anyone can attach the atomist/cljfmt-skill
skill to their GitHub repositories to get the same behavior. We simply enable it on "all" of our repositories because formatting is not a contentious thing. We come to expect that checked in code is going to be formatted. When I read something like,
(defn x []
(println "hi"))
I now almost don't understand it.
Tool -> Skill
The cljfmt skill represents a particular workflow.
- Watch for any push containing
*.clj[sc]?
files - Run
cljfmt --fix
- If no fixes are made, then good, do nothing
- If fixes are made, either commit them back or raise a PR (use skill configuration to decide)
- If no fixes are made, and there is an open formatting PR merging to this branch, then close it! The branch is already back in the right format
So hopefully everyone has cljfmt
in their local dev enviroments, but if not, this skill has your back.
There are many tools that can be wrapped in workflows of this kind. For example, there are skills that detect new library versions in deps.edn or project.clj files, add missing deps.edn aliases, and add GitHub check annotations from clj-kondo linting violations. The hallmark of these is that the "skill"-wrapped tool activates itself by subscribing to events, and provides feedback in the form of pull requests, or github checks. In each case, the skill can be enabled across a diverse set of repositories. We simply enable the atomist/cljfmt-skill
skill on all of our repositories. It's just there.
The author of cljfmt, who very kindly agreed to review this post, mentioned in one of his comments that zprint, another great clojure formatting library, also fits into this workflow. In the meantime, we have also added a zprint skill, which embeds the zprint library in an identical workflow. It has been really interesting to try out the {:style :community}
configuration on a few of our internal Clojure projects. One of the big differences is that zprint might introduce new line breaks if the style dictates.
Sharing Configuration
The default formatting rules that ship with cljfmt are great. However, there are cases where you may want to add rules. For example, if you're sharing a custom macro definition that could benefit from standard indentation, there is an opportunity to push this out to all repositories that use this macro.
Over time, we moved away from push formatting fixes via PR. These days, we have our skill configured to push fixes directly to the branch that has the unformatted changes. However, other options are available.
There's a difference between a set of repositories that is transitioning to a new set of formatting rules, and one that has an established practice. Pull requests are a good way to get started. In practice, it doesn't take long to adopt a standard format.
I can't improve on the project's documentation, but I can share a few observations that I've found helpful.
-
cljfmt
will never create line breaks - it fixes existing indentation -
without custom rules, cljfmt will line up the first arg on a new line with the last arg from the previous line.
(a b c) ;; will be transformed to (a b c) ;; ---------------- (a b c) ;; will be transformed to (a b c)
-
custom indentation rules are maps of symbols to arrays composed of two rules types
[:inner x y]
[:block x]
With those primitives, you can use a repl to compose new rules for expressions that aren't covered by cljfmt's default rules.
If you start up a repl, you can experiment with some custom :indents
by evaluating expressions like:
(require '[cljfmt.core :refer [reformat-string default-indents]])
(print
(reformat-string
(slurp "my_unformatted.clj")
{:indents {'my-2-args-then-body [[:block 2]]}}))
Trying the Skill
The cljfmt skill is available to use today. We are working on releasing a set of skills that we hope will be useful for the Clojure community. If you're interested, here's a short introduction to the skill platform.
- Sign-up for a free account: sign up
- Enable the cljfmt skill
- Enable the zprint skill
Acknowedgements
Thanks to James Reeves for reviewing the content. And for cljfmt!
Editor's note: This post was originally published in July 2020. Since then, Atomist has evolved and updated its platform and product offerings. For up-to-date information, check out the Atomist product page.