-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathimpl.clj
More file actions
327 lines (296 loc) · 15.8 KB
/
impl.clj
File metadata and controls
327 lines (296 loc) · 15.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
(ns clojure.java.doc.impl
(:require
[clojure.string :as str]
[clojure.java.basis :as basis]
[clojure.tools.deps :as deps])
(:import [com.vladsch.flexmark.html2md.converter FlexmarkHtmlConverter]
[org.jsoup Jsoup]
[java.util.jar JarFile]))
(set! *warn-on-reflection* true)
(defn- check-java-version [^String version-str]
(let [version (Integer/parseInt version-str)
min-version 17]
(when (< version min-version)
(throw (ex-info
(str "Java " min-version " or higher is required. Current version: " version-str)
{:current-version version-str
:minimum-version min-version})))))
(defn- find-jar-coords [jar-url-str]
(let [libs (:libs (basis/current-basis))]
(first (for [[lib-sym lib-info] libs
path (:paths lib-info)
:when (str/includes? jar-url-str path)]
{:protocol :jar
:lib lib-sym
:version (select-keys lib-info [:mvn/version])}))))
(defn- find-javadoc-coords [^Class c]
(let [class-name (.getName c)
url (.getResource c (str (.getSimpleName c) ".class"))]
(merge
{:class-name class-name}
(case (.getProtocol url)
"jar" (find-jar-coords (.toString url))
"jrt" {:protocol :jrt :lib 'java/java}
"file" nil))))
(defn- download-javadoc-jar [{:keys [lib version]}]
(let [javadoc-lib (symbol (str lib "$javadoc"))
deps-map {:deps {javadoc-lib version} :mvn/repos (:mvn/repos (deps/root-deps))}
result (deps/resolve-deps deps-map {})]
(first (:paths (get result javadoc-lib)))))
(defn- extract-html-from-jar [jar-path class-name]
(with-open [jar (JarFile. ^String jar-path)]
(if-let [entry (.getJarEntry jar (str (str/replace class-name "." "/") ".html"))]
(slurp (.getInputStream jar entry))
(throw (ex-info (str "Could not find HTML for class in javadoc jar: " class-name)
{:class-name class-name :jar-path jar-path})))))
(defn- javadoc-url [^String classname ^Class klass]
(let [java-version (System/getProperty "java.specification.version")
module-name (.getName (.getModule klass))
url-path (.replace classname \. \/)]
(check-java-version java-version)
(str "https://docs.oracle.com/en/java/javase/" java-version "/docs/api/" module-name "/" url-path ".html")))
(defn- get-javadoc-html [^String classname]
(let [classname (str/replace classname #"\$.*" "")
klass (Class/forName classname)
coords (find-javadoc-coords klass)]
(case (:protocol coords)
:jar (extract-html-from-jar (download-javadoc-jar coords) classname)
:jrt (slurp (javadoc-url classname klass))
(throw (ex-info (str "No javadoc available for local class: " classname) {:class-name classname})))))
(defn- html-to-md [^String html]
(.convert ^FlexmarkHtmlConverter (.build (FlexmarkHtmlConverter/builder)) html))
(defn- resolve-class-name [class-part]
(if-let [class-sym (resolve (symbol class-part))]
(.getName ^Class class-sym)
(throw (ex-info (str "Cannot resolve class: " class-part) {:class-name class-part}))))
(defn- strip-generics
"Strip generic type parameters: Map<K,V> -> Map"
[type-str]
(loop [result type-str]
(let [next (str/replace result #"<[^<>]*>" "")]
(if (= result next)
result
(recur next)))))
(defn- extract-params
"extract parameter types from a method signature: valueOf(char[] data) -> [char[]]"
[signature]
(let [params-str (->> signature
(drop-while #(not= % \())
rest
(take-while #(not= % \)))
(apply str))
params-no-generics (strip-generics params-str)]
(when-not (str/blank? params-no-generics)
(mapv #(first (str/split (str/trim %) #"\s+"))
(str/split params-no-generics #",")))))
(defn- extract-id-params
"Extract parameter types from section ID:
run(java.util.Map,java.util.Set) -> [java.util.Map java.util.Set]"
[id method-name]
(when (and (str/starts-with? id (str method-name "("))
(str/ends-with? id ")"))
(let [params-str (subs id (inc (count method-name)) (dec (count id)))]
(if (str/blank? params-str)
[]
(str/split params-str #",")))))
(defn- params-match-id?
"Check if extracted param types match the ID params handles both simple and fully qualified names"
[param-types id-params]
(and (= (count param-types) (count id-params))
(every? (fn [[param-type id-param]]
(or (= param-type id-param)
(str/ends-with? id-param (str "." param-type))
(str/ends-with? id-param (str "$" param-type))))
(map vector param-types id-params))))
(defn- find-method-section [^org.jsoup.nodes.Document doc method-name param-types]
(let [all-sections (.select doc "section[id]")
param-count (if param-types (count param-types) 0)]
(first
(for [^org.jsoup.nodes.Element section all-sections
:let [id (.attr section "id")
id-params (extract-id-params id method-name)]
:when (and id-params
(= param-count (count id-params))
(params-match-id? param-types id-params))]
section))))
(defn- get-method-detail [^org.jsoup.nodes.Document doc method]
(let [method-signature (:signature method)
method-name (first (str/split method-signature #"\("))
param-types (extract-params method-signature)
detail-section (find-method-section doc method-name param-types)]
(if detail-section
(let [method-html (.outerHtml ^org.jsoup.nodes.Element detail-section)]
(assoc method
:method-description-html method-html
:method-description-md (html-to-md method-html)))
method)))
(defn- get-constructor-detail [^org.jsoup.nodes.Document doc constructor]
(let [param-types (extract-params (:signature constructor))
detail-section (find-method-section doc "<init>" param-types)]
(if detail-section
(let [html (.outerHtml ^org.jsoup.nodes.Element detail-section)]
(assoc constructor
:constructor-description-html html
:constructor-description-md (html-to-md html)))
constructor)))
(defn- expand-array-syntax
"expands array syntax for matching javadoc format: String/2 -> String[][]"
[type-str]
(cond
;; Clojure array syntax: String/2 -> String[][]
(re-find #"/\d+$" type-str) (let [[base-type dims] (str/split type-str #"/")
array-suffix (apply str (repeat (Integer/parseInt dims) "[]"))]
(str base-type array-suffix))
;; varargs: CharSequence... -> CharSequence[]
(str/ends-with? type-str "...") (str/replace type-str #"[.]{3}$" "[]")
:else type-str))
(defn- params-match?
"check if param-tags match the parameters exactly by count and type, supports wildcard _"
[sig-types param-tags]
(when sig-types
(let [param-strs (mapv str param-tags)
expanded-sig-types (mapv expand-array-syntax sig-types)
expanded-param-strs (mapv expand-array-syntax param-strs)]
(and (= (count expanded-sig-types) (count expanded-param-strs))
(every? (fn [[sig-type param-str]]
(or (= param-str "_")
(= sig-type param-str)
(= (strip-generics sig-type) (strip-generics param-str))))
(map vector expanded-sig-types expanded-param-strs))))))
(defn- method-matches? [signature method-name param-tags]
(and (str/starts-with? signature method-name)
(or (nil? param-tags)
(params-match? (extract-params signature) param-tags))))
(defn- filter-methods [all-methods method-name param-tags]
(filterv #(method-matches? (:signature %) method-name param-tags) all-methods))
(defn- reflection-type-name [^Class c]
(if (.isArray c)
(str (reflection-type-name (.getComponentType c)) "[]")
(.getSimpleName c)))
(defn- reflection-params-match? [^java.lang.reflect.Method method param-tags]
(let [param-type-names (mapv reflection-type-name (.getParameterTypes method))]
(params-match? param-type-names param-tags)))
(defn- find-declaring-class [^String class-name method-name param-tags]
(let [klass (Class/forName class-name)
name-matches? (fn [^java.lang.reflect.Method m] (= method-name (.getName m)))
inherited? (fn [^java.lang.reflect.Method m] (not= class-name (.getName (.getDeclaringClass m))))
params-compatible? (fn [^java.lang.reflect.Method m] (or (nil? param-tags) (reflection-params-match? m param-tags)))
matching (->> (.getMethods klass)
(filter (fn [^java.lang.reflect.Method m]
(and (name-matches? m) (inherited? m) (params-compatible? m)))))]
(when-let [^java.lang.reflect.Method method (first matching)]
(.getName (.getDeclaringClass method)))))
(defn- compress-array-syntax
"java to clojure param-tag syntax: String[][] -> String/2"
[java-type]
(let [raw-type (strip-generics java-type)]
(cond
;; arrays: String[][] -> String/2
(str/includes? raw-type "[]") (let [base-type (str/replace raw-type #"\[\]" "")
dims (count (re-seq #"\[" raw-type))]
(str base-type "/" dims))
;; varargs: Object... -> Object/1
(str/ends-with? raw-type "...") (str/replace raw-type #"[.]{3}$" "/1")
:else raw-type)))
(defn- clojure-return-type
"Extract return type from modifier text and convert to Clojure type hint syntax: static char[] -> char/1"
[modifier-text]
(when modifier-text
(-> modifier-text
str/trim
(str/replace #"^(?:public|private|protected)\s+" "")
(str/replace #"^static\s+" "")
(str/replace #"^final\s+" "")
(str/replace #"^default\s+" "")
(str/replace #"^abstract\s+" "")
compress-array-syntax
str/trim)))
(defn- clojure-call-syntax
"javadoc signature to clojure param-tag syntax: valueOf(char[] data) -> ^[char/1] String/valueOf"
[class-part method-signature is-static?]
(let [method-name (first (str/split method-signature #"\("))
param-types (extract-params method-signature)
separator (if is-static? "/" "/.")]
(if param-types
(let [clojure-types (mapv compress-array-syntax param-types)]
(str "^[" (str/join " " clojure-types) "] " class-part separator method-name))
(str class-part separator method-name))))
(defn- clojure-constructor-call-syntax [class-part constructor-signature]
(let [param-types (extract-params constructor-signature)
prefix (when param-types (str "^[" (str/join " " (mapv compress-array-syntax param-types)) "] "))]
(str prefix class-part "/.new")))
(defn parse-javadoc
"Parse the javadoc HTML for a class or method into a data structure:
{:classname 'java.lang.String'
:class-description-html '...'
:class-description-md '...'
:methods [...]
:selected-method [{:signature 'valueOf(char[] data)'
:description 'Returns the string representation...'
:static? true
:clojure-call '^[char/1] String/valueOf'
:method-description-html '...'
:method-description-md '...'}]}"
[s param-tags]
(let [[class-part method-part] (str/split s #"/\.?" 2)
class-name (resolve-class-name class-part)
html (get-javadoc-html class-name)
doc (Jsoup/parse ^String html)
class-desc-section (.selectFirst ^org.jsoup.nodes.Document doc "section.class-description")
method-rows (.select ^org.jsoup.nodes.Document doc "div.method-summary-table.col-second")
all-methods (vec (for [^org.jsoup.nodes.Element method-div method-rows]
(let [desc-div ^org.jsoup.nodes.Element (.nextElementSibling method-div)
signature (.text (.select method-div "code"))
modifier-div ^org.jsoup.nodes.Element (.previousElementSibling method-div)
modifier-text (when modifier-div (.text modifier-div))
is-static? (and modifier-text (str/includes? modifier-text "static"))]
{:signature signature
:description (.text (.select desc-div ".block"))
:static? is-static?
:return-type (clojure-return-type modifier-text)
:clojure-call (clojure-call-syntax class-part signature is-static?)})))
constructor-rows (.select ^org.jsoup.nodes.Document doc "div.col-constructor-name")
all-constructors (vec (for [^org.jsoup.nodes.Element ctor-div constructor-rows]
(let [desc-div ^org.jsoup.nodes.Element (.nextElementSibling ctor-div)
signature (.text (.select ctor-div "code"))]
{:signature signature
:description (.text (.select desc-div ".block"))
:clojure-call (clojure-constructor-call-syntax class-part signature)})))
class-html (when class-desc-section (.outerHtml ^org.jsoup.nodes.Element class-desc-section))
result {:classname class-name
:class-description-html class-html
:class-description-md (when class-html (html-to-md class-html))
:methods all-methods
:constructors all-constructors}]
(cond
(nil? method-part) result
(= method-part "new") (let [filtered (if param-tags
(filterv #(params-match? (extract-params (:signature %)) param-tags) all-constructors)
all-constructors)]
(assoc result :selected-constructor (mapv #(get-constructor-detail doc %) filtered)))
:else (let [filtered (filter-methods all-methods method-part param-tags)
declaring-class (when (empty? filtered) (find-declaring-class class-name method-part param-tags))]
(assoc result :selected-method
(cond
(seq filtered) (mapv #(get-method-detail doc %) filtered)
declaring-class (:selected-method (parse-javadoc (str declaring-class "/." method-part) param-tags))
:else []))))))
(defn print-javadoc [{:keys [classname class-description-md selected-method selected-constructor]}]
(let [condense-lines (fn [s] (str/replace s #"\n{3,}" "\n\n"))]
(cond
selected-constructor (doseq [{:keys [constructor-description-md]} selected-constructor]
(if constructor-description-md
(println (condense-lines constructor-description-md))
(println "No javadoc description available for this constructor.")))
selected-method (doseq [{:keys [method-description-md]} selected-method]
(if method-description-md
(println (condense-lines method-description-md))
(println "No javadoc description available for this method.")))
class-description-md (println (condense-lines class-description-md))
:else (println (str "No javadoc description available for class: " classname)))))
(defn print-signatures [{:keys [classname methods constructors selected-method selected-constructor]}]
(let [items-to-print (or selected-method selected-constructor (concat constructors methods))]
(if (seq items-to-print)
(doseq [{:keys [clojure-call]} items-to-print]
(println clojure-call))
(println (str "No method signatures available for: " classname)))))