Skip to content

Fast multithreaded and customizable linter that checks files for trailing whitespace, tabs, files that don't end in newlines, files that end in blank lines, Unicode characters that look maddeningly similar to ASCII ones, and invisible Unicode characters.

License

Notifications You must be signed in to change notification settings

camsaul/whitespace-linter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

32 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cam's Next-Level Whitespace Linter

Clojars Project

{com.github.camsaul/whitespace-linter {:sha "d10134619f9a53127a17409a6037879452f01bc7"}}

Fast multithreaded and customizable linter that checks files for trailing whitespace, tabs, carriage returns, files that don't end in newlines, files that end in blank lines, Unicode characters that look maddeningly similar to ASCII ones, and invisible Unicode characters. Written in Clojure, but works on any sort of text file.

demo

Standalone Usage

Requires the Clojure CLI (1.10.3.905 or higher). Install it using the instructions here if you haven't already.

clojure -Sdeps \
  '{:aliases {:whitespace-linter {:deps {com.github.camsaul/whitespace-linter {:sha "d10134619f9a53127a17409a6037879452f01bc7"}}
                                  :ns-default whitespace-linter}}}' \
  -T:whitespace-linter lint

Adding to deps.edn

Add it to your deps.edn:

{:aliases
 {:whitespace-linter
  {:deps       {com.github.camsaul/whitespace-linter {:sha "d10134619f9a53127a17409a6037879452f01bc7"}}
   :ns-default whitespace-linter}}}

and run it:

clj -T:whitespace-linter lint

Running with Leiningen

The easiest way to use the whitespace linter with Leiningen is to create an alias.

(defproject my-project "1.0.0-SNAPSHOT"
  :profiles
  {:whitespace-linter
   {:dependencies [[com.camsaul/whitespace-linter "2022.01.27.04.43" :exclusions [org.clojure/clojure]]]}}

  :aliases
  {"whitespace-linter"
   ["with-profile" "+whitespace-linter"
    "run" "-m" "clojure.main" "-e" (do
                                     (require 'whitespace-linter)
                                     (whitespace-linter/lint {:paths            ["src" "test"]
                                                              :include-patterns [#".clj[cs]?$"]}))]})

Then run it with

lein whitespace-linter

Configuration

You can configure the linter by setting :exec-args in the deps.edn alias, or by passing them as arguments to -T:whitespace-linter lint:

clj -T:whitespace-linter lint :paths src
{:aliases
 {:whitespace-linter
  {:deps       {com.github.camsaul/whitespace-linter {:sha "ddfcb3c8f4b3bedc6a0d536780840589ab5f0ec4"}}
   :ns-default whitespace-linter
   :exec-args  {:paths            ["src" "test" "resources"]
                :include-patterns ["\\.clj.?$" "\\.jsx?$" "\\.edn$" "\\.yaml$" "\\.json$" "\\.html$"]
                :exclude-patterns ["resources/i18n/.*\\.edn$"]}}}}

Several options are currently supported:

Option Default Description
:paths ./ Directory(ies) or filename(s) to search for files to lint in.
:include-patterns [#"."] File paths that don't match at least one of these patterns will be ignored.
:exclude-patterns nil File paths that match any of these patterns will be ignored.
:max-file-size-kb 1024 Files over this size will be ignored.

:paths accepts either strings, symbols, or a collection of multiple strings/symbols.

:include-patterns and :exclude-patterns accept either Strings or regex literals (regex literals cannot be embedded in EDN, so use string equivalents instead).

Windows users: The backslash is not recognized as a file separator, use the forward slash / for the file separator character in paths and patterns.

Extensibility

The code uses Methodical under the hood for easy extensibility. It takes advantage of the concat-method-combination which calls every matching multimethod, and concatenates the results; and the everything-dispatcher, which considers every method implementation to be matching regardless of the arguments passed in. This means all you need to do to extend it is write a new method implementation; Methodical will run it automatically along with the ones that ship out of the box.

Adding Linters

To add new linters you should create a new namespace that you use in place of whitespace-linter. Add method implementations to it:

;; linters/my_project/linters/whitespace_linter.clj
(ns my-project.linters.whitespace-linter
  (:require [methodical.core :as m]
            [whitespace-linter :as wsl]))

(m/defmethod wsl/lint-char ::no-capital-as
  [ch options]
  (when (= ch \A)
    [{:message "No capital A's are allowed in this project!"
      :linter ::no-capital-as}]))

(defn lint [options]
  (wsl/lint options))

Linters should return a sequence of error maps with the keys :message and :linter for any errors they decide exist. options are those passed in to the command via the CLI or :exec-args in the deps.edn file.

Update your deps.edn to use your new namespace:

{:aliases
 {:whitespace-linter
  {:deps       {com.github.camsaul/whitespace-linter {:sha "d10134619f9a53127a17409a6037879452f01bc7"}}
   :ns-default my-project.linters.whitespace-linter
   :paths      ["linters"]}}}

That's it! Line and column information is added to the output automatically (where applicable):

$ clj -T:whitespace-linter lint :paths naughty.clj
Finding matching files...
Linting 1 files...
1/1   100% [==================================================]  ETA: 00:00
Linted 1 files in 0.0 seconds.
Found 4 errors
naughty.clj:1:5 Found Unicode character \u1d21 'ᴡ' that looks way too similar to ASCII 'w' (:character/confusing-unicode-character)
naughty.clj:4:5 No capital A's are allowed in this project! (:my-project.linters.whitespace-linter/no-capital-as)
naughty.clj:4:9 No capital A's are allowed in this project! (:my-project.linters.whitespace-linter/no-capital-as)
naughty.clj:4:23 No capital A's are allowed in this project! (:my-project.linters.whitespace-linter/no-capital-as)

There are three methods you can implement (as many times as you want, of course) to add new linters, depending on which situation is most appropriate:

;; called once for each character
(lint-char ^Character ch options)

;; called once for each line. Line DOES NOT include newlines at the end -- use lint-file if you need those
(lint-line ^String line options)

;; called once for each file
(lint-file ^java.io.File file options)

Removing Built-In Linters

You can also remove built-in linters using Methodical's remove-primary-method!:

(m/remove-primary-method! #'wsl/lint-char :character/confusing-unicode-character)

Selectively Disabling Linters

There is not currently a way to selectively disable certain linters for certain files, altho it seems like it wouldn't be to hard to add... PRs are welcome.

Add it as a GitHub Action

Here's an example GitHub action configuration to add the whitespace linter to your project: https://github.com/metabase/metabase/blob/30b54faa9599bff8d17bb46bef1b838de5334135/.github/workflows/whitespace.yml

Interactive/Programmatic Usage

For interactive or programmatic usage, you can use whitespace-linter/lint-interactive instead of lint. This displays REPL-friendly output (i.e., no progress bar), returns errors in a machine-friendly format, and skips calls to System/exit when linting is finished.

License

Copyright © 2021 Cam Saul.

Distributed under the Eclipse Public License, same as Clojure.

About

Fast multithreaded and customizable linter that checks files for trailing whitespace, tabs, files that don't end in newlines, files that end in blank lines, Unicode characters that look maddeningly similar to ASCII ones, and invisible Unicode characters.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •