├── 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 | --------------------------------------------------------------------------------