├── .gitignore ├── README.md ├── project.clj ├── src ├── bikeshed │ └── core.clj └── leiningen │ └── bikeshed.clj └── test └── bikeshed ├── core_test.clj └── core_test.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml* 6 | *.jar 7 | *.class 8 | .lein-* 9 | .nrepl-port 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lein-bikeshed 2 | 3 | A Leiningen plugin designed to tell you your code is bad, and that you 4 | should feel bad. 5 | 6 | ## Usage 7 | 8 | Add to your ~/.lein/profiles.clj: 9 | 10 | ```clojure 11 | {:user {:plugins [[lein-bikeshed "0.5.2"]]}} 12 | ``` 13 | 14 | Just run `lein bikeshed` on your project: 15 | 16 | ``` 17 | ∴ lein bikeshed 18 | 19 | Checking for lines longer than 80 characters. 20 | Badly formatted files: 21 | /home/hinmanm/src/clj/lein-bikeshed/test/bikeshed/core_test.clj:10:(def this-thing-is-over-eighty-characters-long "yep, it certainly is over eighty characters long") 22 | 23 | Checking for lines with trailing whitespace. 24 | Badly formatted files: 25 | /home/hinmanm/src/clj/lein-bikeshed/test/bikeshed/core_test.clj:5:(deftest a-test /home/hinmanm/src/clj/lein-bikeshed/test/bikeshed/core_test.clj:6: (testing "FIXME, I fail, and I have trailing whitespace!" 26 | 27 | Checking for files ending in blank lines. 28 | Badly formatted files: 29 | /home/hinmanm/src/clj/lein-bikeshed/src/bikeshed/core.clj 30 | /home/hinmanm/src/clj/lein-bikeshed/test/bikeshed/core_test.clj 31 | 32 | Checking for redefined var roots in source directories. 33 | with-redefs found in source directory: 34 | /home/hinmanm/src/clj/lein-bikeshed/src/bikeshed/core.clj:17: (with-redefs [+ -] 35 | /home/hinmanm/src/clj/lein-bikeshed/src/bikeshed/core.clj:92: "xargs egrep -H -n '(\\(with-redefs)'") 36 | 37 | Checking whether you keep up with your docstrings. 38 | 1/2 [50.00%] namespaces have docstrings. 39 | 10/12 [83.33%] functions have docstrings. 40 | Use -v to list functions without docstrings")"))) 41 | 42 | Checking for arguments colliding with clojure.core functions. 43 | #'bikeshed.core/colliding-arguments: 'map', 'first' are colliding with core functions 44 | 45 | The following checks failed: 46 | * long-lines 47 | * trailing-whitespace 48 | * trailing-blank-lines 49 | * var-redefs 50 | * name-collisions 51 | 52 | ``` 53 | 54 | ## Options 55 | 56 | | Switches | Default | Desc | 57 | | --------------------------- | ------- | --------------------------- | 58 | | -H, --no-help-me, --help-me | false | Show help | 59 | | -v, --no-verbose, --verbose | false | Display missing doc strings | 60 | | -m, --max-line-length | 80 | Max line length | 61 | | -l, --long-lines | true | Check for lines with length > max line length | 62 | | -w, --trailing-whitespace | true | Check for trailing whitespace | 63 | | -b, --trailing-blank-lines | true | Check for trailing blank lines | 64 | | -r, --var-redefs | true | Check for redefined root vars | 65 | | -d, --docstrings | true | Generate a report of docstring coverage | 66 | | -n, --name-collisions | true | Check for function arg names that collide with clojure.core | 67 | | -x, --exclude-profiles | | Comma-separated profile exclusion | 68 | 69 | You can also add the `:bikeshed` option map directly to your `project.clj`: 70 | 71 | ```clj 72 | (defproject my-thing "1.0.0" 73 | :description "A thing" 74 | ;; Override the default max-line-length 75 | :bikeshed {:max-line-length 60 76 | :var-redefs false 77 | :name-collisions false} 78 | :dependencies [[clj-http "3.3.0"]]) 79 | ``` 80 | 81 | ## License 82 | 83 | Copyright © 2012 Matthew Lee Hinman & Sonian 84 | 85 | Distributed under the Eclipse Public License, the same as Clojure. 86 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject lein-bikeshed "0.5.3-SNAPSHOT" 2 | :description (str "A Leiningen plugin designed to tell you your code is bad, " 3 | "and that you should feel bad") 4 | :url "https://github.com/dakrone/lein-bikeshed" 5 | :license {:name "Eclipse Public License" 6 | :url "http://www.eclipse.org/legal/epl-v10.html"} 7 | :eval-in :leiningen 8 | :dependencies [[org.clojure/tools.cli "0.3.5"] 9 | [org.clojure/tools.namespace "0.2.6"]]) 10 | -------------------------------------------------------------------------------- /src/bikeshed/core.clj: -------------------------------------------------------------------------------- 1 | (ns bikeshed.core 2 | "Define all the functionalities of bikeshed" 3 | (:require [clojure.string :refer [blank? starts-with? trim join]] 4 | [clojure.java.io :as io] 5 | [clojure.tools.namespace.file :as ns-file] 6 | [clojure.tools.namespace.find :as ns-find] 7 | [clojure.string :as str]) 8 | (:import (java.io BufferedReader StringReader File))) 9 | 10 | (defn foo 11 | "I don't do a whole lot." 12 | [x] 13 | (println x "Hello, World!")) 14 | 15 | (defn bad-fn 16 | "this is a bad function." 17 | [] 18 | (with-redefs [+ -] 19 | (+ 2 2))) 20 | 21 | (defn no-docstring 22 | [] 23 | nil) 24 | 25 | (defn empty-docstring 26 | "" ;; hah! take that lein-bikeshed 27 | [] 28 | nil) 29 | 30 | (defn colliding-arguments 31 | "Arguments will be colliding" 32 | ([map]) 33 | ([map first])) 34 | 35 | (defn file-with-extension? 36 | "Returns true if the java.io.File represents a file whose name ends 37 | with one of the Strings in extensions." 38 | [^java.io.File file extensions] 39 | (and (.isFile file) 40 | (let [name (.getName file)] 41 | (some #(.endsWith name %) extensions)))) 42 | 43 | (defn- sort-files-breadth-first 44 | [files] 45 | (sort-by #(.getAbsolutePath ^File %) files)) 46 | 47 | (defn find-sources-in-dir 48 | "Searches recursively under dir for source files. Returns a sequence 49 | of File objects, in breadth-first sort order. 50 | Optional second argument is either clj (default) or cljs, both 51 | defined in clojure.tools.namespace.find." 52 | ([dir] 53 | (find-sources-in-dir dir nil)) 54 | ([^File dir extensions] 55 | (let [extensions (or extensions [".clj" ".cljc"])] 56 | (->> (file-seq dir) 57 | (filter #(file-with-extension? % extensions)) 58 | sort-files-breadth-first)))) 59 | 60 | (defn- get-all 61 | "Returns all the values found for the LOOKED-UP-KEY passed as an argument 62 | recursively walking the MAP-TO-TRAVERSE provided as argument" 63 | ([map-to-traverse looked-up-key] 64 | (let [result (atom [])] 65 | (doseq [[k v] map-to-traverse] 66 | (when (= looked-up-key k) 67 | (swap! result conj v)) 68 | (when (map? v) 69 | (let [sub-map (get-all v looked-up-key)] 70 | (when-not (empty? sub-map) 71 | (reset! result 72 | (apply conj @result sub-map)))))) 73 | @result)) 74 | ([map-to-traverse k & ks] 75 | (mapcat (partial get-all map-to-traverse) (cons k ks)))) 76 | 77 | (defn load-namespace 78 | "Reads a file, returning the namespace name" 79 | [f] 80 | (try 81 | (let [ns-dec (ns-file/read-file-ns-decl f) 82 | ns-name (second ns-dec)] 83 | (require ns-name) 84 | ns-name) 85 | (catch Exception e 86 | (println (str "Unable to parse " f ": " e)) 87 | nil))) 88 | 89 | (defn read-namespace 90 | "Reads a file, returning a map of the namespace to a vector of maps with 91 | information about each var in the namespace." 92 | [f] 93 | (try 94 | (let [ns-dec (ns-file/read-file-ns-decl f) 95 | ns-name (second ns-dec)] 96 | (require ns-name) 97 | (->> ns-name 98 | ns-interns 99 | vals)) 100 | (catch Exception e 101 | (println (str "Unable to parse " f ": " e)) 102 | []))) 103 | 104 | (defn has-doc 105 | "Returns a map of method name to true/false depending on docstring occurance." 106 | [function-name] 107 | {(str function-name) (and (boolean (:doc (meta function-name))) 108 | (not= "" (:doc (meta function-name))))}) 109 | 110 | (defn has-ns-doc 111 | "Returns a map of namespace to true/false depending on docstring occurance." 112 | [namespace-name] 113 | (let [doc (:doc (meta (the-ns (symbol (str namespace-name)))))] 114 | {(str namespace-name) (and (boolean doc) 115 | (not= "" doc))})) 116 | 117 | (defn long-lines 118 | "Complain about lines longer than characters. 119 | max-line-length defaults to 80." 120 | [source-files & {:keys [max-line-length] :or {max-line-length 80}}] 121 | (printf "\nChecking for lines longer than %s characters.\n" max-line-length) 122 | (let [indexed-lines (fn [f] 123 | (with-open [r (io/reader f)] 124 | (doall 125 | (keep-indexed 126 | (fn [idx line] 127 | (when (> (count line) max-line-length) 128 | (trim (join ":" [(.getAbsolutePath f) (inc idx) line])))) 129 | (line-seq r))))) 130 | all-long-lines (flatten (map indexed-lines source-files))] 131 | (if (empty? all-long-lines) 132 | (println "No lines found.") 133 | (do 134 | (println "Badly formatted files:") 135 | (println (join "\n" all-long-lines)) 136 | true)))) 137 | 138 | (defn trailing-whitespace 139 | "Complain about lines with trailing whitespace." 140 | [source-files] 141 | (println "\nChecking for lines with trailing whitespace.") 142 | (let [indexed-lines (fn [f] 143 | (with-open [r (io/reader f)] 144 | (doall 145 | (keep-indexed 146 | (fn [idx line] 147 | (when (re-seq #"\s+$" line) 148 | (trim (join ":" [(.getAbsolutePath f) (inc idx) line])))) 149 | (line-seq r))))) 150 | trailing-whitespace-lines (flatten (map indexed-lines source-files))] 151 | (if (empty? trailing-whitespace-lines) 152 | (println "No lines found.") 153 | (do (println "Badly formatted files:") 154 | (println (join "\n" trailing-whitespace-lines)) 155 | true)))) 156 | 157 | (defn trailing-blank-lines 158 | "Complain about files ending with blank lines." 159 | [source-files] 160 | (println "\nChecking for files ending in blank lines.") 161 | (let [get-last-line (fn [f] 162 | (with-open [r (io/reader f)] 163 | (when (re-matches #"^\s*$" (last (line-seq r))) 164 | (.getAbsolutePath f)))) 165 | bad-files (filter some? (map get-last-line source-files))] 166 | (if (empty? bad-files) 167 | (println "No files found.") 168 | (do (println "Badly formatted files:") 169 | (println (join "\n" bad-files)) 170 | true)))) 171 | 172 | (defn bad-roots 173 | "Complain about the use of with-redefs." 174 | [source-files] 175 | (println "\nChecking for redefined var roots in source directories.") 176 | (let [indexed-lines (fn [f] 177 | (with-open [r (io/reader f)] 178 | (doall 179 | (keep-indexed 180 | (fn [idx line] 181 | (when (re-seq #"\(with-redefs" line) 182 | (trim (join ":" [(.getAbsolutePath f) (inc idx) line])))) 183 | (line-seq r))))) 184 | bad-lines (flatten (map indexed-lines source-files))] 185 | (if (empty? bad-lines) 186 | (println "No with-redefs found.") 187 | (do (println "with-redefs found in source directory:") 188 | (println (join "\n" bad-lines)) 189 | true)))) 190 | 191 | (defn missing-doc-strings 192 | "Report the percentage of missing doc strings." 193 | [project verbose] 194 | (println "\nChecking whether you keep up with your docstrings.") 195 | (try 196 | (let [source-files (mapcat #(-> % io/file 197 | ns-find/find-clojure-sources-in-dir) 198 | (flatten (get-all project :source-paths))) 199 | all-namespaces (->> source-files 200 | (map load-namespace) 201 | (remove nil?)) 202 | all-publics (mapcat read-namespace source-files) 203 | no-docstrings (->> all-publics 204 | (mapcat has-doc) 205 | (filter #(= (val %) false))) 206 | no-ns-doc (->> all-namespaces 207 | (mapcat has-ns-doc) 208 | (filter #(= (val %) false)))] 209 | (printf 210 | "%d/%d [%.2f%%] namespaces have docstrings.\n" 211 | (- (count all-namespaces) (count no-ns-doc)) 212 | (count all-namespaces) 213 | (try 214 | (double 215 | (* 100 (/ (- (count all-namespaces) 216 | (count no-ns-doc)) 217 | (count all-namespaces)))) 218 | (catch ArithmeticException _ Double/NaN))) 219 | (printf 220 | (str "%d/%d [%.2f%%] functions have docstrings.\n" 221 | (when (not verbose) 222 | "Use -v to list namespaces/functions without docstrings\n")) 223 | (- (count all-publics) (count no-docstrings)) 224 | (count all-publics) 225 | (try 226 | (double 227 | (* 100 (/ (- (count all-publics) 228 | (count no-docstrings)) 229 | (count all-publics)))) 230 | (catch ArithmeticException _ Double/NaN))) 231 | (flush) 232 | (when verbose 233 | (println "\nNamespaces without docstrings:") 234 | (doseq [[ns-name _] (sort no-ns-doc)] 235 | (println ns-name))) 236 | (when verbose 237 | (println "\nMethods without docstrings:") 238 | (doseq [[method _] (sort no-docstrings)] 239 | (println method))) 240 | (or (-> no-docstrings count pos?) 241 | (-> no-ns-doc count pos?))) 242 | (catch Throwable t 243 | (println "Sorry, I wasn't able to read your source files -" t)))) 244 | 245 | (defn- wrong-arguments 246 | "Return the list of wrong arguments for the provided function name" 247 | [function-name list-of-forbidden-arguments] 248 | (let [arguments (-> function-name meta :arglists)] 249 | (distinct (flatten (map (fn [args] 250 | (filter #(some (set [%]) 251 | list-of-forbidden-arguments) 252 | args)) 253 | arguments))))) 254 | 255 | (defn- check-all-arguments 256 | "Check if the arguments for functions collide 257 | with function from clojure/core" 258 | [project] 259 | (println "\nChecking for arguments colliding with clojure.core functions.") 260 | (let [core-functions (-> 'clojure.core ns-publics keys) 261 | source-files (mapcat #(-> % io/file 262 | ns-find/find-clojure-sources-in-dir) 263 | (flatten (get-all project :source-paths))) 264 | all-publics (mapcat read-namespace source-files)] 265 | (->> all-publics 266 | (map (fn [function] 267 | (let [args (wrong-arguments function core-functions)] 268 | (when (seq args) 269 | (if (= 1 (count args)) 270 | (println (str function ": '" (first args) "'") 271 | "is colliding with a core function") 272 | (println (str function ": '" 273 | (clojure.string/join "', '" args) "'") 274 | "are colliding with core functions"))) 275 | (count args)))) 276 | (apply +) 277 | (pos?)))) 278 | 279 | (defn visible-project-files 280 | "Given a project and list of keys (such as `:source-paths` or `:test-paths`, 281 | return all source files underneath those directories." 282 | [project & paths-keys] 283 | (remove 284 | #(starts-with? (.getName %) ".") 285 | (mapcat 286 | #(-> % io/file 287 | (find-sources-in-dir [".clj" ".cljs" ".cljc" ".cljx"])) 288 | (flatten (apply get-all project paths-keys))))) 289 | 290 | (defn bikeshed 291 | "Bikesheds your project with totally arbitrary criteria. Returns true if the 292 | code has been bikeshedded and found wanting." 293 | [project {:keys [check? verbose max-line-length]}] 294 | (let [all-files (visible-project-files project :source-paths :test-paths) 295 | source-files (visible-project-files project :source-paths) 296 | results {:long-lines (when (check? :long-lines) 297 | (if max-line-length 298 | (long-lines all-files 299 | :max-line-length max-line-length) 300 | (long-lines all-files))) 301 | :trailing-whitespace (when (check? :trailing-whitespace) 302 | (trailing-whitespace all-files)) 303 | :trailing-blank-lines (when (check? :trailing-blank-lines) 304 | (trailing-blank-lines all-files)) 305 | :var-redefs (when (check? :var-redefs) 306 | (bad-roots source-files)) 307 | :bad-methods (when (check? :docstrings) 308 | (missing-doc-strings project verbose)) 309 | :name-collisions (when (check? :name-collisions) 310 | (check-all-arguments project))} 311 | failures (->> results 312 | (filter second) 313 | (map first) 314 | (remove #{:bad-methods}) 315 | (map name))] 316 | (if (empty? failures) 317 | (println "\nSuccess") 318 | (do (println "\nThe following checks failed:\n *" 319 | (str/join "\n * " failures) 320 | "\n") 321 | failures)))) 322 | -------------------------------------------------------------------------------- /src/leiningen/bikeshed.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.bikeshed 2 | (:require [clojure.string :as str] 3 | [clojure.tools.cli :as cli] 4 | [leiningen.core.eval :as lein] 5 | [leiningen.core.project :as project])) 6 | 7 | (defn help 8 | "Help text displayed from the command line" 9 | [] 10 | "Bikesheds your project with totally arbitrary criteria.") 11 | 12 | (defn bikeshed 13 | "Main function called from Leiningen" 14 | [project & args] 15 | (let [[opts args banner] 16 | (cli/cli 17 | args 18 | ["-H" "--help-me" "Show help" 19 | :flag true :default false] 20 | ["-v" "--verbose" "Display missing doc strings" 21 | :flag true :default false] 22 | ["-m" "--max-line-length" "Max line length" 23 | :parse-fn #(Integer/parseInt %)] 24 | ["-l" "--long-lines" 25 | "If true, check for trailing blank lines" 26 | :parse-fn #(Boolean/valueOf %)] 27 | ["-w" "--trailing-whitespace" 28 | "If true, check for trailing whitespace" 29 | :parse-fn #(Boolean/valueOf %)] 30 | ["-b" "--trailing-blank-lines" 31 | "If true, check for trailing blank lines" 32 | :parse-fn #(Boolean/valueOf %)] 33 | ["-r" "--var-redefs" 34 | "If true, check for redefined var roots in source directories" 35 | :parse-fn #(Boolean/valueOf %)] 36 | ["-d" "--docstrings" 37 | "If true, generate a report of docstring coverage" 38 | :parse-fn #(Boolean/valueOf %)] 39 | ["-n" "--name-collisions" 40 | "If true, check for function arg names that collide with clojure.core" 41 | :parse-fn #(Boolean/valueOf %)] 42 | ["-x" "--exclude-profiles" "Comma-separated profile exclusions" 43 | :default nil 44 | :parse-fn #(mapv keyword (str/split % #","))]) 45 | lein-opts (:bikeshed project) 46 | project (if-let [exclusions (seq (:exclude-profiles opts))] 47 | (-> project 48 | (project/unmerge-profiles exclusions) 49 | (update-in [:profiles] #(apply dissoc % exclusions))) 50 | project)] 51 | (if (:help-me opts) 52 | (println banner) 53 | (lein/eval-in-project 54 | (-> project 55 | (update-in [:dependencies] 56 | conj 57 | ['lein-bikeshed "0.5.3-SNAPSHOT"])) 58 | `(if (bikeshed.core/bikeshed 59 | '~project 60 | {:max-line-length (or (:max-line-length ~opts) 61 | (:max-line-length ~lein-opts)) 62 | :verbose (:verbose ~opts) 63 | :check? #(get (merge ~lein-opts ~opts) % true)}) 64 | (System/exit -1) 65 | (System/exit 0)) 66 | '(require 'bikeshed.core))))) 67 | -------------------------------------------------------------------------------- /test/bikeshed/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns bikeshed.core-test 2 | (:use clojure.test 3 | bikeshed.core)) 4 | 5 | (deftest a-test 6 | (testing "FIXME, I fail, and I have trailing whitespace!" 7 | (is (= 0 1)))) 8 | 9 | (deftest another-test 10 | (with-redefs [get get] 11 | (is (= 2 2)))) 12 | 13 | (def this-thing-is-over-eighty-characters-long "yep, it certainly is over eighty characters long") 14 | 15 | (def a "≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡") 16 | 17 | ;; lot's of extra newlines: 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /test/bikeshed/core_test.cljs: -------------------------------------------------------------------------------- 1 | (ns foo) 2 | 3 | (deftest a-test 4 | (testing "FIXME, I fail, and I have trailing whitespace!" 5 | (is (= 0 1)))) 6 | 7 | (def this-thing-is-over-eighty-characters-long "yep, it certainly is over eighty characters long") 8 | 9 | (def a "≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡") 10 | 11 | ;; lot's of extra newlines: 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | --------------------------------------------------------------------------------