├── ORIGINATOR
├── corpus
├── first.clj
├── fourth.txt
├── double_pound_reader_macros.clj
├── second.cljx
├── namespaced_maps.clj
├── read_eval.clj
├── third.cljs
├── as_alias.clj
├── sets.clj
├── reader_conditionals.cljc
├── keywords.clj
└── keyword_suggestions.clj
├── resources
└── jonase
│ └── kibit
│ └── VERSION
├── .github
├── CODEOWNERS
└── workflows
│ └── main.yml
├── .clj-kondo
├── config.edn
└── rewrite-clj
│ └── rewrite-clj
│ └── config.edn
├── install.sh
├── lein-kibit
├── .gitignore
├── README.md
├── project.clj
└── src
│ └── leiningen
│ └── kibit.clj
├── release.sh
├── .gitignore
├── deps.edn
├── src
└── kibit
│ ├── rules
│ ├── equality.clj
│ ├── arithmetic.clj
│ ├── util.clj
│ ├── control_structures.clj
│ ├── collections.clj
│ └── misc.clj
│ ├── core.clj
│ ├── rules.clj
│ ├── monkeypatch.clj
│ ├── reporters.clj
│ ├── replace.clj
│ ├── driver.clj
│ ├── check
│ └── reader.clj
│ └── check.clj
├── test
└── kibit
│ └── test
│ ├── equality.clj
│ ├── check.clj
│ ├── arithmetic.clj
│ ├── core.clj
│ ├── control_structures.clj
│ ├── reporters.clj
│ ├── replace.clj
│ ├── collections.clj
│ ├── misc.clj
│ ├── check_reader.clj
│ └── driver.clj
├── project.clj
├── CHANGELOG.md
└── README.md
/ORIGINATOR:
--------------------------------------------------------------------------------
1 | @jonase
2 |
--------------------------------------------------------------------------------
/corpus/first.clj:
--------------------------------------------------------------------------------
1 | Test file.
2 |
--------------------------------------------------------------------------------
/resources/jonase/kibit/VERSION:
--------------------------------------------------------------------------------
1 | 0.1.11
2 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @NoahTheDuke @danielcompton
2 |
--------------------------------------------------------------------------------
/corpus/fourth.txt:
--------------------------------------------------------------------------------
1 | Shouldn't be scanned by kibit
2 |
--------------------------------------------------------------------------------
/corpus/double_pound_reader_macros.clj:
--------------------------------------------------------------------------------
1 | [##Inf ##-Inf ##NaN]
2 |
--------------------------------------------------------------------------------
/corpus/second.cljx:
--------------------------------------------------------------------------------
1 | Test file demonstrating cljx extension
2 |
--------------------------------------------------------------------------------
/corpus/namespaced_maps.clj:
--------------------------------------------------------------------------------
1 | #:car{:make "Jeep" :model "Wrangler"}
2 |
--------------------------------------------------------------------------------
/corpus/read_eval.clj:
--------------------------------------------------------------------------------
1 | (if true (do #=(prn :hello :world!) :a))
2 |
--------------------------------------------------------------------------------
/corpus/third.cljs:
--------------------------------------------------------------------------------
1 | Test file demonstrating clojurescript support.
2 |
--------------------------------------------------------------------------------
/.clj-kondo/config.edn:
--------------------------------------------------------------------------------
1 | {:lint-as {kibit.rules.util/defrules clj-kondo.lint-as/def-catch-all}}
2 |
--------------------------------------------------------------------------------
/corpus/as_alias.clj:
--------------------------------------------------------------------------------
1 | (ns resources.as-alias
2 | (:require
3 | [foo.bar :a :b :as-alias fb]))
4 |
5 | ::fb/example
6 |
--------------------------------------------------------------------------------
/corpus/sets.clj:
--------------------------------------------------------------------------------
1 | (ns resources.sets)
2 |
3 | (defn killit [coll]
4 | (not-any? #{"string1" "string2"} (map ffirst coll)))
5 |
--------------------------------------------------------------------------------
/corpus/reader_conditionals.cljc:
--------------------------------------------------------------------------------
1 | #?(:clj (+ 1 1)
2 | :cljs (+ 2 2)
3 | :default (+ 3 3))
4 |
5 | {:a 1 :b 2 #?@(:clj [:c 3 :d 4])}
6 |
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Locally install all the components.
4 |
5 | set -ex
6 |
7 | lein install
8 | pushd lein-kibit
9 | lein install
10 | popd
11 |
12 |
--------------------------------------------------------------------------------
/lein-kibit/.gitignore:
--------------------------------------------------------------------------------
1 | .*.swp
2 | /target
3 | /lib
4 | /classes
5 | /checkouts
6 | pom.xml
7 | pom.xml.asc
8 | *.jar
9 | *.class
10 | .lein-deps-sum
11 | .lein-failures
12 | .lein-plugins
13 | .lein-repl-history
14 |
--------------------------------------------------------------------------------
/.clj-kondo/rewrite-clj/rewrite-clj/config.edn:
--------------------------------------------------------------------------------
1 | {:lint-as
2 | {rewrite-clj.zip/subedit-> clojure.core/->
3 | rewrite-clj.zip/subedit->> clojure.core/->>
4 | rewrite-clj.zip/edit-> clojure.core/->
5 | rewrite-clj.zip/edit->> clojure.core/->>}}
6 |
--------------------------------------------------------------------------------
/lein-kibit/README.md:
--------------------------------------------------------------------------------
1 | # lein-kibit
2 |
3 | See [kibit](https://github.com/clj-commons/kibit) for instructions on how
4 | to install and use this plugin.
5 |
6 | ## Development
7 |
8 | When updating to a new Kibit version, make sure to update both the `project.clj`, and the jonase/kibit dependency in `src/leiningen/kibit.clj`.
9 |
10 | ## License
11 |
12 | Copyright © 2012 Jonas Enlund
13 |
14 | Distributed under the Eclipse Public License, the same as Clojure.
15 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | echo "This is just a dummy representation of what steps you need to take"
4 | echo "It could be automated in the future"
5 | exit 1
6 |
7 | set -ex
8 |
9 | echo "Bump the version in resources/jonase/kibit/VERSION to a release version before running this"
10 | git tag $version
11 | lein deploy
12 |
13 | pushd lein-kibit
14 | lein deploy
15 | popd
16 |
17 | echo "Bump the version in resources to the next SNAPSHOT"
18 | git push
19 | git push --tags
20 |
--------------------------------------------------------------------------------
/corpus/keywords.clj:
--------------------------------------------------------------------------------
1 | (ns resources.keywords
2 | (:require [clojure.java.io :as io]))
3 |
4 | (defn aliased-keyword-access
5 | [x]
6 | (::io/some-fake-key x))
7 |
8 | (ns resources.non-conforming)
9 |
10 | (require '[clojure.string :as str])
11 |
12 | (defn using-a-different-keyword-alias-in-different-ns
13 | [z]
14 | (::str/another-fake-key z))
15 |
16 | (in-ns 'resources.keywords)
17 |
18 | (defn flipped-back-aliases-still-there
19 | [y]
20 | (::io/last-fake-key y))
21 |
22 | (defn auto-namespaced-keyword [w]
23 | (::local-key w))
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Java related
2 | pom.xml
3 | pom.xml.asc
4 | *jar
5 | *.class
6 | /**/target
7 | .cpcache
8 |
9 | # Leiningen
10 | /classes
11 | /lib
12 | /native
13 | /checkouts
14 | /target
15 | .lein-deps-sum
16 | .lein-failures
17 | .lein-env
18 | .lein-repl-history
19 | .nrepl-port
20 | profiles.clj
21 |
22 | # Temp Filest
23 | *.orig
24 | *~
25 | .*.swp
26 | .*.swo
27 | *.tmp
28 | *.bak
29 |
30 | # OS X
31 | .DS_Store
32 |
33 | # Logging
34 | *.log
35 |
36 | # Docs
37 | /autodoc
38 | /docs
39 |
40 | # IntelliJ IDEA
41 | .idea
42 | *.iml
43 |
44 | .clj-kondo/.cache
45 | .lsp/.cache
46 | .portal/vs-code.edn
47 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src"]
2 | :deps {org.clojure/clojure {:mvn/version "1.11.1"}
3 | org.clojure/core.logic {:mvn/version "1.1.0"}
4 | org.clojure/tools.cli {:mvn/version "1.1.230"}
5 | borkdude/edamame {:mvn/version "1.4.25"}
6 | rewrite-clj/rewrite-clj {:mvn/version "1.1.47"}}
7 | ;; Run this as `clojure -X:exec` or `clojure -X:exec -i -r markdown`, for example.
8 | :aliases {:exec {:extra-deps {org.babashka/cli {:mvn/version "0.7.51"}}
9 | :exec-fn kibit.driver/exec
10 | :exec-args {:paths ["."]}
11 | :main-opts ["-m" "babashka.cli.exec"]}}}
12 |
--------------------------------------------------------------------------------
/src/kibit/rules/equality.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.rules.equality
2 | (:require [kibit.rules.util :refer [defrules]]))
3 |
4 | (defrules rules
5 | ;; not=
6 | [(not (= . ?args)) (not= . ?args)]
7 |
8 | ;; zero?
9 | [(= 0 ?x) (zero? ?x)]
10 | [(= ?x 0) (zero? ?x)]
11 | [(== 0 ?x) (zero? ?x)]
12 | [(== ?x 0) (zero? ?x)]
13 |
14 | [(< 0 ?x) (pos? ?x)]
15 | [(> ?x 0) (pos? ?x)]
16 |
17 | [(< ?x 0) (neg? ?x)]
18 | [(> 0 ?x) (neg? ?x)]
19 |
20 | ;; true? false?
21 | [(= true ?x) (true? ?x)]
22 | [(= false ?x) (false? ?x)]
23 |
24 | ; nil?
25 | [(= ?x nil) (nil? ?x)]
26 | [(= nil ?x) (nil? ?x)]
27 | [(not (nil? ?x)) (some? ?x)])
28 |
29 |
--------------------------------------------------------------------------------
/src/kibit/rules/arithmetic.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.rules.arithmetic
2 | (:require [kibit.rules.util :refer [defrules]]))
3 |
4 | (defrules rules
5 | [(+ ?x 1) (inc ?x)]
6 | [(+ 1 ?x) (inc ?x)]
7 | [(- ?x 1) (dec ?x)]
8 |
9 | [(* ?x (* . ?xs)) (* ?x . ?xs)]
10 | [(+ ?x (+ . ?xs)) (+ ?x . ?xs)]
11 |
12 | ;;trivial identities
13 | [(+ ?x 0) ?x]
14 | [(- ?x 0) ?x]
15 | [(* ?x 1) ?x]
16 | [(/ ?x 1) ?x]
17 | [(* ?x 0) 0]
18 |
19 | ;;Math/hypot
20 | [(Math/sqrt (+ (Math/pow ?x 2) (Math/pow ?y 2))) (Math/hypot ?x ?y)]
21 |
22 | ;;Math/expm1
23 | [(dec (Math/exp ?x)) (Math/expm1 ?x)]
24 |
25 | ;;ugly rounding tricks
26 | [(long (+ ?x 0.5)) (Math/round ?x)])
27 |
--------------------------------------------------------------------------------
/src/kibit/rules/util.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.rules.util
2 | (:require [clojure.core.logic :as logic]
3 | [clojure.core.logic.unifier :as unifier]))
4 |
5 | (defn compile-rule [rule]
6 | (let [[pat alt] (unifier/prep rule)]
7 | [(fn [expr] (logic/== expr pat))
8 | (fn [sbst] (logic/== sbst alt))]))
9 |
10 | (defn raw-rule? [rule]
11 | (not (vector? rule)))
12 |
13 | (defmacro defrules [name & rules]
14 | `(let [rules# (for [rule# '~rules]
15 | (unifier/prep
16 | (if (raw-rule? rule#)
17 | (eval rule#) ;; raw rule, no need to compile
18 | (compile-rule rule#))))]
19 | (def ~name (vec rules#))))
20 |
--------------------------------------------------------------------------------
/test/kibit/test/equality.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.test.equality
2 | (:require [kibit.check :as kibit]
3 | [clojure.test :refer [deftest are]]))
4 |
5 | (deftest equality-are
6 | (are [expected-alt-form test-form]
7 | (= expected-alt-form (:alt (kibit/check-expr test-form)))
8 | '(not= x b) '(not (= x b))
9 | '(zero? x) '(= 0 x)
10 | '(zero? x) '(= x 0)
11 | '(zero? x) '(== 0 x)
12 | '(zero? x) '(== x 0)
13 | '(pos? x) '(< 0 x)
14 | '(pos? x) '(> x 0)
15 | '(neg? x) '(< x 0)
16 | '(neg? x) '(> 0 x)
17 | '(true? x) '(= true x)
18 | '(false? x) '(= false x)
19 | '(nil? x) '(= nil x)
20 | '(nil? x) '(= x nil)
21 | '(some? x) '(not (nil? x))))
22 |
--------------------------------------------------------------------------------
/lein-kibit/project.clj:
--------------------------------------------------------------------------------
1 | (defproject lein-kibit (clojure.string/trim-newline (slurp "../resources/jonase/kibit/VERSION"))
2 | :description "kibit lein plugin"
3 | :url "https://github.com/clj-commons/kibit"
4 | :resource-paths ["resources"]
5 | :license {:name "Eclipse Public License"
6 | :url "http://www.eclipse.org/legal/epl-v10.html"}
7 | :dependencies [[org.clojure/tools.namespace "1.5.0"]
8 | [jonase/kibit ~(clojure.string/trim-newline (slurp "../resources/jonase/kibit/VERSION"))]]
9 | :deploy-repositories [["clojars" {:url "https://clojars.org/repo"
10 | :sign-releases false}]
11 | ["snapshots" :clojars]]
12 | :eval-in-leiningen true)
13 |
--------------------------------------------------------------------------------
/corpus/keyword_suggestions.clj:
--------------------------------------------------------------------------------
1 | (ns resources.keyword-suggestions
2 | (:require [clojure.java.io :as io]))
3 |
4 | (defn aliased-keyword-access
5 | [x]
6 | (into [] [::io/some-fake-key ::local-key :some/other-key]))
7 |
8 | (ns resources.non-conforming)
9 |
10 | (require '[clojure.string :as str])
11 |
12 | (defn using-a-different-keyword-alias-in-different-ns
13 | [z]
14 | (into [] [::str/another-fake-key ::local-key2 :some/other-key2]))
15 |
16 | (in-ns 'resources.keyword-suggestions)
17 |
18 | (defn flipped-back-aliases-still-there
19 | [y]
20 | (into [] [::io/last-fake-key ::local-key3 :some/other-key3]))
21 |
22 | (alias 'pprint 'clojure.pprint)
23 |
24 | (defn raw-aliases-work
25 | [y]
26 | (into [] [::pprint/printing-key ::local-key4 :some/other-key4]))
27 |
--------------------------------------------------------------------------------
/test/kibit/test/check.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.test.check
2 | (:require [kibit.check :as kibit]
3 | [clojure.test :refer [deftest are]]))
4 |
5 | ;; These tests are identical to the tests in kibit.test.core
6 | ;; They are here to illustrate kibit use via `check`
7 |
8 | (deftest simplify-alts
9 | (are [expected-alt test-form]
10 | (= expected-alt (:alt (kibit/check-expr test-form)))
11 | [1 2 3] '(do [1 2 3])
12 | [] '(do [])
13 | "Hello" '(do "Hello")
14 | '(when test then) '(do (when test then))
15 | :one '(do :one)
16 | {:one 1} '(do {:one 1})))
17 |
18 | (deftest simplify-exprs
19 | (are [expected-expr test-expr]
20 | (= expected-expr (:expr (kibit/check-expr test-expr)))
21 | '(do [1 2 3]) '(do [1 2 3])
22 | nil '(if (> 2 3) :one :two)))
23 |
--------------------------------------------------------------------------------
/test/kibit/test/arithmetic.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.test.arithmetic
2 | (:require [kibit.check :as kibit]
3 | [clojure.test :refer [deftest are]]))
4 |
5 | (deftest arithmetic-are
6 | (are [expected-alt-form test-form]
7 | (= expected-alt-form (:alt (kibit/check-expr test-form)))
8 | '(inc num) '(+ num 1)
9 | '(inc num) '(+ 1 num)
10 | '(dec num) '(- num 1)
11 | '(* x y z) '(* x (* y z))
12 | '(+ x y z) '(+ x (+ y z))
13 |
14 | ;;hypot
15 | '(Math/hypot x y) '(Math/sqrt (+ (Math/pow x 2) (Math/pow y 2)))
16 |
17 | ;;special exponential form
18 | '(Math/expm1 x) '(- (Math/exp x) 1)
19 | '(Math/expm1 x) '(dec (Math/exp x))
20 |
21 | ;;not that ugly rounding trick, thank you
22 | '(Math/round x) '(long (+ x 0.5))
23 |
24 | ;;trivial identities
25 | 'x '(+ x 0)
26 | 'x '(- x 0)
27 | 'x '(* x 1)
28 | 'x '(/ x 1)
29 | '0 '(* x 0)))
30 |
--------------------------------------------------------------------------------
/src/kibit/rules/control_structures.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.rules.control-structures
2 | (:require [kibit.rules.util :refer [defrules]]))
3 |
4 | (defrules rules
5 | [(if ?x ?y nil) (when ?x ?y)]
6 | [(if ?x nil ?y) (when-not ?x ?y)]
7 | [(if ?x (do . ?y)) (when ?x . ?y)]
8 | [(if (not ?x) ?y ?z) (if-not ?x ?y ?z)]
9 | [(if ?x ?x ?y) (or ?x ?y)]
10 | [(when (not ?x) . ?y) (when-not ?x . ?y)]
11 | [(do ?x) ?x]
12 | [(if-let ?binding ?expr nil) (when-let ?binding ?expr)]
13 | [(when ?x (do . ?y)) (when ?x . ?y)]
14 | [(when-not ?x (do . ?y)) (when-not ?x . ?y)]
15 | [(if-not ?x (do . ?y)) (when-not ?x . ?y)]
16 | [(if-not (not ?x) ?y ?z) (if ?x ?y ?z)]
17 | [(when-not (not ?x) . ?y) (when ?x . ?y)]
18 |
19 | ;; suggest `while` for bindingless loop-recur
20 | [(loop [] (when ?test . ?exprs (recur)))
21 | (while ?test . ?exprs)]
22 | [(let ?binding (do . ?exprs)) (let ?binding . ?exprs)]
23 | [(loop ?binding (do . ?exprs)) (loop ?binding . ?exprs)])
24 |
--------------------------------------------------------------------------------
/test/kibit/test/core.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.test.core
2 | (:require [kibit.check :as kibit]
3 | [kibit.core :as core]
4 | [clojure.core.logic.unifier :as unifier]
5 | [kibit.rules :as core-rules]
6 | [clojure.test :refer [deftest are]]))
7 |
8 | (def all-rules (map unifier/prep core-rules/all-rules))
9 |
10 | (deftest simplify-alts
11 | (are [expected-alt test-expr]
12 | (= expected-alt (core/simplify test-expr all-rules) (:alt (kibit/check-expr test-expr)))
13 | [1 2 3] '(do [1 2 3])
14 | [] '(do [])
15 | "Hello" '(do "Hello")
16 | '(when test then) '(do (when test then))
17 | :one '(do :one)
18 | {:one 1} '(do {:one 1})
19 | #{:a :b} '#{(do :a) (do :b)}))
20 |
21 | ;; This test confirms when checking will happen and when it won't
22 | (deftest simplify-exprs
23 | (are [expected-expr test-expr]
24 | (= expected-expr (:expr (kibit/check-expr test-expr)))
25 | '(do [1 2 3]) '(do [1 2 3])
26 | nil '(if (> 2 3) :one :two)))
27 |
--------------------------------------------------------------------------------
/src/kibit/rules/collections.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.rules.collections
2 | (:require [kibit.rules.util :refer [defrules]]))
3 |
4 | (defrules rules
5 | ;;vector
6 | [(conj [] . ?x) (vector . ?x)]
7 | [(into [] ?coll) (vec ?coll)]
8 | [(assoc ?coll ?key0 (assoc (?key0 ?coll) ?key1 ?val)) (assoc-in ?coll [?key0 ?key1] ?val)]
9 | [(assoc ?coll ?key0 (assoc (?coll ?key0) ?key1 ?val)) (assoc-in ?coll [?key0 ?key1] ?val)]
10 | [(assoc ?coll ?key0 (assoc (get ?coll ?key0) ?key1 ?val)) (assoc-in ?coll [?key0 ?key1] ?val)]
11 | [(assoc ?coll ?key (?fn (?key ?coll) . ?args)) (update-in ?coll [?key] ?fn . ?args)]
12 | [(assoc ?coll ?key (?fn (?coll ?key) . ?args)) (update-in ?coll [?key] ?fn . ?args)]
13 | [(assoc ?coll ?key (?fn (get ?coll ?key) . ?args)) (update-in ?coll [?key] ?fn . ?args)]
14 | [(update-in ?coll ?keys assoc ?val) (assoc-in ?coll ?keys ?val)]
15 |
16 | ;; empty?
17 | [(not (empty? ?x)) (seq ?x)]
18 | [(when-not (empty? ?x) . ?y) (when (seq ?x) . ?y)]
19 |
20 | ;; set
21 | [(into #{} ?coll) (set ?coll)]
22 |
23 | [(take ?n (repeatedly ?coll)) (repeatedly ?n ?coll)]
24 | [(dorun (map ?fn ?coll)) (run! ?fn ?coll)])
25 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | lint:
7 | if: "!contains(github.event.head_commit.message, 'skip ci')"
8 | strategy:
9 | matrix:
10 | os: [ubuntu-latest, macOS-latest]
11 |
12 | runs-on: ${{ matrix.os }}
13 |
14 | name: Run tests
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v3
18 |
19 | # exclusions: babashka, clj-kondo and cljstyle
20 | - name: Prepare java
21 | uses: actions/setup-java@v3
22 | with:
23 | distribution: 'zulu'
24 | java-version: '8'
25 |
26 | - name: Install clojure tools
27 | uses: DeLaGuardo/setup-clojure@12.5
28 | with:
29 | lein: 2.9.1
30 |
31 | - name: Cache lein project dependencies
32 | id: clj-cache
33 | uses: actions/cache@v3
34 | with:
35 | path: |
36 | ~/.m2
37 | ~/.lein
38 | key: ${{ runner.os }}-clojure-${{ hashFiles('**/project.clj') }}
39 | restore-keys: ${{ runner.os }}-clojure
40 |
41 | - name: Run tests
42 | run: lein test-all
43 |
--------------------------------------------------------------------------------
/test/kibit/test/control_structures.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.test.control-structures
2 | (:require [kibit.check :as kibit]
3 | [clojure.test :refer [deftest are]]))
4 |
5 | (deftest control-structures-are
6 | (are [expected-alt-form test-form]
7 | (= expected-alt-form (:alt (kibit/check-expr test-form)))
8 | '(when test then) '(if test then nil)
9 | '(when-not test else) '(if test nil else)
10 | '(when test body) '(if test (do body))
11 | '(if-not test then else) '(if (not test) then else)
12 | '(or test else) '(if test test else)
13 | '(when-not test then) '(when (not test) then)
14 | 'single-expression '(do single-expression)
15 | '(when-let [a test] expr) '(if-let [a test] expr nil)
16 | '(let [a 1] (println a) a) '(let [a 1] (do (println a) a))
17 | '(when test (println a) then) '(when test (do (println a) then))
18 | '(when-not test (println a) then) '(when-not test (do (println a) then))
19 | '(when-not test body) '(if (not test) (do body))
20 | '(when-not test body) '(if-not test (do body))
21 |
22 | '(loop [a 4] (println a) (if (zero? a) a (recur (dec a))))
23 | '(loop [a 4] (do (println a) (if (zero? a) a (recur (dec a)))))))
24 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject jonase/kibit (clojure.string/trim-newline (slurp "resources/jonase/kibit/VERSION"))
2 | :description "There's a function for that!"
3 | :url "https://github.com/clj-commons/kibit"
4 | :license {:name "Eclipse Public License - v 1.0"
5 | :url "http://www.eclipse.org/legal/epl-v10.html"
6 | :distribution :repo
7 | :comments "Contact if any questions"}
8 | :scm {:name "git"
9 | :url "https://github.com/clj-commons/kibit"
10 | :connection "scm:git:git://github.com/clj-commons/kibit.git"
11 | :developerConnection "scm:git:ssh://git@github.com/clj-commons/kibit.git"}
12 | :dependencies [[org.clojure/clojure "1.11.1"]
13 | [org.clojure/core.logic "1.1.0"]
14 | [org.clojure/tools.cli "1.1.230"]
15 | [rewrite-clj "1.1.47"]
16 | [borkdude/edamame "1.4.25"]]
17 | :deploy-repositories [["clojars" {:url "https://clojars.org/repo"
18 | :sign-releases false}]
19 | ["snapshots" :clojars]]
20 | :aliases {"test-all" ["do"
21 | ["clean"]
22 | ["test"]
23 | ["clean"]
24 | ["compile" ":all"]]})
25 |
--------------------------------------------------------------------------------
/src/kibit/core.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.core
2 | "Kibit's core functionality uses core.logic to construct idiomatic
3 | replacements/simplifications for patterns of code."
4 | (:require [clojure.walk :as walk]
5 | [clojure.core.logic :as logic]))
6 |
7 | ;; ### Important notes
8 | ;; Feel free to contribute rules to [kibit's github repo](https://github.com/clj-commons/kibit)
9 |
10 | ;; Building an alternative form
11 | ;; ----------------------------
12 | ;;
13 | ;; ### Applying unification
14 | ;;
15 | ;; Performs the first simplification found in the rules. If no rules
16 | ;; apply the original expression is returned. Does not look at
17 | ;; subforms.
18 | (defn simplify-one [expr rules]
19 | (let [alts (logic/run* [q]
20 | (logic/fresh [pat subst]
21 | (logic/membero [pat subst] rules)
22 | (logic/project [pat subst]
23 | (logic/all (pat expr)
24 | (subst q)))))]
25 | (if (empty? alts) expr (first alts))))
26 |
27 | ;; Simplifies expr according to the rules until no more rules apply.
28 | (defn simplify [expr rules]
29 | (->> expr
30 | (iterate (partial walk/prewalk #(simplify-one % rules)))
31 | (partition 2 1)
32 | (drop-while #(apply not= %))
33 | (ffirst)))
34 |
--------------------------------------------------------------------------------
/test/kibit/test/reporters.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.test.reporters
2 | (:require [kibit.reporters :as reporters]
3 | [clojure.string :as string]
4 | [clojure.test :refer [deftest are]]))
5 |
6 | (deftest plain
7 | (are [check-map result]
8 | (= (with-out-str (reporters/cli-reporter check-map))
9 | (string/join (System/getProperty "line.separator") result))
10 | {:file "some/file.clj"
11 | :line 30
12 | :expr '(+ x 1)
13 | :alt '(inc x)} ["At some/file.clj:30:"
14 | "Consider using:"
15 | " (inc x)"
16 | "instead of:"
17 | " (+ x 1)"
18 | "" ""]))
19 | (deftest gfm
20 | (are [check-map result]
21 | (= (with-out-str (reporters/gfm-reporter check-map))
22 | (string/join (System/getProperty "line.separator") result))
23 | {:file "some/file.clj"
24 | :line 30
25 | :expr '(+ x 1)
26 | :alt '(inc x)} ["----"
27 | "##### `some/file.clj:30`"
28 | "Consider using:"
29 | "```clojure"
30 | " (inc x)"
31 | "```"
32 | "instead of:"
33 | "```clojure"
34 | " (+ x 1)"
35 | "```"
36 | "" ""]))
37 |
--------------------------------------------------------------------------------
/test/kibit/test/replace.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.test.replace
2 | (:require [kibit.replace :as replace]
3 | [clojure.test :refer [deftest are]])
4 | (:import java.io.File
5 | java.io.StringWriter))
6 |
7 | (defmacro discard-output
8 | "Like `with-out-str`, but discards what was written to *out*"
9 | [& body]
10 | `(binding [*out* (StringWriter.)]
11 | ~@body))
12 |
13 | (deftest replace-expr-are
14 | (are [expected-form test-form]
15 | (= expected-form
16 | (discard-output
17 | (replace/replace-expr test-form)))
18 |
19 | '(inc a)
20 | '(+ 1 a)
21 |
22 | '1
23 | '(do 1)
24 |
25 | '(defn "Documentation" ^{:my-meta 1} [a]
26 | ;; a comment
27 | (inc a))
28 | '(defn "Documentation" ^{:my-meta 1} [a]
29 | ;; a comment
30 | (+ 1 a))))
31 |
32 | (deftest replace-file-are
33 | (are [expected-form test-form]
34 | (= expected-form
35 | (let [file (doto (File/createTempFile "replace-file" ".clj")
36 | (.deleteOnExit)
37 | (spit test-form))]
38 | (discard-output (replace/replace-file file))
39 | (slurp file)))
40 |
41 | "(inc a)"
42 | "(+ 1 a)"
43 |
44 | "1"
45 | "(do 1)"
46 |
47 | "(ns replace-file)
48 |
49 | (defn \"Documentation\" ^{:my-meta 1} [a]
50 | ;; a comment
51 | (inc a))"
52 | "(ns replace-file)
53 |
54 | (defn \"Documentation\" ^{:my-meta 1} [a]
55 | ;; a comment
56 | (+ 1 a))"))
57 |
--------------------------------------------------------------------------------
/src/kibit/rules.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.rules
2 | "`rules.clj` provides the core functionality for extracting
3 | and merging rules from namespaces. There are shorthands for
4 | the individual rule sets, via the `rule-map`"
5 | (:require [kibit.rules.arithmetic :as arith]
6 | [kibit.rules.control-structures :as control]
7 | [kibit.rules.collections :as coll]
8 | [kibit.rules.equality :as equality]
9 | [kibit.rules.misc :as misc]))
10 |
11 |
12 | ;; More information on rules
13 | ;; -------------------------
14 | ;;
15 | ;; Rule sets are stored in individual files that have a top level
16 | ;; `(defrules rules ...)`. The collection of rules are in the `rules`
17 | ;; directory.
18 | ;;
19 | ;; TODO Paul - Major revisions
20 | ;; Each rule (also called a rule pair) in a rule set map is comprised of:
21 | ;;
22 | ;; * a pattern expression (e.g. `(+ ?x 1)`)
23 | ;; * a substitution expression (e.g. `(inc ?x)`
24 | ;;
25 | ;; These rules are used in the unification process to generate suggested
26 | ;; code alternatives. For more information see:
27 | ;; [core](#kibit.core) namespace
28 |
29 |
30 | ;; A map of the individual rule sets, keyed by rule group
31 | (def rule-map {:control-structures control/rules
32 | :arithmetic arith/rules
33 | :collections coll/rules
34 | :equality equality/rules
35 | :misc misc/rules})
36 |
37 | ;; TODO: Consider a refactor for this into a function
38 | ;; `(defn rules-for-ns [& namespaces])`
39 | (def all-rules (apply concat (vals rule-map)))
40 |
--------------------------------------------------------------------------------
/src/kibit/monkeypatch.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.monkeypatch
2 | "Various helpers providing a with-monkeypatches form which wraps"
3 | (:require [clojure.core.logic :as c.c.l])
4 | (:import [clojure.lang
5 | Var
6 | IPersistentSet]
7 | [clojure.core.logic.protocols
8 | ITreeTerm]))
9 |
10 | (defn- tree-term? [x]
11 | (and (or (coll? x)
12 | (instance? ITreeTerm x))
13 | (not (instance? IPersistentSet x))))
14 |
15 | (def kibit-redefs
16 | {#'c.c.l/tree-term? tree-term?})
17 |
18 | (defmacro with-monkeypatches
19 | "Builds a try/finally which captures Var bindings (and ^:macro tags) coming in, creates new Var
20 | bindings within the try and in the finally restores the original bindings. This allows users to
21 | establish stack-local patched contexts."
22 | {:style/indent [1]}
23 | [redefs & forms]
24 | (let [redefs (eval redefs)
25 | original-bindings (into {}
26 | (for [k (keys redefs)]
27 | [k (gensym)]))]
28 | `(let [~@(for [[k v] original-bindings
29 | f [v `(deref ~k)]]
30 | f)]
31 | (try ~@(for [[k v] redefs]
32 | `(do (.bindRoot ~k ~v)
33 | ~(if (.isMacro ^Var k)
34 | `(.setMacro ~k))))
35 | ~@forms
36 | (finally
37 | ~@(for [[k v] redefs]
38 | `(do (.bindRoot ~k ~(get original-bindings k))
39 | ~(if (.isMacro ^Var k)
40 | `(.setMacro ~k)))))))))
41 |
--------------------------------------------------------------------------------
/test/kibit/test/collections.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.test.collections
2 | (:require [kibit.check :as kibit]
3 | [clojure.test :refer [deftest are]]))
4 |
5 | (deftest collections-are
6 | (are [expected-alt-form test-form]
7 | (= expected-alt-form (:alt (kibit/check-expr test-form)))
8 | '(seq a) '(not (empty? a))
9 | '(when (seq a) b) '(when-not (empty? a) b)
10 | '(when (seq a) b) '(when (not (empty? a)) b)
11 | '(vector a) '(conj [] a)
12 | '(vector a b) '(conj [] a b)
13 | '(vec coll) '(into [] coll)
14 | '(set coll) '(into #{} coll)
15 | '(update-in coll [k] f) '(assoc coll k (f (k coll)))
16 | '(update-in coll [k] f) '(assoc coll k (f (coll k)))
17 | '(update-in coll [k] f) '(assoc coll k (f (get coll k)))
18 | '(assoc-in coll [k0 k1] a) '(assoc coll k0 (assoc (k0 coll) k1 a))
19 | '(assoc-in coll [k0 k1] a) '(assoc coll k0 (assoc (coll k0) k1 a))
20 | '(assoc-in coll [k0 k1] a) '(assoc coll k0 (assoc (get coll k0) k1 a))
21 | '(update-in coll [k] f a b c) '(assoc coll k (f (k coll) a b c))
22 | '(update-in coll [k] f a b c) '(assoc coll k (f (coll k) a b c))
23 | '(update-in coll [k] f a b c) '(assoc coll k (f (get coll k) a b c))
24 | '(assoc-in coll [k1 k2] v) '(update-in coll [k1 k2] assoc v)
25 | '(repeatedly 10 (constantly :foo)) '(take 10 (repeatedly (constantly :foo)))
26 | '(run! f coll) '(dorun (map f coll))
27 |
28 | ;; some wrong simplifications happened in the past:
29 | nil '(assoc coll k (assoc (coll k0) k1 a))
30 | nil '(assoc coll k (assoc (get coll k0) k1 a))
31 | nil '(assoc coll k (assoc (k0 coll) k1 a))
32 | nil '#{#{}
33 | #{#{}}
34 | #{#{#{}}}
35 | #{#{#{#{}}}}
36 | #{#{#{#{#{}}}}}}))
37 |
--------------------------------------------------------------------------------
/src/kibit/reporters.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.reporters
2 | "Format and display output generated from check-* functions"
3 | (:require [clojure.string :as string]
4 | [clojure.pprint :as pp])
5 | (:import [java.io StringWriter]))
6 |
7 | ;; Reporters are used with `check-file`, passed in with the `:reporter`
8 | ;; keyword argument. For more information, see the [check](#kibit.check)
9 | ;; namespace.
10 | ;;
11 | ;; There is no limit to a reporter - Clojure Data, JSON, HTML...
12 | ;;
13 | ;; Here we have supplied a reporter for standard-out.
14 |
15 | ;; A hack to get the code indented.
16 | (defn pprint-code [form]
17 | (let [string-writer (StringWriter.)]
18 | (pp/write form
19 | :dispatch pp/code-dispatch
20 | :stream string-writer
21 | :pretty true)
22 | (->> (str string-writer)
23 | string/split-lines
24 | (map #(str " " %))
25 | (string/join (System/getProperty "line.separator"))
26 | println)))
27 |
28 | (defn cli-reporter
29 | "Print a check-map to `*out*` in plain text."
30 | [check-map]
31 | (let [{:keys [file line expr alt]} check-map]
32 | (printf "At %s:%s:%nConsider using:%n" file line)
33 | (pprint-code alt)
34 | (println "instead of:")
35 | (pprint-code expr)
36 | (newline)))
37 |
38 | (defn gfm-reporter
39 | "Print a check-map to `*out*` in github flavored markdown."
40 | [check-map]
41 | (let [{:keys [file line expr alt]} check-map]
42 | (printf "----%n##### `%s:%s`%nConsider using:%n" file line)
43 | (println "```clojure")
44 | (pprint-code alt)
45 | (println "```")
46 | (println "instead of:")
47 | (println "```clojure")
48 | (pprint-code expr)
49 | (println "```")
50 | (newline)))
51 |
52 | (defn no-op-reporter
53 | "Prints nothing to `*out*`."
54 | [check-map]
55 | nil)
56 |
57 | (def name-to-reporter {"markdown" gfm-reporter
58 | "text" cli-reporter
59 | "no-op" no-op-reporter})
60 |
--------------------------------------------------------------------------------
/test/kibit/test/misc.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.test.misc
2 | (:require [kibit.check :as kibit]
3 | [kibit.rules.misc :as misc]
4 | [clojure.test :refer [deftest are]]))
5 |
6 | (deftest class-symbol-are
7 | (are [valid? class-symbol]
8 | (= valid? (misc/class-symbol? class-symbol))
9 | true 'Boolean
10 | true 'foo.bar.Baz
11 | false 'boolean
12 | false 'foo.bar.baz
13 | false 'foo.bar))
14 |
15 | (deftest misc-are
16 | (are [expected-alt-form test-form]
17 | (= expected-alt-form (:alt (kibit/check-expr test-form)))
18 | '(clojure.string/join x y) '(apply str (interpose x y))
19 | '(clojure.string/join x) '(apply str x)
20 | '(clojure.string/reverse x) '(apply str (reverse x))
21 | '(mapcat x y) '(apply concat (apply map x y))
22 | '(mapcat x y) '(apply concat (map x y))
23 | '(remove pred coll) '(filter (complement pred) coll)
24 | '(remove empty? coll) '(filter seq coll)
25 | '(remove pred coll) '(filter #(not (pred %)) coll)
26 | '(remove pred coll) '(filter (fn [x] (not (pred x))) coll)
27 | '(filterv pred coll) '(vec (filter pred coll))
28 | '(ffirst coll) '(first (first coll))
29 | '(fnext coll) '(first (next coll))
30 | '(nnext coll) '(next (next coll))
31 | '(nfirst coll) '(next (first coll))
32 | 'fun '(fn [args] (fun args))
33 | 'fun '(fn* [args] (fun args))
34 | '(str x) '(.toString x)
35 | '(.member obj) '(. obj member)
36 | '(.method obj args) '(. obj method args)
37 | '(.method obj args) '(. obj (method args))
38 | '(Klass/staticMethod args) '(. Klass staticMethod args)
39 | '(form arg) '(-> arg form)
40 | '(:form arg) '(-> arg :form)
41 | '(first-of-form arg rest-of-form) '(-> arg (first-of-form rest-of-form))
42 | '(form arg) '(->> arg form)
43 | '(:form arg) '(->> arg :form)
44 | '(first-of-form rest-of-form arg) '(->> arg (first-of-form rest-of-form))
45 | '(not-any? pred coll) '(not (some pred coll))
46 | '(vary-meta arg f) '(with-meta arg (f (meta arg)))
47 | '(vary-meta arg f :a :b :c) '(with-meta arg (f (meta arg) :a :b :c))))
48 |
--------------------------------------------------------------------------------
/lein-kibit/src/leiningen/kibit.clj:
--------------------------------------------------------------------------------
1 | (ns leiningen.kibit
2 | (:require [leiningen.core.eval :refer [eval-in-project]]
3 | [clojure.tools.namespace.find :refer [find-namespaces]]
4 | [clojure.java.io :as io]
5 | [clojure.string :as str])
6 | (:import (java.nio.file Paths)))
7 |
8 |
9 | (defn ^:no-project-needed kibit
10 | [project & args]
11 | (let [src-paths (get-in project [:kibit :source-paths] ["rules"])
12 | repositories (:repositories project)
13 | local-repo (:local-repo project)
14 | kibit-project `{:dependencies [[jonase/kibit ~(str/trim-newline
15 | (slurp
16 | (io/resource
17 | "jonase/kibit/VERSION")))]]
18 | :source-paths ~src-paths
19 | :repositories ~repositories
20 | :local-repo ~local-repo}
21 | cwd (.toAbsolutePath (Paths/get "" (into-array String nil)))
22 | ;; This could become a transducer once we want to force a dependency on Lein 1.6.0 or higher.
23 | paths (->> (concat ;; Collect all of the possible places sources can be defined.
24 | (:source-paths project)
25 | [(:source-path project)]
26 | (mapcat :source-paths (get-in project [:profiles]))
27 | (mapcat :source-paths (get-in project [:cljsbuild :builds]))
28 | (mapcat :source-paths (get-in project [:cljx :builds])))
29 | (filter some?) ;; Remove nils
30 | ;; Convert all String paths to absolute paths (Leiningen turns root :source-paths into absolute path).
31 | (map #(.toAbsolutePath (Paths/get % (into-array String nil))))
32 | (map #(.relativize cwd %)) ;; Relativize them them all to make them easier on the eyes.
33 | (map str) ;; Convert them back to strings.
34 | (set)) ;; Deduplicate paths by putting them all in a set.
35 | rules (get-in project [:kibit :rules])
36 | src `(kibit.driver/external-run '~paths
37 | (when ~rules
38 | (apply concat (vals ~rules)))
39 | ~@args)
40 | ns-xs (mapcat identity (map #(find-namespaces [(io/file %)]) src-paths))
41 | req `(do (require 'kibit.driver)
42 | (doseq [n# '~ns-xs]
43 | (require n#)))]
44 | (try (eval-in-project kibit-project src req)
45 | (catch Exception e
46 | (throw (ex-info "" {:exit-code 1}))))))
47 |
--------------------------------------------------------------------------------
/test/kibit/test/check_reader.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.test.check-reader
2 | (:require [kibit.check.reader :as reader]
3 | [clojure.test :refer [deftest are is]]))
4 |
5 | (deftest derive-aliases-test
6 | (are [expected-alias-map ns-form]
7 | (= expected-alias-map (reader/derive-aliases ns-form))
8 | '{foo foo.bar.baz} '(ns derive.test.one
9 | "This is a namespace string, which should not cause problems"
10 | {:author "Alice"
11 | :purpose "Make sure that attr-map also doesn't cause problems"}
12 | (:require [foo.bar.baz :as foo]))
13 | '{foo foo.bar.baz} '(ns ^{:doc "Docstring as metadata"}
14 | derive.test.one
15 | (:require [foo.bar.baz :as foo]))
16 | '{foo foo.bar.baz} '(ns ^:metadata-x derive.test.one
17 | (:require [foo.bar.baz :as foo]))
18 |
19 | '{foo foo.bar.baz} '(ns derive.test.one
20 | (:require [foo.bar.baz :as foo]))
21 | '{foo foo.bar.baz
22 | foom foo.bar.baz.macros} '(ns derive.test.one
23 | (:require [foo.bar.baz :as foo])
24 | (:require-macros [foo.bar.baz.macros :as foom]))
25 | '{str clojure.string} '(require (quote [clojure.string :as str]))
26 | '{pprint clojure.pprint} '(alias 'pprint 'clojure.pprint)
27 | '{string clojure.string} '(require (quote [clojure [string :as string]]))
28 | '{kibit-check kibit.check
29 | kibit-replace kibit.replace
30 | kibit-reporters kibit.reporters
31 | kibit-rules kibit.rules
32 | foo-bar-war foo.bar.war
33 | foo-baz-waz foo.baz.waz} '(ns derive.test.one
34 | (:require [kibit
35 | [check :as kibit-check]
36 | [replace :as kibit-replace]
37 | [reporters :as kibit-reporters]
38 | [rules :as kibit-rules]]
39 | [foo
40 | [bar
41 | [war :as foo-bar-war]]
42 | [baz
43 | [waz :as foo-baz-waz]]]))
44 | '{kibit-check kibit.check
45 | kibit-replace kibit.replace
46 | kibit-reporters kibit.reporters
47 | kibit-rules kibit.rules
48 | foo-bar-war foo.bar.war
49 | foo-baz-waz foo.baz.waz} '(require (quote [kibit
50 | [check :as kibit-check]
51 | [replace :as kibit-replace]
52 | [reporters :as kibit-reporters]
53 | [rules :as kibit-rules]])
54 | [foo
55 | [bar
56 | [war :as foo-bar-war]]
57 | [baz
58 | [waz :as foo-baz-waz]]])
59 | '{} '(ns derive.test.one
60 | "JS deps should be simply ignored/omitted"
61 | (:require ["react-dom" :as react-dom]
62 | ["some-polyfill"]))))
63 |
64 | (deftest js-dep-spec?
65 | (is (true? (#'reader/js-dep-spec? '["react-dom" :as react-dom])))
66 | (is (true? (#'reader/js-dep-spec? '["some-polyfill"]))))
67 |
--------------------------------------------------------------------------------
/test/kibit/test/driver.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.test.driver
2 | (:require [kibit.driver :as driver]
3 | [clojure.test :refer [deftest is are]]
4 | [clojure.java.io :as io])
5 | (:import (java.io ByteArrayOutputStream PrintWriter)))
6 |
7 | (deftest clojure-file-are
8 | (are [expected file] (= expected (driver/clojure-file? (io/file file)))
9 | true "corpus/first.clj"
10 | true "corpus/second.cljx"
11 | true "corpus/third.cljs"
12 | false "corpus/fourth.txt"))
13 |
14 | (deftest find-clojure-sources-are
15 | (is (= [(io/file "corpus/as_alias.clj")
16 | (io/file "corpus/double_pound_reader_macros.clj")
17 | (io/file "corpus/first.clj")
18 | (io/file "corpus/keyword_suggestions.clj")
19 | (io/file "corpus/keywords.clj")
20 | (io/file "corpus/namespaced_maps.clj")
21 | (io/file "corpus/read_eval.clj")
22 | (io/file "corpus/reader_conditionals.cljc")
23 | (io/file "corpus/second.cljx")
24 | (io/file "corpus/sets.clj")
25 | (io/file "corpus/third.cljs")]
26 | (driver/find-clojure-sources-in-dir (io/file "corpus")))))
27 |
28 | (deftest test-set-file
29 | (is (driver/run ["corpus/sets.clj"] nil)))
30 |
31 | (deftest test-keywords-file
32 | (let [test-buf (ByteArrayOutputStream.)
33 | test-err (PrintWriter. test-buf)]
34 | (binding [*err* test-err]
35 | (driver/run ["corpus/keywords.clj"] nil))
36 | (is (zero? (.size test-buf))
37 | (format "Test err buffer contained '%s'" (.toString test-buf)))))
38 |
39 | (deftest test-keyword-suggestions-file
40 | (is (= '({:alt (vec [:clojure.java.io/some-fake-key :resources.keyword-suggestions/local-key :some/other-key])
41 | :expr (into [] [:clojure.java.io/some-fake-key :resources.keyword-suggestions/local-key :some/other-key])}
42 | {:alt (vec [:clojure.string/another-fake-key :resources.non-conforming/local-key2 :some/other-key2])
43 | :expr (into [] [:clojure.string/another-fake-key :resources.non-conforming/local-key2 :some/other-key2])}
44 | {:alt (vec [:clojure.java.io/last-fake-key :resources.keyword-suggestions/local-key3 :some/other-key3])
45 | :expr (into [] [:clojure.java.io/last-fake-key :resources.keyword-suggestions/local-key3 :some/other-key3])}
46 | {:alt (vec [:clojure.pprint/printing-key :resources.keyword-suggestions/local-key4 :some/other-key4])
47 | :expr (into [] [:clojure.pprint/printing-key :resources.keyword-suggestions/local-key4 :some/other-key4])})
48 | (map #(select-keys % [:expr :alt])
49 | (driver/run ["corpus/keyword_suggestions.clj"] nil "--reporter" "no-op")))))
50 |
51 | (defmacro with-err-str
52 | [& body]
53 | `(let [s# (java.io.StringWriter.)]
54 | (binding [*err* s#]
55 | ~@body
56 | (str s#))))
57 |
58 | (deftest process-reader-macros
59 | (is (= ["" "" "" ""]
60 | [(with-err-str
61 | (driver/run ["corpus/reader_conditionals.cljc"] nil "--reporter" "no-op"))
62 | (with-err-str
63 | (driver/run ["corpus/double_pound_reader_macros.clj"] nil "--reporter" "no-op"))
64 | (with-err-str
65 | (driver/run ["corpus/namespaced_maps.clj"] nil "--reporter" "no-op"))
66 | (with-err-str
67 | (driver/run ["corpus/as_alias.clj"] nil "--reporter" "no-op"))])))
68 |
69 | (deftest no-read-eval-test
70 | (is (= [{:expr
71 | '(if true (do (edamame.core/read-eval (prn :hello :world!)) :a))
72 | :line 1
73 | :column 1
74 | :end-line 1
75 | :end-column 41
76 | :alt '(when true (edamame.core/read-eval (prn :hello :world!)) :a)}]
77 | (map #(dissoc % :file)
78 | (driver/run ["corpus/read_eval.clj"] nil "--reporter" "no-op")))))
79 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com).
4 |
5 | ## [Unreleased]
6 |
7 | ## 0.1.11 / 2024-05-16
8 |
9 | * Update to Clojure 1.11 to handle ##Inf etc. [#237](https://github.com/clj-commons/kibit/pull/237)
10 | * Switch to borkdude/edamame for side-effect free parsing. [#235](https://github.com/clj-commons/kibit/pull/235), [#246](https://github.com/clj-commons/kibit/pull/246)
11 | * Correctly gather options-spec require vectors as maps so we can check for :as and :as-alias. [#238](https://github.com/clj-commons/kibit/pull/238)
12 | * Moved all of the test/resources files to a new corpus folder which isn't loaded by default on test runs.
13 | * Clarify maintenance status in README.
14 |
15 | ## 0.1.10 / 2024-05-09
16 |
17 | * Fix scm information in generated pom.xml.
18 |
19 | ## [0.1.9] / 2024-05-09
20 |
21 | ### Added
22 |
23 | * Add deps.edn section to the README [#251](https://github.com/clj-commons/kibit/pull/251) - [@port19x](https://github.com/port19x)
24 | * Add `kibit.driver/exec` to further support deps.edn usage [#252](https://github.com/clj-commons/kibit/pull/252) - [@carrete](https://github.com/carrete)
25 |
26 | ### Changed
27 |
28 | * Include end of range in `simplify-map` [#239](https://github.com/clj-commons/kibit/pull/239) - [@svdo](https://github.com/svdo)
29 | * Update README, fix various typos [#256](https://github.com/clj-commons/kibit/pull/256) - [@terop](https://github.com/terop)
30 |
31 | ### Fixed
32 |
33 | * Kibit cannot parse ns with string requires [#244](https://github.com/clj-commons/kibit/pull/244) and [#247](https://github.com/clj-commons/kibit/pull/247) - [@marksto](https://github.com/marksto)
34 |
35 | ## [0.1.8] / 2019-11-18
36 |
37 | ### Fixed
38 |
39 | * Handle namespace docstrings [231](https://github.com/jonase/kibit/issues/231) - [@tomjkidd](https://github.com/tomjkidd)
40 |
41 | ## [0.1.7] / 2019-07-15
42 |
43 | ### Added
44 |
45 | * Print the file path, column, and line number if an error occurs while reading a file. [#212](https://github.com/jonase/kibit/pull/212) - [@LukasRychtecky](https://github.com/LukasRychtecky)
46 |
47 | ### Changed
48 |
49 | * Use platform-independent newlines when joining strings
50 |
51 | ### Fixed
52 |
53 | * Make tests pass on Windows. [#208](https://github.com/jonase/kibit/pull/208) - [@voytech](https://github.com/voytech)
54 | * Transfer `:repositories` from the original project.clj. [#222](https://github.com/jonase/kibit/pull/226) - [@tomjkidd](https://github.com/tomjkidd)
55 | * Handle nested requires with aliases. [#226](https://github.com/jonase/kibit/pull/226)- [@tomjkidd](https://github.com/tomjkidd)
56 |
57 | ## [0.1.6] / 2018-11-08
58 |
59 | ### Fixed
60 |
61 | * A long awaited feature/fix - Kibit now supports reading namespaced keywords correctly. A very special thanks to Alex Redington who took this tricky task on. [#198](https://github.com/jonase/kibit/pull/198).
62 | * Make Kibit work with local-repos. [#195](https://github.com/jonase/kibit/pull/195)
63 | * Fixup the monkeypatching. [#192](https://github.com/jonase/kibit/pull/192)
64 | * Add alias support to the reader
65 | * Improve source path handling to prevent checking duplicates
66 |
67 | ## [0.1.5] / 2017-05-02
68 |
69 | * 0.1.4, but released properly.
70 |
71 | ## [0.1.4] / 2017-05-05
72 |
73 | ### Additions
74 |
75 | * Automatic replacement of suggestions (`--replace` and `--interactive` cli arguments)
76 | * Rules for using `run!` instead of `(dorun (map f coll))`
77 |
78 | ## [0.1.3] / 2016-11-21
79 | ### Additions
80 |
81 | * Enabled Emacs' next error function to go to next Kibit suggestion. See the updated code in the README for the change.
82 | * #172 Kibit can now handle sets without crashing!
83 | * #152 Send exceptions to STDERR instead of STDOUT
84 | * New rules (#154, #165, )
85 | * #168 Bumped to new versions of clojure and tools.cli dependencies
86 | * #171 Update core.logic to avoid exception from spec
87 |
88 | ## [0.1.2] / 2015-04-21
89 | ### Additions
90 | * Clojurescript/Cljx support (cljc support coming soon). This just works™, kibit will pick up your source paths from your `project.clj`'s `:source-paths`, `[:cljsbuild :builds]`, and `[:cljx :builds]`.
91 | * Non-zero exit codes. Kibit now exits non-zero when one or more suggestions are made. This is particularly useful for those running checks in a CI environment.
92 | * You can now run kibit on any Clojure project without a project.clj file. Just call `lein kibit` with any number of files and folders and it will inspect the Clojure files contained within.
93 |
--------------------------------------------------------------------------------
/src/kibit/replace.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.replace
2 | (:require [clojure.string :as str]
3 | [clojure.java.io :as io]
4 | [rewrite-clj.zip :as rewrite.zip]
5 | [rewrite-clj.node :as rewrite.node]
6 | [kibit.check :as check]
7 | [kibit.reporters :as reporters]))
8 |
9 | (defn- prompt
10 | "Create a yes/no prompt using the given message.
11 |
12 | From `leiningen.ancient.console`."
13 | [& msg]
14 | (let [msg (str (str/join msg) " [yes/no] ")]
15 | (locking *out*
16 | (loop [i 0]
17 | (when (= (mod i 4) 2)
18 | (println "*** please type in one of 'yes'/'y' or 'no'/'n' ***"))
19 | (print msg)
20 | (flush)
21 | (let [r (or (read-line) "")
22 | r (.toLowerCase ^String r)]
23 | (case r
24 | ("yes" "y") true
25 | ("no" "n") false
26 | (recur (inc i))))))))
27 |
28 | (defn- report-or-prompt
29 | ""
30 | [file interactive? {:keys [line expr alt]}]
31 | (if interactive?
32 | (prompt (with-out-str
33 | (println "Would you like to replace")
34 | (reporters/pprint-code expr)
35 | (println " with")
36 | (reporters/pprint-code alt)
37 | (print (format "in %s:%s?" file line))))
38 | (do
39 | (println "Replacing")
40 | (reporters/pprint-code expr)
41 | (println " with")
42 | (reporters/pprint-code alt)
43 | (println (format "in %s:%s" file line))
44 |
45 | true)))
46 |
47 | (def ^:private expr? (comp not rewrite.node/printable-only? rewrite.zip/node))
48 |
49 | (defn- map-zipper
50 | "Apply `f` to all code forms in `zipper0`"
51 | [f zipper0]
52 | (let [zipper (if (expr? zipper0)
53 | (rewrite.zip/postwalk zipper0
54 | expr?
55 | f)
56 | zipper0)]
57 | (if (rewrite.zip/rightmost? zipper)
58 | zipper
59 | (recur f (rewrite.zip/right zipper)))))
60 |
61 | (defn- replace-zipper*
62 | ""
63 | [zipper reporter kw-opts]
64 | (if-let [check-map (apply check/check-expr
65 | (rewrite.zip/sexpr zipper)
66 | :resolution
67 | :subform
68 | kw-opts)]
69 | (if (reporter (assoc check-map
70 | :line
71 | (-> zipper rewrite.zip/node meta :row)))
72 | (recur (rewrite.zip/edit zipper
73 | (fn -replace-zipper [sexpr]
74 | (let [alt (:alt check-map)]
75 | (if (meta alt)
76 | (vary-meta alt
77 | (fn -remove-loc [m]
78 | (dissoc m
79 | :line
80 | :column)))
81 | alt))))
82 | reporter
83 | kw-opts)
84 | zipper)
85 | zipper))
86 |
87 | (defn- replace-zipper
88 | ""
89 | [zipper & kw-opts]
90 | (let [options (apply hash-map kw-opts)]
91 | ;; TODO use (:reporter options) to determine format?
92 | (replace-zipper* zipper
93 | (partial report-or-prompt
94 | (:file options)
95 | (:interactive options))
96 | kw-opts)))
97 |
98 | (defn replace-expr
99 | "Apply any suggestions to `expr`.
100 |
101 | `expr` - Code form to check and replace
102 | `kw-opts` - any valid option for `check/check-expr`, as well as:
103 | - `:file` current filename
104 | - `:interactive` prompt for confirmation before replacement or not
105 |
106 | Returns a string of the replaced form"
107 | [expr & kw-opts]
108 | (->> (str expr)
109 | rewrite.zip/of-string
110 | (map-zipper (fn -replace-expr [node]
111 | (apply replace-zipper
112 | node
113 | kw-opts)))
114 | rewrite.zip/root
115 | rewrite.node/sexpr))
116 |
117 | (defn replace-file
118 | "Apply any suggestions to `file`.
119 |
120 | `file` - File to check and replace in
121 | `kw-opts` - any valid option for `check/check-expr`, as well as:
122 | - `:interactive` prompt for confirmation before replacement or not
123 |
124 | Modifies `file`, returns `nil`"
125 | [file & kw-opts]
126 | (->> (slurp file)
127 | rewrite.zip/of-string
128 | (map-zipper (fn -replace-zipper [node]
129 | (apply replace-zipper
130 | node
131 | :file (str file)
132 | kw-opts)))
133 | rewrite.zip/root-string
134 | (spit file)))
135 |
--------------------------------------------------------------------------------
/src/kibit/rules/misc.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.rules.misc
2 | (:require [clojure.core.logic :as logic]
3 | [kibit.rules.util :refer [defrules]]))
4 |
5 | ;; Returns true if symbol is of
6 | ;; form Foo or foo.bar.Baz
7 | (defn class-symbol? [sym]
8 | (let [sym (pr-str sym)
9 | idx (.lastIndexOf sym ".")]
10 | (if (neg? idx)
11 | (Character/isUpperCase (first sym))
12 | (Character/isUpperCase (nth sym (inc idx))))))
13 |
14 |
15 | (defrules rules
16 | ;; clojure.string
17 | [(apply str (interpose ?x ?y)) (clojure.string/join ?x ?y)]
18 | [(apply str (reverse ?x)) (clojure.string/reverse ?x)]
19 | [(apply str ?x) (clojure.string/join ?x)]
20 |
21 | ;; mapcat
22 | [(apply concat (apply map ?x ?y)) (mapcat ?x ?y)]
23 | [(apply concat (map ?x . ?y)) (mapcat ?x . ?y)]
24 |
25 | ;; filter
26 | [(filter (complement ?pred) ?coll) (remove ?pred ?coll)]
27 | [(filter seq ?coll) (remove empty? ?coll)]
28 | [(filter (fn* [?x] (not (?pred ?x))) ?coll) (remove ?pred ?coll)]
29 | [(filter (fn [?x] (not (?pred ?x))) ?coll) (remove ?pred ?coll)]
30 | [(vec (filter ?pred ?coll)) (filterv ?pred ?coll)]
31 |
32 | ;; first/next shorthands
33 | [(first (first ?coll)) (ffirst ?coll)]
34 | [(first (next ?coll)) (fnext ?coll)]
35 | [(next (next ?coll)) (nnext ?coll)]
36 | [(next (first ?coll)) (nfirst ?coll)]
37 |
38 | ;; Unneeded anonymous functions
39 | (let [fun (logic/lvar)
40 | args (logic/lvar)]
41 | [(fn [expr]
42 | (logic/all
43 | (logic/conde
44 | [(logic/== expr (list 'fn args (logic/llist fun args)))]
45 | [(logic/== expr (list 'fn* args (logic/llist fun args)))])
46 | (logic/pred fun #(or (keyword? %)
47 | (and (symbol? %)
48 | (not-any? #{\/ \.} (str %)))))))
49 | #(logic/== % fun)])
50 |
51 | ;; Java stuff
52 | [(.toString ?x) (str ?x)]
53 |
54 | (let [obj (logic/lvar)
55 | method (logic/lvar)
56 | args (logic/lvar)]
57 | [#(logic/all
58 | (logic/== % (logic/llist '. obj method args))
59 | (logic/pred obj (complement class-symbol?)))
60 | #(logic/project [method args]
61 | (let [s? (seq? method)
62 | args (if s? (rest method) args)
63 | method (if s? (first method) method)]
64 | (logic/== % `(~(symbol (str "." method)) ~obj ~@args))))])
65 |
66 | (let [klass (logic/lvar)
67 | static-method (logic/lvar)
68 | args (logic/lvar)]
69 | [#(logic/all
70 | (logic/== % (logic/llist '. klass static-method args))
71 | (logic/pred klass class-symbol?))
72 | #(logic/project [klass static-method args]
73 | (let [s? (seq? static-method)
74 | args (if s? (rest static-method) args)
75 | static-method (if s? (first static-method) static-method)]
76 | (logic/== % `(~(symbol (str klass "/" static-method)) ~@args))))])
77 |
78 | ;; Threading
79 | (let [form (logic/lvar)
80 | arg (logic/lvar)]
81 | [#(logic/all (logic/== % (list '-> arg form)))
82 | (fn [sbst]
83 | (logic/conde
84 | [(logic/all
85 | (logic/pred form #(or (symbol? %) (keyword? %)))
86 | (logic/== sbst (list form arg)))]
87 | [(logic/all
88 | (logic/pred form seq?)
89 | (logic/project [form]
90 | (logic/== sbst (list* (first form) arg (rest form)))))]))])
91 |
92 | (let [form (logic/lvar)
93 | arg (logic/lvar)]
94 | [#(logic/all (logic/== % (list '->> arg form)))
95 | (fn [sbst]
96 | (logic/conde
97 | [(logic/all
98 | (logic/pred form #(or (symbol? %) (keyword? %)))
99 | (logic/== sbst (list form arg)))]
100 | [(logic/all
101 | (logic/pred form seq?)
102 | (logic/project [form]
103 | (logic/== sbst (concat form (list arg)))))]))])
104 |
105 |
106 | ;; Other
107 | [(not (some ?pred ?coll)) (not-any? ?pred ?coll)]
108 | [(with-meta ?x (?f (meta ?x) . ?arg)) (vary-meta ?x ?f . ?arg)])
109 |
110 |
111 | (comment
112 | (apply concat (apply map f (apply str (interpose \, "Hello"))))
113 | (filter (complement nil?) [1 2 3])
114 |
115 | (.toString (apply str (reverse "Hello")))
116 |
117 | (map (fn [x] (inc x)) [1 2 3])
118 | (map (fn [x] (.method x)) [1 2 3])
119 | (map #(dec %) [1 2 3])
120 | (map #(.method %) [1 2 3])
121 | (map #(Double/parseDouble %) [1 2 3])
122 | (map (fn [x] (Integer/parseInteger x))
123 | [1 2 3])
124 |
125 |
126 | (map (fn [m] (:key m)) [some maps])
127 | (map (fn [m] (:key m alt)) [a b c])
128 |
129 | (. obj toString)
130 | (. obj toString a b c)
131 |
132 | (. Thread (sleep (read-string "2000")))
133 | (. Thread sleep (read-string "2000"))
134 |
135 | (-> x f) ;; (f x)
136 | (-> x (f a b)) ;; (f x a b)
137 | (-> x (f)) ;; (f x)
138 |
139 | (->> x f) ;; (f x)
140 | (->> x (f a b)) ;; (f a b x)
141 | (->> x (f)) ;; (f x)
142 |
143 | )
144 |
--------------------------------------------------------------------------------
/src/kibit/driver.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.driver
2 | "The (leiningen) facing interface for Kibit. Provides helpers for finding files in a project, and
3 | linting a list of files."
4 | (:require [clojure.java.io :as io]
5 | [clojure.tools.cli :refer [cli]]
6 | [kibit.check :refer [check-file]]
7 | [kibit.replace :refer [replace-file]]
8 | [kibit.reporters :refer [name-to-reporter cli-reporter]]
9 | [kibit.rules :refer [all-rules]])
10 | (:import java.io.File))
11 |
12 | (def cli-specs [["-r" "--reporter"
13 | "The reporter used when rendering suggestions"
14 | :default "text"]
15 | ["-e" "--replace"
16 | "Automatically apply suggestions to source file"
17 | :flag true]
18 | ["-i" "--interactive"
19 | "Interactively prompt before replacing suggestions in source file (Requires `--replace`)"
20 | :flag true]])
21 |
22 | (defn ends-with?
23 | "Returns true if the java.io.File ends in any of the strings in coll"
24 | [file coll]
25 | (boolean (some #(.endsWith (.getName ^File file) %) coll)))
26 |
27 | (defn clojure-file?
28 | "Returns true if the java.io.File represents a Clojure source file.
29 | Extensions taken from https://github.com/github/linguist/blob/master/lib/linguist/languages.yml"
30 | [file]
31 | (and (.isFile ^File file)
32 | (ends-with? file [".clj" ".cl2" ".cljc" ".cljs" ".cljscm" ".cljx" ".hic" ".hl"])))
33 |
34 | (defn find-clojure-sources-in-dir
35 | "Searches recursively under dir for Clojure source files.
36 | Returns a sequence of File objects, in breadth-first sort order.
37 | Taken from clojure.tools.namespace.find"
38 | [^File dir]
39 | ;; Use sort by absolute path to get breadth-first search.
40 | (sort-by #(.getAbsolutePath ^File %)
41 | (filter clojure-file? (file-seq dir))))
42 |
43 | (defn- run-replace [source-files rules options]
44 | (doseq [file source-files]
45 | (replace-file file
46 | :rules (or rules all-rules)
47 | :interactive (:interactive options))))
48 |
49 | (defn- run-check [source-files rules {:keys [reporter]}]
50 | (mapcat (fn [file] (try (check-file file
51 | :reporter (name-to-reporter reporter
52 | cli-reporter)
53 | :rules (or rules all-rules))
54 | (catch Exception e
55 | (let [e-info (ex-data e)]
56 | (binding [*out* *err*]
57 | (println (format "Check failed -- skipping rest of file (%s:%s:%s)"
58 | (.getPath ^File file)
59 | (:line e-info)
60 | (:column e-info)))
61 | (println (.getMessage e)))))))
62 | source-files))
63 |
64 | (defn run
65 | "Runs the kibit checker against the given paths, rules and args.
66 |
67 | Paths is expected to be a sequence of io.File objects.
68 |
69 | Rules is either a collection of rules or nil. If rules is nil, all of kibit's checkers are used.
70 |
71 | Optionally accepts a :reporter keyword argument, defaulting to \"text\"
72 | If :replace is provided in options, suggested replacements will be performed automatically."
73 | [source-paths rules & args]
74 | (let [[options file-args usage-text] (apply (partial cli args) cli-specs)
75 | source-files (mapcat #(-> % io/file find-clojure-sources-in-dir)
76 | (if (empty? file-args) source-paths file-args))]
77 | (if (:replace options)
78 | (run-replace source-files rules options)
79 | (run-check source-files rules options))))
80 |
81 | (defn external-run
82 | "Used by lein-kibit to count the results and exit with exit-code 1 if results are found"
83 | [source-paths rules & args]
84 | (if (zero? (count (apply run source-paths rules args)))
85 | (System/exit 0)
86 | (System/exit 1)))
87 |
88 | (defn exec
89 | "Given a [Clojure CLI-style map][Execute a function] `options`, turn this map
90 | into an equivalent set of options as expected by run and external-run above.
91 |
92 | Please note that rules is not supported. nil is passed to external-run below
93 | which enables all rules (see kibit.rules/all-rules).
94 |
95 | **DO NOT** escape the dobule-quotes in deps.edn or on the command-line.
96 |
97 | To make use of this, add an alias, e.g. kibit, to deps.edn:
98 |
99 | :kibit {:extra-deps {jonase/kibit {:mvn/version \"0.1.11\"}}
100 | :exec-fn kibit.driver/exec
101 | :exec-args {:paths [\"src\" \"test\"]}}
102 |
103 | Then run:
104 |
105 | clojure -X:kibit
106 |
107 | Additional command-line options can be added in deps.edn or on the
108 | command-line. For example, in deps.edn:
109 |
110 | :kibit {:extra-deps {jonase/kibit {:mvn/version \"0.1.11\"}}
111 | :exec-fn kibit.driver/exec
112 | :exec-args {:paths [\"src\" \"test\"]
113 | :interactive true}}
114 |
115 | Or on the command-line:
116 |
117 | clojure -X:kibit :interactive true
118 |
119 | To use [babashka.cli][babashka.cli], update the kibit alias in deps.edn:
120 |
121 | :kibit {:extra-deps {jonase/kibit {:mvn/version \"0.1.11\"}}
122 | org.babashka/cli {:mvn/version \"0.7.51\"}}
123 | :exec-fn kibit.driver/exec
124 | :exec-args {:paths [\"src\" \"test\"]}
125 | :main-opts [\"-m\" \"babashka.cli.exec\"]}
126 |
127 | Then run:
128 |
129 | clojure -X:kibit -i -r markdown
130 |
131 | [Execute a function]: https://clojure.org/reference/deps_and_cli#_execute_a_function
132 | [babashka.cli]: https://github.com/babashka/cli
133 | "
134 | {:org.babashka/cli {:alias {:r :reporter
135 | :e :replace
136 | :i :interactive}
137 | :coerce {:paths [:string]
138 | :reporter :string
139 | :replace :boolean
140 | :interactive :boolean}}}
141 | [{:keys [paths reporter replace interactive] :as _options}]
142 | (apply (partial external-run paths nil) ["-r" (or reporter "text")
143 | (when replace "-e")
144 | (when interactive "-i")]))
145 |
--------------------------------------------------------------------------------
/src/kibit/check/reader.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.check.reader
2 | (:require [edamame.core :as e])
3 | (:import [clojure.lang LineNumberingPushbackReader]))
4 |
5 | ;; Preprocessing
6 | ;; -------------
7 | ;; Alias pre-processing
8 |
9 | ;; It is necessary at read time to maintain a small amount of state
10 | ;; about the contents of the stream read so far to enable the proper
11 | ;; reading of aliased keywords. Clojure accomplishes this during
12 | ;; `(:require ...)` because evaluation and compilation is interlaced
13 | ;; with reading so as to establish aliases in a namespace as it is
14 | ;; being loaded. To do this statically we need to maintain a temporary
15 | ;; table of namespaces to aliases. Additionally, we cannot simply use
16 | ;; a map of aliases as it is possible to switch namespaces mid-file
17 | ;; and get one stream to effectively hop between two namespaces.
18 |
19 | (defmulti derive-aliases first :default 'ns)
20 |
21 | (defn unquote-if-quoted
22 | [form]
23 | (if (and (seq? form)
24 | (= 'quote (first form)))
25 | (second form)
26 | form))
27 |
28 | ;; NOTE: `prefix-spec?`, `options-spec?`, and `deps-from-libspec` were derived
29 | ;; from the private fns defined in [clojure.tools.namespace.parse][1]
30 | ;; [1]: https://github.com/clojure/tools.namespace
31 | (defn- prefix-spec?
32 | "Returns true if form represents a libspec prefix list like
33 | (prefix name1 name1) or [com.example.prefix [name1 :as name1]]"
34 | [form]
35 | (and (sequential? form) ; should be a list, but often is not
36 | (symbol? (first form))
37 | (not-any? keyword? form)
38 | (< 1 (count form)))) ; not a bare vector like [foo]
39 |
40 | (defn- option-spec?
41 | "Returns true if form represents a libspec vector containing optional
42 | keyword arguments like [namespace :as alias] or
43 | [namespace :refer (x y)] or just [namespace]"
44 | [form]
45 | (and (sequential? form) ; should be a vector, but often is not
46 | (symbol? (first form))
47 | (or (keyword? (second form)) ; vector like [foo :as f]
48 | (= 1 (count form))))) ; bare vector like [foo]
49 |
50 | (defn- js-dep-spec?
51 | "A version of `option-spec?` for native JS dependencies, i.e. vectors
52 | like [\"react-dom\" :as react-dom] or just [\"some-polyfill\"]"
53 | [form]
54 | (and (sequential? form) ; should be a vector, but often is not
55 | (string? (first form))
56 | (or (keyword? (second form)) ; vector like ["foo" :as f]
57 | (= 1 (count form)))))
58 |
59 | (defn- deps-from-libspec
60 | "A slight modification from clojure.tools.namespace.parse/deps-from-libspec,
61 | in which aliases are captured as metadata."
62 | [prefix form alias]
63 | (cond (prefix-spec? form)
64 | (mapcat (fn [f] (deps-from-libspec
65 | (symbol (str (when prefix (str prefix "."))
66 | (first form)))
67 | f
68 | nil))
69 | (rest form))
70 |
71 | (option-spec? form)
72 | (let [opts (apply hash-map (next form))]
73 | (deps-from-libspec prefix (first form) (or (:as opts) (:as-alias opts))))
74 |
75 | (symbol? form)
76 | (list (with-meta
77 | (symbol (str (when prefix (str prefix ".")) form))
78 | {:alias alias}))
79 |
80 | (js-dep-spec? form) ; to stop it from going into the JS code
81 | nil
82 |
83 | (#{:reload-all} form) ; Some people write (:require ... :reload-all)
84 | nil
85 |
86 | :else
87 | (throw (ex-info "Unparsable namespace form"
88 | {:reason ::unparsable-ns-form
89 | :form form}))))
90 |
91 | (defn derive-aliases-from-deps
92 | "Takes a vector of `deps`, of which each element is in the form accepted by
93 | the `ns` and `require` functions to specify dependencies. Returns a map where
94 | each key is a clojure.lang.Symbol that represents the alias, and each value
95 | is the clojure.lang.Symbol that represents the namespace that the alias refers to."
96 | [deps]
97 | (->> deps
98 | (mapcat #(deps-from-libspec nil (unquote-if-quoted %) nil))
99 | (remove (comp nil? :alias meta))
100 | (into {} (map (fn [dep] [(-> dep meta :alias) dep])))))
101 |
102 | (defmethod derive-aliases 'ns
103 | [[_ _ns & ns-asserts]]
104 | (->> ns-asserts
105 | (remove #(or (string? %) (map? %)))
106 | (group-by (comp keyword name first))
107 | ((juxt :require :require-macros))
108 | (apply concat)
109 | (map (comp derive-aliases-from-deps rest))
110 | (apply merge)))
111 |
112 | (defmethod derive-aliases 'require
113 | [[_ & deps]]
114 | (derive-aliases-from-deps deps))
115 |
116 | (defmethod derive-aliases 'alias
117 | [[_ alias namespace-sym]]
118 | ;; Remove quotes
119 | {(second alias) (second namespace-sym)})
120 |
121 | ;; Reading source files
122 | ;; --------------------
123 | ;; ### Extracting forms
124 |
125 | ;; `read-file` is intended to be used with a Clojure source file,
126 | ;; read in by Clojure's LineNumberingPushbackReader *(LNPR)*. Expressions are
127 | ;; extracted using the clojure reader (ala `read`), and line numbers
128 | ;; are added as `:line` metadata to the forms (via LNPR).
129 |
130 | (defn- careful-refer
131 | "Refers into the provided namespace all public vars from clojure.core
132 | except for those that would clobber any existing interned vars in that
133 | namespace. This is needed to ensure that symbols read within syntax-quote
134 | end up being fully-qualified to clojure.core as appropriate, and only
135 | to *ns* if they're not available there. AFAICT, this will work for all
136 | symbols in syntax-quote except for those referring to vars that are referred
137 | into the namespace."
138 | [ns]
139 | (binding [*ns* ns]
140 | (refer 'clojure.core :exclude (or (keys (ns-interns ns)) ())))
141 | ns)
142 |
143 | (def eof (Object.))
144 |
145 | (defn- make-edamame-opts [alias-map]
146 | (e/normalize-opts
147 | {:all true
148 | :read_eval true
149 | :eof eof
150 | :row-key :line
151 | :col-key :column
152 | :end-row-key :end-line
153 | :end-col-key :end-column
154 | :end-location true
155 | :features #{:clj :cljs}
156 | :read-cond :allow
157 | :readers (fn reader [r]
158 | (fn reader-value [v]
159 | (list r v)))
160 | :auto-resolve (fn [x]
161 | (if (= :current x)
162 | *ns*
163 | (get alias-map x)))}))
164 |
165 | (defn read-file
166 | "Generate a lazy sequence of top level forms from a
167 | LineNumberingPushbackReader"
168 | [^LineNumberingPushbackReader r init-ns]
169 | (let [ns (careful-refer (create-ns init-ns))
170 | do-read (fn do-read [ns alias-map]
171 | (lazy-seq
172 | (let [form (binding [*ns* ns]
173 | (e/parse-next r (make-edamame-opts
174 | (merge (ns-aliases ns)
175 | (alias-map ns)))))
176 | [ns? new-ns] (when (sequential? form) form)
177 | new-ns (unquote-if-quoted new-ns)
178 | ns (if (and (symbol? new-ns)
179 | (#{'ns 'in-ns} ns?))
180 | (careful-refer (create-ns new-ns))
181 | ns)
182 | alias-map (if (#{'require 'ns 'alias} ns?)
183 | (update alias-map ns
184 | merge
185 | (derive-aliases form))
186 | alias-map)]
187 | (when-not (= form eof)
188 | (cons form (do-read ns alias-map))))))]
189 | (do-read ns {ns {}})))
190 |
--------------------------------------------------------------------------------
/src/kibit/check.clj:
--------------------------------------------------------------------------------
1 | (ns kibit.check
2 | "Kibit's integration point and public API"
3 | (:require [clojure.java.io :as io]
4 | [kibit.core :as core]
5 | [kibit.rules :as core-rules]
6 | [kibit.reporters :as reporters]
7 | [kibit.monkeypatch
8 | :refer [with-monkeypatches kibit-redefs]]
9 | [kibit.check.reader :as kibit-read])
10 | (:import [clojure.lang LineNumberingPushbackReader]))
11 |
12 | ;; ### Overview
13 | ;; The public API for Kibit is through the `check-*` functions below.
14 | ;;
15 | ;; * `check-expr` - for checking single expressions (great on the REPL)
16 | ;; * `check-reader` - for checking all forms passed in via a `PushbackReader`
17 | ;; * `check-file` - for checking any file, or string/URI/URL for a file
18 | ;;
19 | ;; All other functions in this namespace exist to provide support and ease
20 | ;; of use for integrated Kibit into other technologies.
21 |
22 | ;; The rule sets
23 | ;; -------------
24 | ;;
25 | ;; Rule sets are stored in individual files that have a top level
26 | ;; `(defrules rules ...)`. The collection of rules are in the `rules`
27 | ;; directory.
28 | ;;
29 | ;; Here, we logically prepare all the rules, by substituting in logic vars
30 | ;; where necessary.
31 | ;;
32 | ;; For more information, see: [rules](#kibit.rules) namespace
33 | (def all-rules core-rules/all-rules)
34 |
35 | ;; ### Analyzing the pieces
36 |
37 | ;; `tree-seq` returns a lazy-seq of nodes for a tree.
38 | ;; Given an expression, we can then match rules against its pieces.
39 | ;; This is like using `clojure.walk` with `identity`:
40 | ;;
41 | ;; user=> (expr-seq '(if (pred? x) (inc x) x))
42 | ;; ((if (pred? x) (inc x) x)
43 | ;; if
44 | ;; (pred? x)
45 | ;; pred?
46 | ;; x
47 | ;; (inc x)
48 | ;; inc
49 | ;; x
50 | ;; x)`
51 | ;;
52 | ;; This is needed for `:subform` reporting.
53 | (defn expr-seq
54 | "Given an expression (any piece of Clojure data), return a lazy (depth-first)
55 | sequence of the expr and all its sub-expressions"
56 | [expr]
57 | (tree-seq sequential?
58 | seq
59 | expr))
60 |
61 | ;; Building results / `simplify-maps`
62 | ;; -----------------------------------
63 |
64 | ;; See the [core](#kibit-core) namespace for details on simplifying an expression.
65 | (defn- build-simplify-map
66 | "Construct the canonical simplify-map
67 | given an expression and a simplified expression."
68 | [expr simplified-expr]
69 | (let [expr-meta (meta expr)]
70 | {:expr expr
71 | :line (:line expr-meta)
72 | :column (:column expr-meta)
73 | :end-line (:end-line expr-meta)
74 | :end-column (:end-column expr-meta)
75 | :alt simplified-expr}))
76 |
77 | ;; ### Guarding the check
78 |
79 | ;; Guarding `check-*` allows for fine-grained control over what
80 | ;; gets passed to a reporter. This allows those using kibit
81 | ;; as a library or building out tool/IDE integration to shape
82 | ;; the results prior to reporting.
83 | ;;
84 | ;; Normally, you'll only want to report an alternative form if it differs
85 | ;; from the original expression form. You can use `identity` to short circuit
86 | ;; the guard and ALWAYS receive the `simlify-map`.
87 | ;;
88 | ;; Check-guards take a map and return a map or nil
89 |
90 | (defn unique-alt?
91 | "A 'check guard' that only returns a result if the
92 | alternative is different than the original expression"
93 | [simplify-map]
94 | (let [{:keys [expr alt]} simplify-map]
95 | (when-not (= alt expr)
96 | simplify-map)))
97 |
98 | ;; Default args for the keyword options passed to the check-* functions
99 | (def ^:private default-args
100 | {:rules all-rules
101 | :guard unique-alt?
102 | :resolution :subform
103 | :init-ns 'user})
104 |
105 | ;; ### Resolution
106 | ;; Kibit can report at various levels of resolution.
107 | ;;
108 | ;; `:toplevel` will simplify a toplevel form, like `(defn ...)`
109 | ;; and all of the subforms it contains. This is exceptionally useful if
110 | ;; you're looking for paragraph-sized suggestions, or you're using
111 | ;; Kibit on the REPL (per expression).
112 | ;;
113 | ;; `:subform` will only report on the subforms. This is most common
114 | ;; for standard reporting, and what gets used when Kibit's Leiningen
115 | ;; plugin is `:verbose false`, the default setting.
116 |
117 | ;; Map the levels of resolution to the correct combination of `simplify`
118 | ;; and `read-seq` functions.
119 | (def ^:private res->simplify
120 | {:toplevel core/simplify
121 | :subform core/simplify-one})
122 |
123 | (def ^:private res->read-seq
124 | {:toplevel (fn [reader init-ns]
125 | (kibit-read/read-file (LineNumberingPushbackReader. reader) init-ns))
126 | :subform (fn [reader init-ns]
127 | (mapcat expr-seq (kibit-read/read-file (LineNumberingPushbackReader. reader) init-ns)))})
128 |
129 | ;; Checking the expressions
130 | ;; ------------------------
131 | ;;
132 | ;; All of the `check-*` functions take an expression and the same
133 | ;; core keyword arguments. They use the most common arguments by default
134 | ;; and all return a sequence of `simplify-maps` that pass the check-guard.
135 | ;;
136 | ;; You can pass in your own `:rules` set, check `:guard`, and toggle
137 | ;; the `:resolution` to achieve your desired output map sequence.
138 | ;;
139 | ;; Here are two examples:
140 | ;;
141 | ;; (check-expr '(if true :a :b))
142 | ;; (check-expr '(if true :a :b)
143 | ;; :rules other-rules
144 | ;; :guard identity
145 | ;; :resolution :subform)
146 |
147 | ;; `check-aux` is the heart of all the check related functions.
148 | ;; The threading expression can be visualized like this `let` block
149 | ;; (formatted for space)
150 | ;;
151 | ;; (let [simplified-expr
152 | ;; ((res->simplify resolution) expr rules)
153 | ;; simplify-map
154 | ;; (build-simplify-map expr simplified-expr)]
155 | ;; (guard simplify-map))
156 | ;;
157 | ;; `simplify-fn` is built from:
158 | ;; `#((res->simplify resolution) % rules)`
159 | (defn- check-aux
160 | "Simplify an expression, build a simplify-map, and guard the returning map"
161 | [expr simplify-fn guard]
162 | (with-monkeypatches kibit-redefs
163 | (->> expr simplify-fn (build-simplify-map expr) guard)))
164 |
165 | ;; The default resolution is overridden via the `merge`
166 | (defn check-expr
167 | [expr & kw-opts]
168 | (let [{:keys [rules guard resolution]}
169 | (merge default-args
170 | {:resolution :toplevel}
171 | (apply hash-map kw-opts))
172 | simplify-fn #((res->simplify resolution) % rules)]
173 | (check-aux expr simplify-fn guard)))
174 |
175 | (defn check-reader
176 | [reader & kw-opts]
177 | (let [{:keys [rules guard resolution init-ns]}
178 | (merge default-args
179 | (apply hash-map kw-opts))
180 | simplify-fn #((res->simplify resolution) % rules)]
181 | (keep #(check-aux % simplify-fn guard)
182 | ((res->read-seq resolution) reader init-ns))))
183 |
184 | (def ^:private default-data-reader-binding
185 | (when (resolve '*default-data-reader-fn*)
186 | {(resolve '*default-data-reader-fn*) (fn [tag val] val)}))
187 |
188 | (defn check-file
189 | [source-file & kw-opts]
190 | (let [{:keys [rules guard resolution reporter init-ns]
191 | :or {reporter reporters/cli-reporter}}
192 | (merge default-args
193 | (apply hash-map kw-opts))]
194 | (with-open [reader (io/reader source-file)]
195 | (with-bindings default-data-reader-binding
196 | (doall (map (fn [simplify-map]
197 | (let [file-simplify (assoc simplify-map :file source-file)]
198 | (reporter file-simplify)
199 | file-simplify))
200 | (check-reader reader
201 | :rules rules
202 | :guard guard
203 | :resolution resolution
204 | :init-ns init-ns)))))))
205 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # kibit
2 |
3 | *There's a function for that!*
4 |
5 | `kibit` is a static code analyzer for Clojure, ClojureScript, [cljx](https://github.com/lynaghk/cljx)
6 | and other Clojure variants. It uses [`core.logic`](https://github.com/clojure/core.logic)
7 | to search for patterns of code that could be rewritten with a more idiomatic function
8 | or macro. For example if kibit finds the code
9 |
10 | ```clojure
11 | (if (some test)
12 | (some action)
13 | nil)
14 | ```
15 |
16 | it will suggest using `when` instead:
17 |
18 | ```clojure
19 | (when (some test)
20 | (some action))
21 | ```
22 |
23 | ## Maintenance Status
24 |
25 | `kibit` is currently in maintenance. It will receive bug fixes and support for new versions of Clojure, but there are no plans for major changes or additions to the core functionality. Why not? `core.logic` is very clever but quite slow, and the existing architecture is such that to move away from `core.logic` would be to rewrite almost the entire library.
26 |
27 | [Splint](https://github.com/NoahTheDuke/splint) is such a rewrite, built from the ground-up to be fast and extensible. It supports all of `kibit`'s rules (and many others), runs in a fraction of the time, can be configured from a file, and can output warnings in `kibit`-like text and markdown, as well as clj-kondo-compatible, json, and edn.
28 |
29 | For other Clojure linters, see [clj-kondo](https://github.com/borkdude/clj-kondo) and [Eastwood](https://github.com/jonase/eastwood).
30 |
31 | ## Usage
32 |
33 | Add `[lein-kibit "0.1.11"]` to your `:plugins` vector in your `:user` profile. Then you can run
34 |
35 | $ lein kibit
36 |
37 | to analyze a Leiningen project's namespaces. Kibit will automatically pick up source paths from your project.clj from the following keyseqs: `[:source-paths]`, `[:cljsbuild :builds]`, and `[:cljx :builds]`. You can also run Kibit manually on individual files or folders (even if there is no Leiningen `project.clj`) by running:
38 |
39 | $ lein kibit path/to/some/file.clj #or
40 | $ lein kibit path/to/src/ #or
41 | $ lein kibit path/to/src/clj/ path/to/src/cljs/util.cljs some/combo/of/files/and/folders.cljx
42 |
43 |
44 | If you want to know how the Kibit rule system works there are some slides available at [http://jonase.github.io/kibit-demo/](http://jonase.github.io/kibit-demo/).
45 |
46 | ## Exit codes
47 |
48 | If `lein kibit` returns any suggestions to forms then its exit code will be 1. Otherwise it will exit 0. This can be useful to add in a build step for automated testing.
49 |
50 |
51 | $ lein kibit
52 | ... suggestions follow
53 |
54 | $ echo $?
55 | 1
56 |
57 | ## Automatically rerunning when files change
58 |
59 | You can use [lein-auto](https://github.com/weavejester/lein-auto) to run kibit automatically when files change. Visit
60 | lein-auto's README for installation instructions. Note that this will run kibit over all of your files, not just the
61 | ones that have changed.
62 |
63 | $ lein auto kibit
64 | auto> Files changed: project.clj, [...]
65 | auto> Running: lein kibit
66 | ... suggestions follow
67 | auto> Failed.
68 | auto> Files changed: test/my/test/misc.clj
69 | auto> Running: lein kibit
70 | ... suggestions follow
71 | auto> Failed.
72 |
73 | ## Automatically replacing suggestions in source file
74 |
75 | You can have kibit automatically apply suggestions to your source files.
76 |
77 | Given a file:
78 |
79 | ```clojure
80 | (ns example)
81 |
82 | (+ 1 a)
83 | ```
84 |
85 | $ lein kibit --replace
86 |
87 | will rewrite the file as:
88 |
89 | ```clojure
90 | (ns example)
91 |
92 | (inc a)
93 | ```
94 |
95 | Replacement can also be run interactively:
96 |
97 | $ lein kibit --replace --interactive
98 | Would you like to replace
99 | (+ 1 a)
100 | with
101 | (inc a)
102 | in example.clj:3? [yes/no]
103 |
104 | ## Reporters
105 |
106 | Kibit comes with two reporters, the default plaintext reporter, and a GitHub Flavoured Markdown reporter. To specify a reporter, use the `-r` or `--reporter` commandline argument. For example:
107 |
108 | lein kibit --reporter markdown
109 | ----
110 | ##### `test/project/core.clj:31`
111 | Consider using:
112 | ```clojure
113 | (when true (println "hi"))
114 | ```
115 | instead of:
116 | ```clojure
117 | (if true (do (println "hi")))
118 | ```
119 |
120 | ----
121 | ##### `test/project/core.clj:32`
122 | Consider using:
123 | ```clojure
124 | (println "hi")
125 | ```
126 | instead of:
127 | ```clojure
128 | (do (println "hi"))
129 | ```
130 |
131 | which renders to:
132 |
133 | ----
134 | ##### `test/project/core.clj:31`
135 | Consider using:
136 | ```clojure
137 | (when true (println "hi"))
138 | ```
139 | instead of:
140 | ```clojure
141 | (if true (do (println "hi")))
142 | ```
143 | ...
144 | ----
145 |
146 | ## Usage from deps.edn
147 |
148 | Add the following to your aliases
149 |
150 | ```clojure
151 | :kibit {:extra-deps {jonase/kibit {:mvn/version "0.1.11"}}
152 | :exec-fn kibit.driver/exec
153 | :exec-args {:paths ["."]}}
154 | ```
155 |
156 | Then run `clojure -X:kibit`. For more options, please see the
157 | docstring on `kibit.driver/exec`.
158 |
159 | **NOTE:** At least Clojure v1.9 and Kibit v0.1.11 are required to use this
160 | exec-fn method.
161 |
162 | ## Usage from inside Emacs
163 |
164 | If you use Emacs for hacking Clojure, here's a way to use kibit from
165 | inside Emacs with all the fanciness you are used to from `M-x compile`.
166 | The [kibit-helper](https://github.com/brunchboy/kibit-helper) package
167 | available from [MELPA](http://melpa.org/) provides several handy
168 | commands. First, make sure you have MELPA available as a source of
169 | packages (which you may well already have done). As described in their
170 | [Getting started](http://melpa.org/#/getting-started) section, put the
171 | following into your `~/.emacs`:
172 |
173 | ```elisp
174 | (add-to-list 'package-archives
175 | '("melpa-stable" . "http://stable.melpa.org/packages/") t)
176 | ```
177 |
178 | (If you want to be more on the cutting edge, you can include unreleased
179 | versions of packages using the non-stable URL, as explained in the
180 | MELPA instructions, but kibit-helper is also available from the less
181 | exciting stable repository.)
182 |
183 | This will give you three new commands,
184 |
185 | M-x kibit
186 | M-x kibit-current-file
187 | M-x kibit-accept-proposed-change
188 |
189 | The first two cause the properly highlighted and hyperlinked kibit output to be
190 | presented in a `*Kibit Suggestions*` buffer. The third lets you automatically
191 | apply most of those suggestions to your source. (Suggestions which cite large
192 | blocks of code including comments cannot be automatically applied, as Kibit
193 | discards comments during processing.)
194 |
195 | You will likely want to bind the last function to C-x
196 | C-\` so it is easy to alternate with the `next-error`
197 | function (conventionally C-x \`) as you walk
198 | through the suggestions made by Kibit:
199 |
200 | ```elisp
201 | (global-set-key (kbd "C-x C-`") 'kibit-accept-proposed-change)
202 | ```
203 |
204 | ## Usage from inside vim (through Leiningen plugin)
205 |
206 | [kibit-vim](https://github.com/fbeline/kibit-vim) allows you to analyze the current opened file by running:
207 |
208 | `:Kibit`
209 |
210 | ## Usage from inside vim (with vim-fireplace)
211 |
212 | If you have [vim-fireplace](https://github.com/tpope/vim-fireplace/) installed, you can use [vim-kibit](https://github.com/humorless/vim-kibit) to run Kibit on your current buffer through the running REPL session.
213 |
214 | vim-kibit is invoked by running:
215 |
216 | `:Kibit`
217 |
218 | ## Known limitations
219 |
220 | Kibit
221 | [reads](http://clojure.github.com/clojure/clojure.core-api.html#clojure.core/read)
222 | source code without any macro expansion or evaluation. A macro can
223 | therefore easily invalidate a rule. Also, kibit will not know if the
224 | symbol `+` in the form `(+ x 1)` actually refers to a local or to a
225 | function in a namespace other than `clojure.core`. Expect
226 | some false positives.
227 |
228 | ## Contributing
229 |
230 | It is very easy to write new patterns for `kibit`. Take a look at
231 | [`control-structures.clj`](https://github.com/clj-commons/kibit/blob/master/src/kibit/rules/control_structures.clj)
232 | to see how new patterns are created. If you know of a recurring
233 | pattern of code that can be simplified, please consider sending me a
234 | pull request.
235 |
236 | Bugs can be reported using the GitHub [issue tracker](https://github.com/clj-commons/kibit/issues/).
237 |
238 | ## Contributors
239 |
240 | Thanks to all who have [contributed](https://github.com/clj-commons/kibit/graphs/contributors) to kibit!
241 |
242 | ## TODO
243 |
244 | * Leiningen project.clj setting for rule exclusion
245 | * Leiningen project.clj setting for a directory of rules to include
246 |
247 | ## License
248 |
249 | Copyright © 2012 Jonas Enlund
250 |
251 | Distributed under the Eclipse Public License, the same as Clojure.
252 |
--------------------------------------------------------------------------------