Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom hooks to allow issue reporting somewhere else than stdout ? #37 #38

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,32 @@ You can also add the `:bikeshed` option map directly to your `project.clj`:
:dependencies [[clj-http "3.3.0"]])
```

## Using Bikeshed Processing Results

In order to allow third party library consumers to decide on issue reporting approach, bikeshed returns a record containing processing details:

Results can look as follows:

```clj
{:failures
("long-lines" "var-redefs" "name-collisions"),
:details [
{:description "Line length exceeds 80", :type :long-lines, :issues (
{:file "C:\\PROGRAMMING\\github\\lein-bikeshed\\src\\bikeshed\\core.clj", :line 301, :content " (let [join-args #(str (clojure.string/join \"' '\" %) \"'\")]"}
{:file "C:\\PROGRAMMING\\github\\lein-bikeshed\\src\\bikeshed\\core.clj", :line 349, :content " :max-line-length max-line-length)"}
{:file "C:\\PROGRAMMING\\github\\lein-bikeshed\\src\\bikeshed\\core.clj", :line 352, :content " (trailing-whitespace details all-files))"}
{:file "C:\\PROGRAMMING\\github\\lein-bikeshed\\src\\bikeshed\\core.clj", :line 354, :content " (trailing-blank-lines details all-files))"}
{:file "C:\\PROGRAMMING\\github\\lein-bikeshed\\src\\bikeshed\\core.clj", :line 358, :content " (missing-doc-strings details project verbose))"})}
{:description "Found with-redefs calls", :type :var-redefs, :issues (
{:file "C:\\PROGRAMMING\\github\\lein-bikeshed\\src\\bikeshed\\core.clj", :line 18, :content " (with-redefs [+ -]"}
{:file "C:\\PROGRAMMING\\github\\lein-bikeshed\\src\\bikeshed\\core.clj", :line 201, :content " (when (re-seq #\"\\(with-redefs\" line)"})}
{:description "Arguments colliding with core functions", :type :name-collisions, :issues [
{:file #'bikeshed.core/colliding-arguments, :content "map' 'first'"}]}]}
```
As You can see the record contains two root entries:
- failures - which is a brief summary on what linting methods have failed.
- details - which contain processing details. This entry can contain a lot of data depending on linting method being invoked. General convention is that it contains a vector of reported issues and each issue in that vector is composed of: file, content (which may vary depending on function), line (optional).

## License

Copyright © 2012 Matthew Lee Hinman & Sonian
Expand Down
134 changes: 92 additions & 42 deletions src/bikeshed/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
([map])
([map first]))

(defn add-issue [details issue]
(swap! details conj issue))

(defn file-with-extension?
"Returns true if the java.io.File represents a file whose name ends
with one of the Strings in extensions."
Expand Down Expand Up @@ -114,86 +117,109 @@
{(str namespace-name) (and (boolean doc)
(not= "" doc))}))

(defn- format-issue [issue]
(trim (str (:file issue) ":" (:line issue) ":" (:content issue))))

(defn long-lines
"Complain about lines longer than <max-line-length> characters.
max-line-length defaults to 80."
[source-files & {:keys [max-line-length] :or {max-line-length 80}}]
[details source-files & {:keys [max-line-length] :or {max-line-length 80}}]
(printf "\nChecking for lines longer than %s characters.\n" max-line-length)
(let [indexed-lines (fn [f]
(with-open [r (io/reader f)]
(doall
(keep-indexed
(fn [idx line]
(when (> (count line) max-line-length)
(trim (join ":" [(.getAbsolutePath f) (inc idx) line]))))
{:file (.getAbsolutePath f)
:line (inc idx)
:content line}))
(line-seq r)))))
all-long-lines (flatten (map indexed-lines source-files))]
(if (empty? all-long-lines)
(println "No lines found.")
(do
(println "Badly formatted files:")
(println (join "\n" all-long-lines))
(println (join "\n" (mapv format-issue all-long-lines)))
(add-issue details {:description (str "Line length exceeds "
max-line-length)
:type :long-lines
:issues all-long-lines})
true))))

(defn trailing-whitespace
"Complain about lines with trailing whitespace."
[source-files]
[details source-files]
(println "\nChecking for lines with trailing whitespace.")
(let [indexed-lines (fn [f]
(with-open [r (io/reader f)]
(doall
(keep-indexed
(fn [idx line]
(when (re-seq #"\s+$" line)
(trim (join ":" [(.getAbsolutePath f) (inc idx) line]))))
{:file (.getAbsolutePath f)
:line (inc idx)
:content line}))
(line-seq r)))))
trailing-whitespace-lines (flatten (map indexed-lines source-files))]
(if (empty? trailing-whitespace-lines)
(println "No lines found.")
(do (println "Badly formatted files:")
(println (join "\n" trailing-whitespace-lines))
(println (join "\n" (mapv format-issue trailing-whitespace-lines)))
(add-issue details {:description "Remove trailing whitespace lines"
:type :trailing-whitespace
:issues trailing-whitespace-lines})
true))))

(defn trailing-blank-lines
"Complain about files ending with blank lines."
[source-files]
[details source-files]
(println "\nChecking for files ending in blank lines.")
(let [get-last-line (fn [f]
(with-open [r (io/reader f)]
(when (re-matches #"^\s*$" (last (line-seq r)))
(.getAbsolutePath f))))
{:file (.getAbsolutePath f)})))
bad-files (filter some? (map get-last-line source-files))]
(if (empty? bad-files)
(println "No files found.")
(do (println "Badly formatted files:")
(println (join "\n" bad-files))
(println (join "\n" (mapv #(trim (:file %)) bad-files)))
(add-issue details {:description "Remove blank line at the end"
:type :trailing-blank-lines
:issues bad-files})
true))))

(defn bad-roots
"Complain about the use of with-redefs."
[source-files]
[details source-files]
(println "\nChecking for redefined var roots in source directories.")
(let [indexed-lines (fn [f]
(with-open [r (io/reader f)]
(doall
(keep-indexed
(fn [idx line]
(when (re-seq #"\(with-redefs" line)
(trim (join ":" [(.getAbsolutePath f) (inc idx) line]))))
{:file (.getAbsolutePath f)
:line (inc idx)
:content line}))
(line-seq r)))))
bad-lines (flatten (map indexed-lines source-files))]
(if (empty? bad-lines)
(println "No with-redefs found.")
(do (println "with-redefs found in source directory:")
(println (join "\n" bad-lines))
(println (join "\n" (mapv format-issue bad-lines)))
(add-issue details {:description "Found with-redefs calls"
:type :var-redefs
:issues bad-lines})
true))))

(defn missing-doc-strings
"Report the percentage of missing doc strings."
[project verbose]
[details project verbose]
(println "\nChecking whether you keep up with your docstrings.")
(try
(let [source-files (mapcat #(-> % io/file
(let [issues (atom [])
source-files (mapcat #(-> % io/file
ns-find/find-clojure-sources-in-dir)
(flatten (get-all project :source-paths)))
all-namespaces (->> source-files
Expand Down Expand Up @@ -232,11 +258,20 @@
(when verbose
(println "\nNamespaces without docstrings:")
(doseq [[ns-name _] (sort no-ns-doc)]
(println ns-name)))
(println ns-name)
(swap! issues conj {:file " "
:content (str "Namespace without docstrings: "
ns-name)})))
(when verbose
(println "\nMethods without docstrings:")
(doseq [[method _] (sort no-docstrings)]
(println method)))
(println method)
(swap! issues conj {:file " "
:content (str "Method without docstrings: "
method)})))
(add-issue details {:description "No docstrings"
:type :docstrings
:issues @issues})
(or (-> no-docstrings count pos?)
(-> no-ns-doc count pos?)))
(catch Throwable t
Expand All @@ -255,27 +290,41 @@
(defn- check-all-arguments
"Check if the arguments for functions collide
with function from clojure/core"
[project]
[collisions project]
(println "\nChecking for arguments colliding with clojure.core functions.")
(let [core-functions (-> 'clojure.core ns-publics keys)
source-files (mapcat #(-> % io/file
ns-find/find-clojure-sources-in-dir)
(flatten (get-all project :source-paths)))
all-publics (mapcat read-namespace source-files)]
all-publics (mapcat read-namespace source-files)
add-collision (fn [func args]
(let [join-args #(str (clojure.string/join "' '" %) "'")]
(swap! collisions conj {:file func
:content (join-args args)})))]
(->> all-publics
(map (fn [function]
(let [args (wrong-arguments function core-functions)]
(when (seq args)
(if (= 1 (count args))
(println (str function ": '" (first args) "'")
"is colliding with a core function")
(println (str function ": '"
(clojure.string/join "', '" args) "'")
"are colliding with core functions")))
(do
(if (= 1 (count args))
(println (str function ": '" (first args) "'")
"is colliding with a core function")
(println (str function ": '"
(clojure.string/join "', '" args) "'")
"are colliding with core functions"))
(add-collision function args)))
(count args))))
(apply +)
(pos?))))

(defn check-name-collisions [details project]
(let [collisions (atom [])
result (check-all-arguments collisions project)]
(add-issue details {:description "Arguments colliding with core functions"
:type :name-collisions
:issues @collisions})
result))

(defn visible-project-files
"Given a project and list of keys (such as `:source-paths` or `:test-paths`,
return all source files underneath those directories."
Expand All @@ -291,31 +340,32 @@
"Bikesheds your project with totally arbitrary criteria. Returns true if the
code has been bikeshedded and found wanting."
[project {:keys [check? verbose max-line-length]}]
(let [all-files (visible-project-files project :source-paths :test-paths)
(let [details (atom [])
all-files (visible-project-files project :source-paths :test-paths)
source-files (visible-project-files project :source-paths)
results {:long-lines (when (check? :long-lines)
(if max-line-length
(long-lines all-files
:max-line-length max-line-length)
(long-lines all-files)))
results {:long-lines (when (check? :long-lines)
(if max-line-length
(long-lines details all-files
:max-line-length max-line-length)
(long-lines details all-files)))
:trailing-whitespace (when (check? :trailing-whitespace)
(trailing-whitespace all-files))
(trailing-whitespace details all-files))
:trailing-blank-lines (when (check? :trailing-blank-lines)
(trailing-blank-lines all-files))
:var-redefs (when (check? :var-redefs)
(bad-roots source-files))
:bad-methods (when (check? :docstrings)
(missing-doc-strings project verbose))
:name-collisions (when (check? :name-collisions)
(check-all-arguments project))}
(trailing-blank-lines details all-files))
:var-redefs (when (check? :var-redefs)
(bad-roots details source-files))
:bad-methods (when (check? :docstrings)
(missing-doc-strings details project verbose))
:name-collisions (when (check? :name-collisions)
(check-name-collisions details project))}
failures (->> results
(filter second)
(map first)
(remove #{:bad-methods})
(map name))]
(if (empty? failures)
(println "\nSuccess")
(do (println "\nThe following checks failed:\n *"
(str/join "\n * " failures)
"\n")
failures))))
(println "\nThe following checks failed:\n *"
(str/join "\n * " failures)
"\n"))
{:failures failures :details @details}))