├── .cljstyle ├── .gitignore ├── .projections.json ├── README.md ├── deps.edn ├── epl-v10.html ├── project.clj ├── src └── repetition │ ├── hunter.clj │ └── hunter │ └── core.clj ├── test └── repetition │ └── hunter │ ├── core_test.clj │ └── sample.clj └── tests.edn /.cljstyle: -------------------------------------------------------------------------------- 1 | {:files {:extensions #{"cljc" "cljs" "clj" "cljx" "edn" "cljstyle"} 2 | :ignore #{".hg" ".git" #".clj-kondo/.*?/"}} 3 | :rules {:indentation {:indents ^:replace {#".*" [[:inner 0]]} 4 | :list-indent 2} 5 | :namespaces {:import-break-width 80}}} 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.jar 3 | .cpcache/ 4 | .lein-* 5 | pom.xml 6 | pom.xml.asc 7 | target/ 8 | -------------------------------------------------------------------------------- /.projections.json: -------------------------------------------------------------------------------- 1 | { 2 | "src/*.clj": { 3 | "alternate": "test/{}_test.clj", 4 | "type": "source" 5 | }, 6 | "test/*_test.clj": { 7 | "alternate": "src/{}.clj", 8 | "type": "test" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Repetition Hunter 2 | Do you repeat yourself in your code? This is for you. It finds 3 | repetitions in your code. 4 | 5 | Add `[repetition-hunter "1.0.0"]` to the `:dependencies` of your 6 | `:user` profile. 7 | 8 | It works with clojure version 1.2.0 and up. 9 | 10 | ## Usage 11 | ### Lein plugin 12 | [Andrés Gómez Urquiza](https://github.com/nez) created a lein plugin that you can find at 13 | [https://github.com/fractalLabs/lein-repetition-hunter](https://github.com/fractalLabs/lein-repetition-hunter) 14 | 15 | Thanks @nez! 16 | 17 | ### As a library 18 | You can use it from the REPL: 19 | 20 | user=> (use 'repetition.hunter) 21 | nil 22 | user=> (require 'your.namespace) 23 | nil 24 | user=> (hunt 'your.namespace) 25 | 2 repetitions of complexity 5 26 | 27 | Line 474 - your.namespace: 28 | (or (modifiers->str mname) (name mname)) 29 | 30 | Line 479 - your.namespace: 31 | (or (modifiers->str m) (name m)) 32 | 33 | ====================================================================== 34 | 35 | 3 repetitions of complexity 5 36 | 37 | Line 50 - your.namespace: 38 | (str "(" (first t) ")") 39 | 40 | Line 294 - your.namespace: 41 | (str "(" (first f) ")") 42 | 43 | Line 360 - your.namespace: 44 | (str "(" (first c) ")") 45 | 46 | ====================================================================== 47 | 48 | 2 repetitions of complexity 7 49 | 50 | Line 162 - your.namespace: 51 | (str/join ", " (map (partial identifier->str db) column)) 52 | 53 | Line 170 - your.namespace: 54 | (str/join ", " (map (partial identifier->str db) column)) 55 | 56 | ====================================================================== 57 | nil 58 | 59 | Each repetition is presented with a header showing the number of repetitions 60 | and their complexity. Complexity is the count of flatten the form 61 | `(count (flatten (form)))`. It is sorted by default by complexity, from less 62 | complex to more complex. 63 | 64 | Now it also support multiple namespaces. Require them all and pass a list to 65 | hunt: 66 | 67 | user=> (hunt '(your.namespace1 your.namespace2 your.namespace3)) 68 | 69 | You can also sort by repetitions using the optional parameter `:repetition` 70 | like this: 71 | 72 | user=> (hunt 'your.namespace :sort :repetition) 73 | 74 | There are filters: 75 | 76 | user=> (hunt 'your.namespace :filter {:min-repetition 2 77 | :min-complexity 5 78 | :remove-flat true}) 79 | 80 | The filters default to :min-repetition 2, :min-complexity 3 and :remove-flat false 81 | Remove flat is a filter to remove flat forms. 82 | 83 | After the header the repeated code is shown with the line number and namespace. 84 | 85 | If it doesn't find repetitions it doesn't print anything. 86 | 87 | That's it. Now go refactor your code. 88 | 89 | ## Acknowledgments 90 | 91 | Thanks to [Tom Crayford](https://github.com/tcrayford) for pointing me 92 | to his abandoned [umbrella](https://github.com/tcrayford/umbrella) and 93 | [Phil Hagelberg](https://github.com/technomancy) for helping me on #clojure. 94 | 95 | ## Bugs and Enhancements 96 | 97 | Please open issues and send pull requests. 98 | 99 | ## TODO 100 | 101 | * Add some tests. 102 | 103 | ## Changelog 104 | * v1.0.0 105 | * Add/Fix search files in directories outside src/. Now it tries to find in every classpath directory. 106 | * Fix ClassNotFoundException when file contains namespace-like symbols (thank you [tsholmes](https://github.com/tsholmes)). 107 | 108 | * v0.3.1 109 | * Fix NPE when using with clojure 1.4.0 110 | 111 | * v0.3.0 112 | * Add multiple namespaces support 113 | * Add filters :min-repetition, :min-complexity, :remove-flat 114 | * Indented code in results 115 | 116 | * v0.2.0 117 | * Add line numbers 118 | * Show original forms 119 | * Better output format 120 | * Has sort order 121 | * Improve var detection 122 | 123 | ## License 124 | 125 | Copyright © 2013-2016 Marcelo Nomoto 126 | 127 | Licensed under the EPL, the same as Clojure (see the file epl-v10.html). 128 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.10.3"}} 2 | :paths ["src"] 3 | :aliases {:dev {:extra-paths ["test"]} 4 | :dev-easy {:extra-deps {cider/cider-nrepl {:mvn/version "0.27.2"} 5 | djblue/portal {:mvn/version "0.18.0"} 6 | refactor-nrepl/refactor-nrepl {:mvn/version "3.1.0"}} 7 | :main-opts ["-e" "(do(require,'portal.api)(portal.api/tap)(portal.api/open)(set!,*print-namespace-maps*,false))" "-m" "nrepl.cmdline" "--middleware" "[cider.nrepl/cider-middleware]"] 8 | :jvm-opts ["-XX:-OmitStackTraceInFastThrow"]} 9 | :test {:extra-paths ["test"]} 10 | :test-runner {:extra-deps {io.github.cognitect-labs/test-runner {:git/tag "v0.5.0" :git/sha "b3fd0d2"}} 11 | :jvm-opts ["-XX:-OmitStackTraceInFastThrow"] 12 | :main-opts ["-m" "cognitect.test-runner"] 13 | :exec-fn cognitect.test-runner.api/test} 14 | :kaocha {:extra-deps {lambdaisland/kaocha {:mvn/version "1.60.945"}} 15 | :jvm-opts ["-XX:-OmitStackTraceInFastThrow"] 16 | :main-opts ["-m" "kaocha.runner"]}}} 17 | 18 | -------------------------------------------------------------------------------- /epl-v10.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Eclipse Public License - Version 1.0 6 | 23 | 24 | 25 | 26 | 27 | 28 |

Eclipse Public License - v 1.0

29 | 30 |

THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 31 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR 32 | DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS 33 | AGREEMENT.

34 | 35 |

1. DEFINITIONS

36 | 37 |

"Contribution" means:

38 | 39 |

a) in the case of the initial Contributor, the initial 40 | code and documentation distributed under this Agreement, and

41 |

b) in the case of each subsequent Contributor:

42 |

i) changes to the Program, and

43 |

ii) additions to the Program;

44 |

where such changes and/or additions to the Program 45 | originate from and are distributed by that particular Contributor. A 46 | Contribution 'originates' from a Contributor if it was added to the 47 | Program by such Contributor itself or anyone acting on such 48 | Contributor's behalf. Contributions do not include additions to the 49 | Program which: (i) are separate modules of software distributed in 50 | conjunction with the Program under their own license agreement, and (ii) 51 | are not derivative works of the Program.

52 | 53 |

"Contributor" means any person or entity that distributes 54 | the Program.

55 | 56 |

"Licensed Patents" mean patent claims licensable by a 57 | Contributor which are necessarily infringed by the use or sale of its 58 | Contribution alone or when combined with the Program.

59 | 60 |

"Program" means the Contributions distributed in accordance 61 | with this Agreement.

62 | 63 |

"Recipient" means anyone who receives the Program under 64 | this Agreement, including all Contributors.

65 | 66 |

2. GRANT OF RIGHTS

67 | 68 |

a) Subject to the terms of this Agreement, each 69 | Contributor hereby grants Recipient a non-exclusive, worldwide, 70 | royalty-free copyright license to reproduce, prepare derivative works 71 | of, publicly display, publicly perform, distribute and sublicense the 72 | Contribution of such Contributor, if any, and such derivative works, in 73 | source code and object code form.

74 | 75 |

b) Subject to the terms of this Agreement, each 76 | Contributor hereby grants Recipient a non-exclusive, worldwide, 77 | royalty-free patent license under Licensed Patents to make, use, sell, 78 | offer to sell, import and otherwise transfer the Contribution of such 79 | Contributor, if any, in source code and object code form. This patent 80 | license shall apply to the combination of the Contribution and the 81 | Program if, at the time the Contribution is added by the Contributor, 82 | such addition of the Contribution causes such combination to be covered 83 | by the Licensed Patents. The patent license shall not apply to any other 84 | combinations which include the Contribution. No hardware per se is 85 | licensed hereunder.

86 | 87 |

c) Recipient understands that although each Contributor 88 | grants the licenses to its Contributions set forth herein, no assurances 89 | are provided by any Contributor that the Program does not infringe the 90 | patent or other intellectual property rights of any other entity. Each 91 | Contributor disclaims any liability to Recipient for claims brought by 92 | any other entity based on infringement of intellectual property rights 93 | or otherwise. As a condition to exercising the rights and licenses 94 | granted hereunder, each Recipient hereby assumes sole responsibility to 95 | secure any other intellectual property rights needed, if any. For 96 | example, if a third party patent license is required to allow Recipient 97 | to distribute the Program, it is Recipient's responsibility to acquire 98 | that license before distributing the Program.

99 | 100 |

d) Each Contributor represents that to its knowledge it 101 | has sufficient copyright rights in its Contribution, if any, to grant 102 | the copyright license set forth in this Agreement.

103 | 104 |

3. REQUIREMENTS

105 | 106 |

A Contributor may choose to distribute the Program in object code 107 | form under its own license agreement, provided that:

108 | 109 |

a) it complies with the terms and conditions of this 110 | Agreement; and

111 | 112 |

b) its license agreement:

113 | 114 |

i) effectively disclaims on behalf of all Contributors 115 | all warranties and conditions, express and implied, including warranties 116 | or conditions of title and non-infringement, and implied warranties or 117 | conditions of merchantability and fitness for a particular purpose;

118 | 119 |

ii) effectively excludes on behalf of all Contributors 120 | all liability for damages, including direct, indirect, special, 121 | incidental and consequential damages, such as lost profits;

122 | 123 |

iii) states that any provisions which differ from this 124 | Agreement are offered by that Contributor alone and not by any other 125 | party; and

126 | 127 |

iv) states that source code for the Program is available 128 | from such Contributor, and informs licensees how to obtain it in a 129 | reasonable manner on or through a medium customarily used for software 130 | exchange.

131 | 132 |

When the Program is made available in source code form:

133 | 134 |

a) it must be made available under this Agreement; and

135 | 136 |

b) a copy of this Agreement must be included with each 137 | copy of the Program.

138 | 139 |

Contributors may not remove or alter any copyright notices contained 140 | within the Program.

141 | 142 |

Each Contributor must identify itself as the originator of its 143 | Contribution, if any, in a manner that reasonably allows subsequent 144 | Recipients to identify the originator of the Contribution.

145 | 146 |

4. COMMERCIAL DISTRIBUTION

147 | 148 |

Commercial distributors of software may accept certain 149 | responsibilities with respect to end users, business partners and the 150 | like. While this license is intended to facilitate the commercial use of 151 | the Program, the Contributor who includes the Program in a commercial 152 | product offering should do so in a manner which does not create 153 | potential liability for other Contributors. Therefore, if a Contributor 154 | includes the Program in a commercial product offering, such Contributor 155 | ("Commercial Contributor") hereby agrees to defend and 156 | indemnify every other Contributor ("Indemnified Contributor") 157 | against any losses, damages and costs (collectively "Losses") 158 | arising from claims, lawsuits and other legal actions brought by a third 159 | party against the Indemnified Contributor to the extent caused by the 160 | acts or omissions of such Commercial Contributor in connection with its 161 | distribution of the Program in a commercial product offering. The 162 | obligations in this section do not apply to any claims or Losses 163 | relating to any actual or alleged intellectual property infringement. In 164 | order to qualify, an Indemnified Contributor must: a) promptly notify 165 | the Commercial Contributor in writing of such claim, and b) allow the 166 | Commercial Contributor to control, and cooperate with the Commercial 167 | Contributor in, the defense and any related settlement negotiations. The 168 | Indemnified Contributor may participate in any such claim at its own 169 | expense.

170 | 171 |

For example, a Contributor might include the Program in a commercial 172 | product offering, Product X. That Contributor is then a Commercial 173 | Contributor. If that Commercial Contributor then makes performance 174 | claims, or offers warranties related to Product X, those performance 175 | claims and warranties are such Commercial Contributor's responsibility 176 | alone. Under this section, the Commercial Contributor would have to 177 | defend claims against the other Contributors related to those 178 | performance claims and warranties, and if a court requires any other 179 | Contributor to pay any damages as a result, the Commercial Contributor 180 | must pay those damages.

181 | 182 |

5. NO WARRANTY

183 | 184 |

EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS 185 | PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 186 | OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, 187 | ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY 188 | OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely 189 | responsible for determining the appropriateness of using and 190 | distributing the Program and assumes all risks associated with its 191 | exercise of rights under this Agreement , including but not limited to 192 | the risks and costs of program errors, compliance with applicable laws, 193 | damage to or loss of data, programs or equipment, and unavailability or 194 | interruption of operations.

195 | 196 |

6. DISCLAIMER OF LIABILITY

197 | 198 |

EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT 199 | NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, 200 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING 201 | WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF 202 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 203 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR 204 | DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED 205 | HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

206 | 207 |

7. GENERAL

208 | 209 |

If any provision of this Agreement is invalid or unenforceable under 210 | applicable law, it shall not affect the validity or enforceability of 211 | the remainder of the terms of this Agreement, and without further action 212 | by the parties hereto, such provision shall be reformed to the minimum 213 | extent necessary to make such provision valid and enforceable.

214 | 215 |

If Recipient institutes patent litigation against any entity 216 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 217 | Program itself (excluding combinations of the Program with other 218 | software or hardware) infringes such Recipient's patent(s), then such 219 | Recipient's rights granted under Section 2(b) shall terminate as of the 220 | date such litigation is filed.

221 | 222 |

All Recipient's rights under this Agreement shall terminate if it 223 | fails to comply with any of the material terms or conditions of this 224 | Agreement and does not cure such failure in a reasonable period of time 225 | after becoming aware of such noncompliance. If all Recipient's rights 226 | under this Agreement terminate, Recipient agrees to cease use and 227 | distribution of the Program as soon as reasonably practicable. However, 228 | Recipient's obligations under this Agreement and any licenses granted by 229 | Recipient relating to the Program shall continue and survive.

230 | 231 |

Everyone is permitted to copy and distribute copies of this 232 | Agreement, but in order to avoid inconsistency the Agreement is 233 | copyrighted and may only be modified in the following manner. The 234 | Agreement Steward reserves the right to publish new versions (including 235 | revisions) of this Agreement from time to time. No one other than the 236 | Agreement Steward has the right to modify this Agreement. The Eclipse 237 | Foundation is the initial Agreement Steward. The Eclipse Foundation may 238 | assign the responsibility to serve as the Agreement Steward to a 239 | suitable separate entity. Each new version of the Agreement will be 240 | given a distinguishing version number. The Program (including 241 | Contributions) may always be distributed subject to the version of the 242 | Agreement under which it was received. In addition, after a new version 243 | of the Agreement is published, Contributor may elect to distribute the 244 | Program (including its Contributions) under the new version. Except as 245 | expressly stated in Sections 2(a) and 2(b) above, Recipient receives no 246 | rights or licenses to the intellectual property of any Contributor under 247 | this Agreement, whether expressly, by implication, estoppel or 248 | otherwise. All rights in the Program not expressly granted under this 249 | Agreement are reserved.

250 | 251 |

This Agreement is governed by the laws of the State of New York and 252 | the intellectual property laws of the United States of America. No party 253 | to this Agreement will bring a legal action under this Agreement more 254 | than one year after the cause of action arose. Each party waives its 255 | rights to a jury trial in any resulting litigation.

256 | 257 | 258 | 259 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject repetition-hunter "1.0.0" 2 | :description "A tool to hunt repetitions in your code." 3 | :url "https://github.com/mynomoto/repetition-hunter" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"}) 6 | -------------------------------------------------------------------------------- /src/repetition/hunter.clj: -------------------------------------------------------------------------------- 1 | (ns repetition.hunter 2 | (:require 3 | [repetition.hunter.core :as rep])) 4 | 5 | 6 | (defn hunt 7 | "Given a namespace or seq of namespaces prints the repetitions found. 8 | You should require the namespace(s) before using hunt. Accepts sort and 9 | filter options. 10 | 11 | :sort accepts either :complexity or :repetition and it defaults to 12 | :complexity. 13 | 14 | :filter accepts a map. Possible key-vals: :min-repetition n, 15 | :min-complexity n :remove-flat true. :min-repetition defaults to 2, 16 | :min-complexity to 3, :remove-flat to false. 17 | 18 | (hunt 'your.namespace) 19 | 20 | (hunt '(your.namespace1 your.namespace2) 21 | :sort :repetition :filter {:min-complexity 5 :remove-flat true})" 22 | [nss & {:keys [sort filter] 23 | :or {sort :complexity}}] 24 | (let [nss (if (list? nss) 25 | nss 26 | (list nss))] 27 | (try 28 | (rep/check-file nss {:sort sort :filter filter}) 29 | (catch Exception e 30 | (println "Hunt failed") 31 | (println (.getMessage e)))))) 32 | -------------------------------------------------------------------------------- /src/repetition/hunter/core.clj: -------------------------------------------------------------------------------- 1 | (ns repetition.hunter.core 2 | (:require 3 | [clojure.java.io :as io] 4 | [clojure.pprint :as pprint] 5 | [clojure.string :as str] 6 | [clojure.walk :as walk]) 7 | (:import 8 | (clojure.lang 9 | LineNumberingPushbackReader) 10 | (java.io 11 | File))) 12 | 13 | 14 | (defn symbol->ns 15 | [s] 16 | (require s) 17 | (the-ns s)) 18 | 19 | 20 | (defn ns->file 21 | [ns] 22 | (-> (symbol->ns ns) 23 | ns-publics 24 | vals 25 | (nth 0) 26 | meta 27 | :file)) 28 | 29 | 30 | (defn is-file? 31 | [x] 32 | (.isFile 33 | (File. x))) 34 | 35 | 36 | (defn adjust-zipfile 37 | [z] 38 | (-> z 39 | (str/replace "zipfile:///" "file:/") 40 | (str/replace "::" "!/"))) 41 | 42 | 43 | (defn adjust-file-name 44 | [s] 45 | (cond 46 | (is-file? s) s 47 | (str/starts-with? s "zipfile:///") (adjust-zipfile s) 48 | :else (-> (.getResource (clojure.lang.RT/baseLoader) s) 49 | .getPath))) 50 | 51 | 52 | (defn read-jar 53 | [jar-file-name] 54 | (let [[jar path] (str/split jar-file-name #"!/" 2) 55 | jar (clojure.string/replace-first jar #"file:" "") 56 | jar-file (java.util.jar.JarFile. jar) 57 | ba (java.io.ByteArrayOutputStream.) 58 | is (.getInputStream jar-file (.getJarEntry jar-file path))] 59 | (clojure.java.io/copy is ba) 60 | (java.lang.String. (.toByteArray ba)))) 61 | 62 | 63 | (defn- symbol-replacements 64 | "Given a set of symbols return a map of original symbols as keys and 65 | generic replacements as values." 66 | [symbols] 67 | (into {} (map #(vector %1 (symbol (str "x_" %2))) symbols (iterate inc 0)))) 68 | 69 | 70 | (defn construct-replacement-form 71 | "Given a generic form, tag it with the original form." 72 | [nf of] 73 | (let [x (with-meta 74 | nf 75 | {:old of})] 76 | x)) 77 | 78 | 79 | (defn- make-generic-sub-form 80 | "Given a form and a map of replacements return a generic form." 81 | [f replacements] 82 | (let [variable? (set (keys replacements))] 83 | (if (variable? f) 84 | (construct-replacement-form (replacements f) f) 85 | f))) 86 | 87 | 88 | (def exclusion-words 89 | #{"if" "catch" "try" "throw" "finally" "do" "quote" "var" "recur"}) 90 | 91 | 92 | (defn find-unbound-vars 93 | "Given a form and a namespace returns a set of symbols that are not bound 94 | in the namespace." 95 | [f ns] 96 | (->> (flatten f) 97 | (filter symbol?) 98 | distinct 99 | (remove (fn [sym] 100 | (let [sym-str (name sym)] 101 | (or 102 | (and (some #(= \. %) sym-str) 103 | (not-any? #(= \/ %) sym-str)) 104 | (ns-resolve ns sym) 105 | (exclusion-words sym-str) 106 | (= \. (first sym-str)) 107 | (= \. (last sym-str)))))) 108 | (into []))) 109 | 110 | (defn make-generic 111 | "Given a namespace and a form returns a generic form" 112 | [ns f] 113 | (let [replacements (symbol-replacements (find-unbound-vars f ns)) 114 | new-form (walk/postwalk #(make-generic-sub-form % replacements) f)] 115 | (if-some [m (meta f)] 116 | (with-meta new-form m) 117 | new-form))) 118 | 119 | 120 | (defn- maybe-replace-with-old-form 121 | "Given a generic form, returns the original if it is in the tag." 122 | [f] 123 | (if-let [n (:old (meta f))] n f)) 124 | 125 | 126 | (defn- make-original 127 | "Given a generic form returns the original if possible." 128 | [generic-form] 129 | (walk/postwalk maybe-replace-with-old-form 130 | generic-form)) 131 | 132 | 133 | (defn- expression-breaker 134 | "Given an expression break return a seq of expression parts. 135 | If the part doesn't have metadata keep the original expression 136 | metadata." 137 | [exp] 138 | (let [t (tree-seq coll? identity exp) 139 | m (fn [ex] 140 | (if-not (:line (meta ex)) 141 | (try 142 | (with-meta ex (meta exp)) 143 | (catch Exception _e 144 | ex)) 145 | ex))] 146 | (map m t))) 147 | 148 | 149 | (defn- create-repetition-map 150 | "Given seqs of generic forms make one seq and find repetitions. Returns 151 | a map of original forms and measures of repetition and complexity." 152 | [exp exps] 153 | (->> (apply concat exp exps) 154 | (group-by identity) 155 | (map #(hash-map 156 | :complexity (count (flatten (first %))) 157 | :repetition (count (second %)) 158 | :original (map (juxt meta make-original) (second %)))))) 159 | 160 | 161 | (defn- find-all-generic 162 | "Given a seq of forms returns a seq of all generic subforms. It ignores 163 | forms of one element. It also ignores forms in the namespace declaration." 164 | [exp ns] 165 | (->> exp 166 | (mapcat expression-breaker) 167 | (filter #(and (coll? %) (> (count %) 1))) 168 | (map #(make-generic ns %)) 169 | (remove #(nil? (:line (meta %)))) 170 | (map #(vary-meta % assoc :ns (name ns))))) 171 | 172 | 173 | (def ^:private eof (Object.)) 174 | 175 | 176 | (defn- read-file 177 | "Generate a lazy sequence of top level forms from a 178 | LineNumberingPushbackReader." 179 | [^LineNumberingPushbackReader r] 180 | (let [do-read (fn do-read 181 | [] 182 | (lazy-seq 183 | (let [form (read r false eof)] 184 | (when-not (= form eof) 185 | (cons form (do-read))))))] 186 | (do-read))) 187 | 188 | 189 | (def ^:private default-data-reader-binding 190 | (if-let [dr (resolve '*default-data-reader-fn*)] 191 | {dr (fn [_tag val] val)} 192 | {})) 193 | 194 | 195 | (defn- read-all 196 | "Given a reader returns a seq of all forms in the reader." 197 | [readable] 198 | (with-open [reader (io/reader readable)] 199 | (with-bindings default-data-reader-binding 200 | (doall 201 | (for [f (read-file (LineNumberingPushbackReader. reader))] 202 | f))))) 203 | 204 | 205 | (defn ns->readable 206 | [ns-symbol] 207 | (let [file-name (-> ns-symbol ns->file adjust-file-name)] 208 | (if (str/starts-with? file-name "file:/") 209 | (char-array (read-jar file-name)) 210 | file-name))) 211 | 212 | 213 | (defn- sort-results 214 | "Given a sort order and a seq of results returns a sorted seq of results. 215 | If the sort order is nil, it defaults to :complexity." 216 | [sort-order r] 217 | (let [so (if (= :repetition sort-order) 218 | (juxt :repetition :complexity) 219 | (juxt :complexity :repetition))] 220 | (sort-by so r))) 221 | 222 | 223 | (defn- filter-flat 224 | "Given results filter flat forms when pred is true." 225 | [pred r] 226 | (if pred 227 | (filter #(some coll? (second (first (:original %)))) r) 228 | r)) 229 | 230 | 231 | (defn- filter-results 232 | "Given results and filter options returns the filtered results." 233 | [f r] 234 | (->> r 235 | (filter #(and (>= (:repetition %) (or (:min-repetition f) 2)) 236 | (>= (:complexity %) (or (:min-complexity f) 1)))) 237 | (filter-flat (:remove-flat f)))) 238 | 239 | 240 | (defn results 241 | "Given a seq of namespaces and a map of options returns a seq of 242 | repetitions in the corresponding file with options." 243 | [nss {:keys [sort] :as options}] 244 | (let [files (for [n nss 245 | :let [f (ns->readable n)]] 246 | (find-all-generic (read-all f) n))] 247 | (->> (create-repetition-map (first files) (rest files)) 248 | (filter-results (:filter options)) 249 | (sort-results sort)))) 250 | 251 | 252 | (defn- print-results 253 | "Given a seq of results prints the formated results" 254 | [sr] 255 | (doseq [r sr] 256 | (println (str (:repetition r) " repetitions of complexity " (:complexity r))) 257 | (newline) 258 | (doseq [o (:original r)] 259 | (println (str "Line " (:line (first o)) " - " (:ns (first o)) ":")) 260 | (pprint/with-pprint-dispatch pprint/code-dispatch 261 | (pprint/pprint (second o))) 262 | (newline)) 263 | (println "======================================================================") 264 | (newline))) 265 | 266 | 267 | (defn check-file 268 | "Given a seq of namespaces and a map of options prints repetitions in the 269 | corresponding file with options." 270 | [nss options] 271 | (print-results (results nss options))) 272 | -------------------------------------------------------------------------------- /test/repetition/hunter/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns repetition.hunter.core-test 2 | (:require 3 | [clojure.java.io :as io] 4 | [clojure.test :refer [deftest is]] 5 | [repetition.hunter.core :as rep])) 6 | 7 | 8 | (deftest symbol->ns 9 | (is (= (the-ns 'repetition.hunter.core) 10 | (rep/symbol->ns 'repetition.hunter.core))) 11 | (is (thrown-with-msg? java.io.FileNotFoundException #"Could not locate abc" 12 | (rep/symbol->ns 'abc)))) 13 | 14 | 15 | (deftest ns->file 16 | (is (re-find #"repetition/hunter/core.clj" 17 | (rep/ns->file 'repetition.hunter.core))) 18 | (is (= "clojure/string.clj" 19 | (rep/ns->file 'clojure.string))) 20 | (is (re-find #"clojure/java/io.clj" 21 | (rep/ns->file 'clojure.java.io)))) 22 | 23 | 24 | (deftest is-file? 25 | (is (= true 26 | (rep/is-file? "/home/y/code/repetition-hunter/src/repetition/hunter/core.clj"))) 27 | (is (= false 28 | (rep/is-file? "clojure/string.clj"))) 29 | (is (= false 30 | (rep/is-file? "zipfile:///home/y/.m2/repository/org/clojure/clojure/1.10.3/clojure-1.10.3.jar::clojure/java/io.clj")))) 31 | 32 | 33 | (deftest adjust-file-name 34 | (is (= "src/repetition/hunter.clj" 35 | (rep/adjust-file-name "src/repetition/hunter.clj"))) 36 | (is (= "file:/home/y/.m2/repository/org/clojure/clojure/1.10.3/clojure-1.10.3.jar!/clojure/string.clj" 37 | (rep/adjust-file-name "clojure/string.clj"))) 38 | (is (= "file:/home/y/.m2/repository/org/clojure/clojure/1.10.3/clojure-1.10.3.jar!/clojure/java/io.clj" 39 | (rep/adjust-file-name "zipfile:///home/y/.m2/repository/org/clojure/clojure/1.10.3/clojure-1.10.3.jar::clojure/java/io.clj")))) 40 | 41 | 42 | (deftest read-jar 43 | (is (string? (rep/read-jar 44 | (rep/adjust-file-name (rep/ns->file 'clojure.string))))) 45 | (is (string? (rep/read-jar 46 | (rep/adjust-file-name (rep/ns->file 'clojure.java.io)))))) 47 | 48 | 49 | (deftest readable-from-ns 50 | (is (seq (line-seq (io/reader (rep/ns->readable 'repetition.hunter.sample))))) 51 | (is (seq (line-seq (io/reader (rep/ns->readable 'clojure.string))))) 52 | (is (seq (line-seq (io/reader (rep/ns->readable 'clojure.java.io)))))) 53 | 54 | 55 | (deftest find-unbound-vars 56 | (is (= ['y] 57 | (rep/find-unbound-vars 58 | '(defn a 59 | [y] 60 | (-> y first second)) 61 | (the-ns 'repetition.hunter.sample))))) 62 | 63 | 64 | (deftest results 65 | (is (= [{:complexity 4 66 | :repetition 2 67 | :original [[{:line 6 :column 3 :ns "repetition.hunter.sample"} 68 | '(-> y first second)] 69 | [{:line 11 :column 3 :ns "repetition.hunter.sample"} 70 | '(-> x first second)]]}] 71 | (rep/results '[repetition.hunter.sample] {})))) 72 | -------------------------------------------------------------------------------- /test/repetition/hunter/sample.clj: -------------------------------------------------------------------------------- 1 | (ns repetition.hunter.sample) 2 | 3 | 4 | (defn a 5 | [y] 6 | (-> y first second)) 7 | 8 | 9 | (defn b 10 | [x] 11 | (-> x first second)) 12 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 {} 2 | --------------------------------------------------------------------------------