Introducing Splint, an idiom checker/code-shape linter. It reads code forms and checks them against a set of rules, aka code patterns which have “better” or more idiomatic forms. For example, (when (pos? x) (do (y) (z))) can drop the do to be (when (pos? x) (y) (z)). As such, it occupies the same space as Kibit or Rubocop.
It currently covers all of Kibit’s existing rules and has a number more.
It is designed to be significantly faster by using macros to compile somewhat efficient predicate functions for the offending patterns instead of relying on the powerful but slow core.logic library:
$ tokei
=====
Clojure 169 113620 102756 4266 6598
ClojureC 4 1012 850 36 126
ClojureScript 48 12666 11649 142 875
$ time lein kibit
...
real 34m30.395s
$ time splint .
...
real 0m9.220s
The rules are easy to read and write, and you can write your own directly in your projects if you need project-specific rules.
Rules look like this:
(defrule lint/when-do
"`when` already defines an implicit `do`. Rely on it.
Examples:
; bad
(when x (do (println :a) (println :b) :c))
; good
(when x (println :a) (println :b) :c)
"
{:pattern '(when ?x (do &&. ?y))
:message "Unnecessary `do` in `when` body."
:replace '(when ?x &&. ?y)})
It currently doesn’t do any replacements (like Kibit) but that’s on my to-do list for coming releases.
to be honest, i don’t find linters that useful so i have no way of doing the comparisons.
i think gpt is an extremely good learning tool and i’m pretty sure it understands the code structure - you can feed it a piece of python code and ask it to write the same function in js or some really esoteric language.
I’ve forgotten to post any patch notes in here, so here’s a loose summary of the last 3 releases:
v1.3.0 is all new rules, some of which might be helpful! These are all pretty self-explanatory:
naming/single-segment-namespace: Prefer (ns foo.bar) to (ns foo).
lint/prefer-require-over-use: Prefer (:require [clojure.string ...]) to (:use clojure.string). Accepts different styles in the replacement form: :as, :refer [...] and :refer :all.
naming/conventional-aliases: Prefer idiomatic aliases for core libraries ([clojure.string :as str] to [clojure.string :as string]).
naming/lisp-case: Prefer kebab-case over other cases for top-level definitions. Relies on camel-snake-kebab.
style/multiple-arity-order: Function definitions should have multiple arities sorted fewest arguments to most: (defn foo ([a] 1) ([a b] 2) ([a b & more] 3))
v1.4.0 added -v and --version to the cli, which is obvious in hindsight. Also adds config TYPE to print either the full config (any local merged on top of default), the local (just the local file), and the diff (to only see where the local is different from default when explicitly setting local config to match defaults).
I also fixed a couple small bugs.
v1.5.0 adds a new genre! metrics will handle rules that are like style rules but meant to help give you rough measures of code quality (complexity, etc).
metrics/fn-length: Function bodies shouldn’t be longer than 10 lines. Has :body and :defn styles, and :length configurable value to set maximum length.
Changed how I write tests to make it better: Now I’m using nubank/matcher-combinators to do sub-matching, meaning I can check multiple things at once in a single assertion. Love it. Here’s some other changes:
Track end position of diagnostics.
Attach location metadata to function “arities” when a defn arg+body isn’t wrapped in a list.
Parse defn forms in postprocessing and attach as metadata instead of parsing in individual rules.
Fixed some bugs:
Fix style/multiple-arity-order with :arglists metadata.
Last two updates have had some big changes/improvements!
v1.6.0 correctly merges cli and local options (fixes #5). I also wrote short descriptions in the default config.edn to make reading/editing easier. This will help when I get around to implementing a “generate todo .splint.edn” command.
v1.7.0 adds a new rule, a new cli flag, and two new output options.
metrics/parameter-count: Functions shouldn’t have more than 4 positional parameters. Has :positional and :include-rest styles (only positional or include & args rest params too?), and :count configurable value to set maximum number of parameters allowed.
After using --quiet for a while, I realized that it still prints a lot, so I added -s / --silent to print literally nothing (only returning an exit code). Been helpful when running smoke tests locally, or testing speed of execution.
I added json and json-pretty to --output, which outputs json strings, raw or prettified. The prettified json doesn’t look great (it’s a clojurist’s pprint, not jq or eslint lol) but it’s better than nothing. Might revisit this at a later date.
Splint now tracks all files that have been parsed and checked in the return map under :checked-files. No use case yet, but it’s helpful to know what’s actually been looked at.
Splint now uses farolero to handle splint.runner errors. I was worried it would slow down the app but so far, I’ve not run into anything yet.
And here’s a short list of bug fixes:
Only attach parsed defn metadata when fn name exactly matches defn or defn- and second form is a symbol.
--no-parallel was producing a lazy seq, now consumes to actually check all files. Oops lol.
Map over top-level forms with nil parent form instead of treating the whole file as a top-level vector of forms. Fixes naming/lisp-case.
Add pre- and post- attr-maps to defn metadata when parsing defn forms.
lint/warn-on-reflection: Require that (set! *warn-on-reflection* true) is called after the ns declaration at the start of every file. Defaults to false.
Breaking
Deprecate --config. Add --print-config. No timeline for removal of --config (maybe never?).
Added
edn / edn-pretty output: Print diagnostics as edn using clojure.core/prn and clojure.pprint/pprint.
Continue to process files after running into errors during rules checking.
Just released v1.10.0 which includes many cool changes. Forgot to post after 1.9 but that was a pretty small release overall.
In short: v1.9 added a new rule and a new cli flag (--[no-]summary). v1.10 added reading available deps.edn and project.clj files to determine the directories to check, changing the pattern DSL to use a variation of pangloss/pattern’s DSL, and to add 5 new rules, 4 of which are performance rules.
The change in v1.10 means that you can run bb splint or clojure -M:splint and it’ll “just work”: it’ll check the primary directories and the directories under and :test alias without any extra fiddling. This makes it much easier to use quickly. I’ve always been annoyed at clj-kondo’s insistence on forcing the --lint flag for choosing files, and loved Rubocop’s “just run it” mentality.
The performance rules are disabled by default cuz they might make code more wordy or annoying to work with, and they’re highly opinionated. I like them but I get it.
New rules
performance/assoc-many: Prefer (-> m (assoc :k1 1) (assoc :k2 2)) over (assoc m :k1 1 :k2 2).
performance/avoid-satisfies: Do not use clojure.core/satisfies?, full stop.
performance/get-in-literals: Prefer (-> m :k1 :k2 :k3) over (get-in m [:k1 :k2 :k3]).
performance/get-keyword: Prefer (:k m) over (get m :k).
style/prefer-clj-string: Prefer clojure.string functions over raw interop. Defaults to true.
style/redundant-regex-constructor: Prefer #"abc" over (re-pattern #"abc").