Awesome
Parlinter
A low-friction linter for Lisp that finally allows your team members to use Parinfer.
Unlike full pretty-printers, it preserves as much of the original source as possible, only fixing confusing indentation and dangling close-parens. But it's still flexible, allowing any-space indentation within thresholds.
Adopt Parlinter to make your project Parinfer friendly!
Want a Quick Look?
- See concrete examples of Common Lint Results in Clojure.
- See the only Two Rules it follows.
- Try it out on your project then check
git diff -w
to verify the minor changes.
Two Rules
Parlinter performs minimal source transformation in order to satisfy two rules:
Rule #1 - no dangling close-parens
Close-parens at the beginning of a line are moved to the end of its previous token:
;; bad
(foo (bar
) baz
) ^
^
;; fixed
(foo (bar)
baz) ^
^
Conventional Formatters comply with Rule #1 for all cases except to allow close-parens after a comment. Parlinter does NOT make this exception.
Rule #2 - ordered indentation
Newlines must be indented to the right of their parent open-paren:
(foo (bar)
|
|
>
bar) ;; must be indented to the right of the point above: ✔️
^
But they cannot extend too far right into another open-paren:
(foo (bar)
|
|
<
bar) ;; must be indented to the left of the point above: ✔️
^
To fix, indentation is clamped to these points:
;; bad
(foo (bar)
baz)
;; fixed
(foo (bar)
baz) ;; <-- nudged to the right
;; bad
(foo (bar)
baz)
;; fixed
(foo (bar)
baz) ;; <-- nudged to the left
Conventional formatters comply with Rule #2 for all cases, except clojure.pprint and older versions of clojure-mode, which cause extra indentation of multi-arity function bodies.
Install
npm install -g parlinter
or
yarn global add parlinter
Want to use as a plugin in your build environment instead? (e.g. lein, boot) Help wanted! Please create an issue.
Usage
$ parlinter
Usage: parlinter [opts] [filename|glob ...]
Available options:
--write Edit the file in-place. (Beware!)
--trim Remove lines that become empty after linting.
--list-different or -l Print filenames of files that are different from Parlinter formatting.
--stdin Read input from stdin.
--version or -v Print Parlinter version.
Glob patterns must be quoted.
Examples
Format all clojure files:
parlinter --trim --write "**/*.{clj,cljs,cljc,edn}"
Verify non-whitespace changes below for peace-of-mind: (AST not changed)
git diff -w
Check if all clojure files are properly formatted (non-zero exit code if not):
$ parlinter -l "**/*.{clj,cljs,cljc,edn}"
Performance
It takes ~0.5s to run against ~40k lines. (tested on the Clojure and ClojureScript project repos)
It was heavily optimized to allow Parinfer to run at 60hz on a ~3k line file while typing.
Compatibility
Syntactically compatible with Clojure, Racket, Scheme, and other Lisps that follow this syntax:
- delimiters
(
,{
,[
- strings
"
- characters
\
- comments
;
Culturally compatible with standard Lisp styles*:
* some allow close-parens on their own line, but still allow them to be removed as Parlinter does
Common Lint Results in Clojure
A collection of common changes performed by Parlinter on Clojure code—the Lisp I am most familiar with.
1. Multi-arity function bodies
Sometimes function bodies for multi-arity functions are indented past the function params.
;; bad
(defn foo
"I have two arities."
([x]
(foo x 1))
([x y]
(+ x y)))
;; fixed
(defn foo
"I have two arities."
([x]
(foo x 1))
([x y]
(+ x y)))
2. Close-parens after comments
Since close-parens cannot be at the beginning of a line, they cannot come after comments.
;; bad
(-> 10
(foo 20)
(bar 30)
;; my comment
)
;; fixed
(-> 10
(foo 20)
(bar 30))
;; my comment
3. Lines inside strings are not touched
Indentation of lines inside multi-line strings is significant, so it is not modified:
;; bad
(foo (bar
"Hello
world"))
;; fixed
(foo (bar
"Hello
world")) ;; <-- not nudged
4. Recessed function bodies
Function bodies are sometimes indented to its grandparent form rather than its parent:
;; bad
(foo bar (fn [a]
(println a)))
;; fixed
(foo bar (fn [a]
(println a))) ;; <-- nudged to be inside "(fn"
5. Recessed lines after JSON-style {
It is sometimes common to use JSON-style indentation in a top-level EDN config:
;; bad
:cljsbuild {
:builds [...]
}
;; fixed
:cljsbuild {
:builds [...]} ;; <-- nudged to be inside "{"
;; fine (but not automated)
:cljsbuild {:builds [...]}
;; fine (but not automated)
:cljsbuild
{:builds [...]}
6. Recessed lines after #_
and comment
Comment and ignore forms are commonly added retroactively without adjusting indentation:
;; bad
#_(defn foo []
(bar baz))
;; fixed
#_(defn foo []
(bar baz))
;; bad
(comment
(defn foo []
(bar baz))
)
;; fixed
(comment
(defn foo []
(bar baz)))
7. Vertically-aligned comments
Linting may throw off the alignment of comments, due to paren movement:
;; bad
(let [foo 1 ; this is number one
bar 2 ; this is number two
]
(+ foo bar))
;; fixed
(let [foo 1 ; this is number one
bar 2] ; this is number two
(+ foo bar))
Motivation
Though Parinfer was designed to lower the barrier for newcomers, it faced a problem of practicality by not allowing them to collaborate smoothly with people who didn't use it. This friction was not part of the intended experience.
Par<em>linter</em> was designed as an answer to this problem, since there now seems to be a growing acceptance of linters and even full-formatters like Prettier, refmt, and gofmt from other language communities.
Thus, I hope that Parlinter at least spurs some thoughts on what is an acceptable amount of process around linting in Lisp, whether or not Parinfer is worth linting for, and how else we can help newcomers get into Lisp easier.
(It may also open the door for some exciting next-gen things I'm not yet ready to talk about.)
Written for Lisp with <3