Home

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!

parinfer friendly Build Status

Want a Quick Look?

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:

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