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.
4 Likes
@NoahTheDuke: I’m curious if you’ve tried chatgpt to do linting/style checking and how it might compare to a programmed linter.
I’ve not as I’m not a fan of chatgpt and the examples I’ve seen don’t make me confident it would produce helpful code for me.
You should try it and let me know how it goes!
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.
though i’m only pretty sure - not 100%
It currently covers all of Kibit’s existing rules and has a number more.
It is designed to be significantly faster …
scrolls down
$ time lein kibit
...
real 34m30.395s
$ time splint .
...
real 0m9.220s
Wow.
Great work @NoahTheDuke; I’ll be downloading this!
1 Like
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.
- Fix binding pattern when binding is falsey.
- Skip
#(.someMethod %)
in lint/fn-wrapper
.
- Skip
and
and or
in style/prefer-condp
.
1 Like
Awesome, I stopped using Kibit because it was so slow, this is a great replacement.
1 Like
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.
I published v1.8.0 yesterday.
New rules
-
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.
Changed
- Dependencies are updated to latest.
-
json
and json-pretty
keys are now sorted.
- Small performance improvements to patterns.
1 Like