├── .gitignore ├── tests.edn ├── .github └── workflows │ └── test.yml ├── deps.edn ├── src └── clojure │ └── java │ └── doc │ ├── api.clj │ └── impl.clj ├── README.md ├── LICENSE └── test └── clojure └── java └── doc └── impl_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | .cpcache 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:tests [{:id :unit 3 | :test-paths ["test"] 4 | :source-paths ["src"]}] 5 | :reporter [kaocha.report/dots] 6 | :fail-fast? false} -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | 13 | steps: 14 | - name: Setup Java 15 | uses: actions/setup-java@v4 16 | with: 17 | distribution: 'temurin' 18 | java-version: '21' 19 | 20 | - name: Install CLI 21 | run: | 22 | curl -L -O https://github.com/clojure/brew-install/releases/latest/download/posix-install.sh 23 | chmod +x posix-install.sh 24 | sudo ./posix-install.sh 25 | clojure 26 | 27 | - name: Run tests 28 | run: clojure -M:test 29 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.12.3"} 3 | org.clojure/tools.deps {:mvn/version "0.26.1553"} 4 | com.vladsch.flexmark/flexmark-html2md-converter {:mvn/version "0.64.8"} 5 | org.jsoup/jsoup {:mvn/version "1.18.1"}} 6 | :aliases {:dev {:extra-paths ["test"] 7 | :extra-deps {nrepl/nrepl {:mvn/version "1.3.0"} 8 | cider/cider-nrepl {:mvn/version "0.50.2"}} 9 | :main-opts ["-m" "nrepl.cmdline" 10 | "--middleware" "[cider.nrepl/cider-middleware]" 11 | "--interactive"]} 12 | :test {:extra-paths ["test"] 13 | :extra-deps {io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" :git/sha "dfb30dd"}} 14 | :main-opts ["-m" "cognitect.test-runner"]} 15 | :test-watch {:extra-paths ["test"] 16 | :extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}} 17 | :main-opts ["-m" "kaocha.runner" "--watch"]}}} 18 | -------------------------------------------------------------------------------- /src/clojure/java/doc/api.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.java.doc.api 2 | (:require 3 | [clojure.java.doc.impl :refer [parse-javadoc print-javadoc print-signatures]])) 4 | 5 | (defn javadoc-data-fn [s param-tags] 6 | (parse-javadoc s param-tags)) 7 | 8 | (defn javadoc-fn [s param-tags] 9 | (print-javadoc (javadoc-data-fn s param-tags))) 10 | 11 | (defmacro jdoc-data 12 | "Returns a map containg javadoc data for a class or method. 13 | 14 | Examples: 15 | (jdoc-data String) ; Get class data 16 | (jdoc-data String/valueOf) ; Get data for all valueOf overloads 17 | (jdoc-data ^[char/1] String/valueOf) ; Get data for specific overload" 18 | [class-or-method] 19 | `(javadoc-data-fn ~(str class-or-method) '~(:param-tags (meta class-or-method)))) 20 | 21 | (defmacro jdoc 22 | "Print the javadoc html as markdown for a class or qualified method (with optional param-tags). 23 | 24 | Examples: 25 | (jdoc String) ; Print class description 26 | (jdoc String/valueOf) ; Print all valueOf overloads 27 | (jdoc ^[char/1] String/valueOf) ; Print specific overload" 28 | [class-or-method] 29 | `(javadoc-fn ~(str class-or-method) '~(:param-tags (meta class-or-method)))) 30 | 31 | (defn sigs-fn [s param-tags] 32 | (print-signatures (javadoc-data-fn s param-tags))) 33 | 34 | (defmacro sigs 35 | "Print method signatures in qualified method syntax with param tags. 36 | 37 | Examples: 38 | (sigs String/valueOf) ; Print all valueOf signatures" 39 | [class-or-method] 40 | `(sigs-fn ~(str class-or-method) '~(:param-tags (meta class-or-method)))) 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # java.doc 2 | 3 | A Clojure library for accessing Javadocs in your REPL 4 | 5 | ## Installation 6 | 7 | ### deps.edn 8 | 9 | ```clojure 10 | {:deps {org.clojure/java.doc {:git/url "https://github.com/clojure/java.doc" 11 | :git/tag "v0.1.2" 12 | :git/sha "fc518b1"}}} 13 | ``` 14 | 15 | ### In the REPL with add-libs 16 | 17 | For usage without modifying your project deps: 18 | 19 | ```clojure 20 | ;; This require is only necessary if not in user namespace 21 | (require '[clojure.repl.deps :refer [add-lib]]) 22 | 23 | (add-lib 'io.github.clojure/java.doc {:git/tag "v0.1.2" :git/sha "fc518b1"}) 24 | 25 | (require '[clojure.java.doc.api :refer [jdoc jdoc-data sigs]]) 26 | 27 | ;; Now you can use it 28 | (jdoc String) 29 | ``` 30 | 31 | ### From the Command Line 32 | 33 | Invoke directly from the command line, useful for piping into a .md file to display in your editor: 34 | 35 | ```bash 36 | clojure -Sdeps '{:deps {org.clojure/java.doc {:git/url "https://github.com/clojure/java.doc" :git/tag "v0.1.2" :git/sha "fc518b1"}}}' \ 37 | -M -e "(require '[clojure.java.doc.api :refer [jdoc]]) (jdoc String)" 38 | ``` 39 | 40 | ## Usage 41 | 42 | The core namespace provides three functions: 43 | 44 | ### jdoc 45 | 46 | Print Javadoc HTML as Markdown for a class or qualified method (with optional param-tags). 47 | 48 | ```clojure 49 | (require '[clojure.java.doc.api :refer [jdoc jdoc-data sigs]]) 50 | 51 | ;; Print class description 52 | (jdoc String) 53 | 54 | ;; Print all overloads of a method 55 | (jdoc String/valueOf) 56 | 57 | ;; Specify a specific overload using param-tags 58 | (jdoc ^[char/1] String/valueOf) 59 | 60 | ;; Use _ to match any type: 61 | (jdoc ^[_ int] String/.substring) 62 | ``` 63 | 64 | ### sigs 65 | 66 | Print method signatures in qualified method syntax with param tags. 67 | 68 | ```clojure 69 | (sigs String/valueOf) 70 | ;; ^[boolean] String/valueOf 71 | ;; ^[char] String/valueOf 72 | ;; ^[char/1] String/valueOf 73 | ;; ^[char/1 int int] String/valueOf 74 | ;; ^[double] String/valueOf 75 | ;; ... 76 | ``` 77 | 78 | ```clojure 79 | (sigs java.util.UUID) 80 | ;; java.util.UUID/.clockSequence 81 | ;; ^[UUID] java.util.UUID/.compareTo 82 | ;; ^[Object] java.util.UUID/.equals 83 | ;; ^[String] java.util.UUID/fromString 84 | ;; java.util.UUID/.getLeastSignificantBits 85 | ;; java.util.UUID/.getMostSignificantBits 86 | ;; ... 87 | ``` 88 | ### jdoc-data 89 | 90 | Returns all the structured data instead of printing the description. 91 | 92 | ```clojure 93 | (jdoc-data String) 94 | ;; => {:classname "java.lang.String" 95 | ;; :class-description-html "..." 96 | ;; :class-description-md "..." 97 | ;; :methods [{:signature "valueOf(int i)" 98 | ;; :description "Returns the string representation..." 99 | ;; :static? true 100 | ;; :clojure-call "^[int] String/valueOf"} 101 | ;; ...]} 102 | 103 | (jdoc-data ^[char/1] String/valueOf) 104 | ;; => {:classname "java.lang.String" 105 | ;; :class-description-html "..." 106 | ;; :class-description-md "..." 107 | ;; :methods [...] 108 | ;; :selected-method [{:signature "valueOf(char[] data)" 109 | ;; :description "Returns the string representation..." 110 | ;; :static? true 111 | ;; :clojure-call "^[char/1] String/valueOf" 112 | ;; :method-description-html "..." 113 | ;; :method-description-md "..."}]} 114 | ``` 115 | 116 | ## Requirements 117 | 118 | - Java 17+ 119 | 120 | ## Copyright and License 121 | 122 | Copyright © 2025 123 | 124 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 125 | 126 | http://www.apache.org/licenses/LICENSE-2.0 127 | 128 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | e, and distribute the 2 | Work and such Derivative Works in Source or Object form. 3 | 4 | 3.prominent notices 5 | stating that You changed the files; and 6 | 7 | (c) You must retain, in the Source form of any Derivative Works 8 | within a NOTICE text file distributed 9 | as part of the Derivative Works; within the Source form or 10 | documentation, if provided along with the Derivative Works; or, 11 | withless for any liability 12 | incurred by, or claims asserted against, such Contributor by reason 13 | of your accepting any such warranty or additional liability. 14 | 15 | END OF TERMS AND CONDITIONS 16 | 17 | APPENDIX: How to apply the Apache License to your work. 18 | 19 | To apply the Apache License to your work, attach the following 20 | boilerplate notice, with the fields enclosed by brackets "[]" 21 | replaced with your own identifying information. (Don't include 22 | the brackets!) The text should be enclosed in the appropriate 23 | comment syntax for the file format. We also recommend that a 24 | file or class name and description of purpose be included on the 25 | same "printed page" as the copyright notice for easier 26 | identification within third-party archives. 27 | 28 | Copyright [yyyy] [name of copyright owner] 29 | 30 | Licensed under the Apache License, Version 2.0 (the "License"); 31 | you may not use this file except in compliance with the License. 32 | You may obtain a copy of the License at 33 | 34 | http://www.apache.org/licenses/LICENSE-2.0 35 | 36 | Unless required by applicable law or agreed to in writing, software 37 | distributed under the License is distributed on an "AS IS" BASIS, 38 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 39 | See the License for the specific language governing permissions and 40 | limitations under the License.A 41 | PARTICULAR PURPOSE. You are solely responsible for determining the 42 | appropriateness of using or redistributing the Work and assume any 43 | risks associated with Your exercise of permissions under this License. 44 | 45 | 8. Limitation of Liability. In no event and under no legal theory, 46 | whether in tort (including negligence), contract, or otherwise, 47 | unless required by applicable law (such as deliberate and grossly 48 | negligent acts) or agreed to in writing, shall any Contributor be 49 | liable to You for damages, including any direct, indirect, special, 50 | incidental, or consequential damages of any character arising as a 51 | result of this License or out of the use or inability to use the 52 | Work (including but not limited to damages for loss of goodwill, 53 | work stoppage, computer failure or malfunction, or any and all 54 | other commercial damages or losses), even if such Contributor 55 | has been advised of the possibility of such damages. 56 | 57 | 9. Accepting Warranty or Additional Liability. While redistributing 58 | the Work or Derivative Works thereof, You may choose to offer, 59 | and charge a fee for, acceptance of support, warranty, indemnity, 60 | or other liability obligations and/or rights consistent with this 61 | License. However, in accepting such obligations, You may act only 62 | on Your own behalf and on Your sole responsibility, not on behalf 63 | of any other Contributor, and only if You agree to indemnify, 64 | defend, and hold each Contributor harmr modifications and 65 | may provide additional or different license terms and conditions 66 | for use, reproduction, or distribution of Your modifications, or 67 | for any such Derivative Works as a whole, provided Your use, 68 | reproduction, and distribution of the Work otherwise complies with 69 | the conditions stated in this License. 70 | 71 | 5. Submission of Contributions. Unless You explicitly state otherwise, 72 | any Contribution intentionally submitted for inclusion in the Work 73 | by You to the Licensor shall be under the terms and conditions of 74 | this License, without any additional terms or conditions. 75 | Notwithstanding the above, nothing herein shall supersede or modify 76 | the terms of any separate license agreement you may have executed 77 | with Licensor regarding such Contributions. 78 | 79 | 6. Trademarks. This License does not grant permission to use the trade 80 | names, trademarks, service marks, or product names of the Licensor, 81 | except as required for reasonable and customary use in describing the 82 | origin of the Work and reproducing the content of the NOTICE file. 83 | 84 | 7. Disclaimer of Warranty. Unless required by applicable law or 85 | agreed to in writing, Licensor provides the Work (and each 86 | Contributor provides its Contributions) on an "AS IS" BASIS, 87 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 88 | implied, including, without limitation, any warranties or conditions 89 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR in a display generated by the Derivative Works, if and 90 | wherever such third-party notices normally appear. The contents 91 | of the NOTICE file are for informational purposes only and 92 | do not modify the License. You may add Your own attribution 93 | notices within Derivative Works that You distribute, alongside 94 | or as an addendum to the NOTICE text from the Work, provided 95 | that such additional attribution notices cannot be construed 96 | as modifying the License. 97 | 98 | You may add Your own copyright statement to You that You distribute, all copyright, patent, trademark, and 99 | attribution notices from the Source form of the Work, 100 | excluding those notices that do not pertain to any part of 101 | the Derivative Works; and 102 | 103 | (d) If the Work includes a "NOTICE" text file as part of its 104 | distribution, then any Derivative Works that You distribute must 105 | include a readable copy of the attribution notices contained 106 | within such NOTICE file, excluding those notices that do not 107 | pertain to any part of the Derivative Works, in at least one 108 | of the following places: Grant of Patent License. Subject to the terms and conditions of 109 | this License, each Contributor hereby grants to You a perpetual, 110 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 111 | (except as stated in this section) patent license to make, have made, 112 | use, offer to sell, sell, import, and otherwise transfer the Work, 113 | where such license applies only to those patent claims licensable 114 | by such Contributor that are necessarily infringed by their 115 | Contribution(s) alone or by combination of their Contribution(s) 116 | with the Work to which such Contribution(s) was submitted. If You 117 | institute patent litigation against any entity (including a 118 | cross-claim or counterclaim in a lawsuit) alleging that the Work 119 | or a Contribution incorporated within the Work constitutes direct 120 | or contributory patent infringement, then any patent licenses 121 | granted to You under this License for that Work shall terminate 122 | as of the date such litigation is filed. 123 | 124 | 4. Redistribution. You may reproduce and distribute copies of the 125 | Work or Derivative Works thereof in any medium, with or without 126 | modifications, and in Source or Object form, provided that You 127 | meet the following conditions: 128 | 129 | (a) You must give any other recipients of the Work or 130 | Derivative Works a copy of this License; and 131 | 132 | (b) You must cause any modified files to carry n, 133 | and conversions to other media types. 134 | 135 | "Work" shall mean the work of authorship, whether in Source or 136 | Object form, made available under the License, as indicated by a 137 | copyright notice that is included in or attached to the work 138 | (an example is provided in the Appendix below). 139 | 140 | "Derivative Works" shall mean any work, whether in Source or Object 141 | form, that is based on (or derived from) the Work and for which the 142 | editorial revisions, annotations, elaborations, or other modifications 143 | represent, as a whole, an original work of authorship. For the purposes 144 | of this License, Derivative Works shall not include works that remain 145 | separable from, or merely link (or bind by name) to the interfaces of, 146 | the Work and Derivative Works thereof. 147 | 148 | "Contribution" shall mean any work of authorship, including 149 | the original version of the Work and any modifications or additions 150 | to that Work or Derivative Works thereof, that is intentionally 151 | submitted to Licensor for inclusion in the Work by the copyright owner 152 | or by an individual or Legal Entity authorized to submit on behalf of 153 | the copyright owner. For the purposes of this definition, "submitted" 154 | means any form of electronic, verbal, or written communication sent 155 | to the Licensor or its representatives, including but not limited to 156 | communication on electronic mailing lists, source code control systems, 157 | and issue tracking systems that are managed by, or on behalf of, the 158 | Licensor for the purpose of discussing and improving the Work, but 159 | excluding communication that is conspicuously marked or otherwise 160 | designated in writing by the copyright owner as "Not a Contribution." 161 | 162 | "Contributor" shall mean Licensor and any individual or Legal Entity 163 | on behalf of whom a Contribution has been received by Licensor and 164 | subsequently incorporated within the Work. 165 | 166 | 2. Grant of Copyright License. Subject to the terms and conditions of 167 | this License, each Contributor hereby grants to You a perpetual, 168 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 169 | copyright license to reproduce, prepare Derivative Works of, 170 | publicly display, publicly perform, sublicens 171 | Apache License 172 | Version 2.0, January 2004 173 | http://www.apache.org/licenses/ 174 | 175 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 176 | 177 | 1. Definitions. 178 | 179 | "License" shall mean the terms and conditions for use, reproduction, 180 | and distribution as defined by Sections 1 through 9 of this document. 181 | 182 | "Licensor" shall mean the copyright owner or entity authorized by 183 | the copyright owner that is granting the License. 184 | 185 | "Legal Entity" shall mean the union of the acting entity and all 186 | other entities that control, are controlled by, or are under common 187 | control with that entity. For the purposes of this definition, 188 | "control" means (i) the power, direct or indirect, to cause the 189 | direction or management of such entity, whether by contract or 190 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 191 | outstanding shares, or (iii) beneficial ownership of such entity. 192 | 193 | "You" (or "Your") shall mean an individual or Legal Entity 194 | exercising permissions granted by this License. 195 | 196 | "Source" form shall mean the preferred form for making modifications, 197 | including but not limited to software source code, documentation 198 | source, and configuration files. 199 | 200 | "Object" form shall mean any form resulting from mechanical 201 | transformation or translation of a Source form, including but 202 | not limited to compiled object code, generated documentatio 203 | -------------------------------------------------------------------------------- /src/clojure/java/doc/impl.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.java.doc.impl 2 | (:require 3 | [clojure.string :as str] 4 | [clojure.java.basis :as basis] 5 | [clojure.tools.deps :as deps]) 6 | (:import [com.vladsch.flexmark.html2md.converter FlexmarkHtmlConverter] 7 | [org.jsoup Jsoup] 8 | [java.util.jar JarFile])) 9 | 10 | (set! *warn-on-reflection* true) 11 | 12 | (defn- check-java-version [^String version-str] 13 | (let [version (Integer/parseInt version-str) 14 | min-version 17] 15 | (when (< version min-version) 16 | (throw (ex-info 17 | (str "Java " min-version " or higher is required. Current version: " version-str) 18 | {:current-version version-str 19 | :minimum-version min-version}))))) 20 | 21 | (defn- find-jar-coords [jar-url-str] 22 | (let [libs (:libs (basis/current-basis))] 23 | (first (for [[lib-sym lib-info] libs 24 | path (:paths lib-info) 25 | :when (str/includes? jar-url-str path)] 26 | {:protocol :jar 27 | :lib lib-sym 28 | :version (select-keys lib-info [:mvn/version])})))) 29 | 30 | (defn- find-javadoc-coords [^Class c] 31 | (let [class-name (.getName c) 32 | url (.getResource c (str (.getSimpleName c) ".class"))] 33 | (merge 34 | {:class-name class-name} 35 | (case (.getProtocol url) 36 | "jar" (find-jar-coords (.toString url)) 37 | "jrt" {:protocol :jrt :lib 'java/java} 38 | "file" nil)))) 39 | 40 | (defn- download-javadoc-jar [{:keys [lib version]}] 41 | (let [javadoc-lib (symbol (str lib "$javadoc")) 42 | deps-map {:deps {javadoc-lib version} :mvn/repos (:mvn/repos (deps/root-deps))} 43 | result (deps/resolve-deps deps-map {})] 44 | (first (:paths (get result javadoc-lib))))) 45 | 46 | (defn- extract-html-from-jar [jar-path class-name] 47 | (with-open [jar (JarFile. ^String jar-path)] 48 | (if-let [entry (.getJarEntry jar (str (str/replace class-name "." "/") ".html"))] 49 | (slurp (.getInputStream jar entry)) 50 | (throw (ex-info (str "Could not find HTML for class in javadoc jar: " class-name) 51 | {:class-name class-name :jar-path jar-path}))))) 52 | 53 | (defn- javadoc-url [^String classname ^Class klass] 54 | (let [java-version (System/getProperty "java.specification.version") 55 | module-name (.getName (.getModule klass)) 56 | url-path (.replace classname \. \/)] 57 | (check-java-version java-version) 58 | (str "https://docs.oracle.com/en/java/javase/" java-version "/docs/api/" module-name "/" url-path ".html"))) 59 | 60 | (defn- get-javadoc-html [^String classname] 61 | (let [classname (str/replace classname #"\$.*" "") 62 | klass (Class/forName classname) 63 | coords (find-javadoc-coords klass)] 64 | (case (:protocol coords) 65 | :jar (extract-html-from-jar (download-javadoc-jar coords) classname) 66 | :jrt (slurp (javadoc-url classname klass)) 67 | (throw (ex-info (str "No javadoc available for local class: " classname) {:class-name classname}))))) 68 | 69 | (defn- html-to-md [^String html] 70 | (.convert ^FlexmarkHtmlConverter (.build (FlexmarkHtmlConverter/builder)) html)) 71 | 72 | (defn- resolve-class-name [class-part] 73 | (if-let [class-sym (resolve (symbol class-part))] 74 | (.getName ^Class class-sym) 75 | (throw (ex-info (str "Cannot resolve class: " class-part) {:class-name class-part})))) 76 | 77 | (defn- strip-generics 78 | "Strip generic type parameters: Map -> Map" 79 | [type-str] 80 | (loop [result type-str] 81 | (let [next (str/replace result #"<[^<>]*>" "")] 82 | (if (= result next) 83 | result 84 | (recur next))))) 85 | 86 | (defn- extract-params 87 | "extract parameter types from a method signature: valueOf(char[] data) -> [char[]]" 88 | [signature] 89 | (let [params-str (->> signature 90 | (drop-while #(not= % \()) 91 | rest 92 | (take-while #(not= % \))) 93 | (apply str)) 94 | params-no-generics (strip-generics params-str)] 95 | (when-not (str/blank? params-no-generics) 96 | (mapv #(first (str/split (str/trim %) #"\s+")) 97 | (str/split params-no-generics #","))))) 98 | 99 | (defn- extract-id-params 100 | "Extract parameter types from section ID: 101 | run(java.util.Map,java.util.Set) -> [java.util.Map java.util.Set]" 102 | [id method-name] 103 | (when (and (str/starts-with? id (str method-name "(")) 104 | (str/ends-with? id ")")) 105 | (let [params-str (subs id (inc (count method-name)) (dec (count id)))] 106 | (if (str/blank? params-str) 107 | [] 108 | (str/split params-str #","))))) 109 | 110 | (defn- params-match-id? 111 | "Check if extracted param types match the ID params handles both simple and fully qualified names" 112 | [param-types id-params] 113 | (and (= (count param-types) (count id-params)) 114 | (every? (fn [[param-type id-param]] 115 | (or (= param-type id-param) 116 | (str/ends-with? id-param (str "." param-type)) 117 | (str/ends-with? id-param (str "$" param-type)))) 118 | (map vector param-types id-params)))) 119 | 120 | (defn- find-method-section [^org.jsoup.nodes.Document doc method-name param-types] 121 | (let [all-sections (.select doc "section[id]") 122 | param-count (if param-types (count param-types) 0)] 123 | (first 124 | (for [^org.jsoup.nodes.Element section all-sections 125 | :let [id (.attr section "id") 126 | id-params (extract-id-params id method-name)] 127 | :when (and id-params 128 | (= param-count (count id-params)) 129 | (params-match-id? param-types id-params))] 130 | section)))) 131 | 132 | (defn- get-method-detail [^org.jsoup.nodes.Document doc method] 133 | (let [method-signature (:signature method) 134 | method-name (first (str/split method-signature #"\(")) 135 | param-types (extract-params method-signature) 136 | detail-section (find-method-section doc method-name param-types)] 137 | (if detail-section 138 | (let [method-html (.outerHtml ^org.jsoup.nodes.Element detail-section)] 139 | (assoc method 140 | :method-description-html method-html 141 | :method-description-md (html-to-md method-html))) 142 | method))) 143 | 144 | (defn- expand-array-syntax 145 | "expands array syntax for matching javadoc format: String/2 -> String[][]" 146 | [type-str] 147 | (cond 148 | ;; Clojure array syntax: String/2 -> String[][] 149 | (re-find #"/\d+$" type-str) (let [[base-type dims] (str/split type-str #"/") 150 | array-suffix (apply str (repeat (Integer/parseInt dims) "[]"))] 151 | (str base-type array-suffix)) 152 | ;; varargs: CharSequence... -> CharSequence[] 153 | (str/ends-with? type-str "...") (str/replace type-str #"[.]{3}$" "[]") 154 | :else type-str)) 155 | 156 | (defn- params-match? 157 | "check if param-tags match the parameters exactly by count and type, supports wildcard _" 158 | [sig-types param-tags] 159 | (when sig-types 160 | (let [param-strs (mapv str param-tags) 161 | expanded-sig-types (mapv expand-array-syntax sig-types) 162 | expanded-param-strs (mapv expand-array-syntax param-strs)] 163 | (and (= (count expanded-sig-types) (count expanded-param-strs)) 164 | (every? (fn [[sig-type param-str]] 165 | (or (= param-str "_") 166 | (= sig-type param-str) 167 | (= (strip-generics sig-type) (strip-generics param-str)))) 168 | (map vector expanded-sig-types expanded-param-strs)))))) 169 | 170 | (defn- method-matches? [signature method-name param-tags] 171 | (and (str/starts-with? signature method-name) 172 | (or (nil? param-tags) 173 | (params-match? (extract-params signature) param-tags)))) 174 | 175 | (defn- filter-methods [all-methods method-name param-tags] 176 | (filterv #(method-matches? (:signature %) method-name param-tags) all-methods)) 177 | 178 | (defn- compress-array-syntax 179 | "java to clojure param-tag syntax: String[][] -> String/2" 180 | [java-type] 181 | (let [raw-type (strip-generics java-type)] 182 | (cond 183 | ;; arrays: String[][] -> String/2 184 | (str/includes? raw-type "[]") (let [base-type (str/replace raw-type #"\[\]" "") 185 | dims (count (re-seq #"\[" raw-type))] 186 | (str base-type "/" dims)) 187 | ;; varargs: Object... -> Object/1 188 | (str/ends-with? raw-type "...") (str/replace raw-type #"[.]{3}$" "/1") 189 | :else raw-type))) 190 | 191 | (defn- clojure-call-syntax 192 | "javadoc signature to clojure param-tag syntax: valueOf(char[] data) -> ^[char/1] String/valueOf" 193 | [class-part method-signature is-static?] 194 | (let [method-name (first (str/split method-signature #"\(")) 195 | param-types (extract-params method-signature) 196 | separator (if is-static? "/" "/.")] 197 | (if param-types 198 | (let [clojure-types (mapv compress-array-syntax param-types)] 199 | (str "^[" (str/join " " clojure-types) "] " class-part separator method-name)) 200 | (str class-part separator method-name)))) 201 | 202 | (defn parse-javadoc 203 | "Parse the javadoc HTML for a class or method into a data structure: 204 | {:classname 'java.lang.String' 205 | :class-description-html '...' 206 | :class-description-md '...' 207 | :methods [...] 208 | :selected-method [{:signature 'valueOf(char[] data)' 209 | :description 'Returns the string representation...' 210 | :static? true 211 | :clojure-call '^[char/1] String/valueOf' 212 | :method-description-html '...' 213 | :method-description-md '...'}]}" 214 | [s param-tags] 215 | (let [[class-part method-part] (str/split s #"/\.?" 2) 216 | class-name (resolve-class-name class-part) 217 | html (get-javadoc-html class-name) 218 | doc (Jsoup/parse ^String html) 219 | class-desc-section (.selectFirst ^org.jsoup.nodes.Document doc "section.class-description") 220 | method-rows (.select ^org.jsoup.nodes.Document doc "div.method-summary-table.col-second") 221 | all-methods (vec (for [^org.jsoup.nodes.Element method-div method-rows] 222 | (let [desc-div ^org.jsoup.nodes.Element (.nextElementSibling method-div) 223 | signature (.text (.select method-div "code")) 224 | modifier-div ^org.jsoup.nodes.Element (.previousElementSibling method-div) 225 | modifier-html (when modifier-div (.html modifier-div)) 226 | is-static? (and modifier-html (str/includes? modifier-html "static"))] 227 | {:signature signature 228 | :description (.text (.select desc-div ".block")) 229 | :static? is-static? 230 | :clojure-call (clojure-call-syntax class-part signature is-static?)}))) 231 | class-html (when class-desc-section (.outerHtml ^org.jsoup.nodes.Element class-desc-section)) 232 | result {:classname class-name 233 | :class-description-html class-html 234 | :class-description-md (when class-html (html-to-md class-html)) 235 | :methods all-methods}] 236 | (if method-part 237 | (let [filtered (filter-methods all-methods method-part param-tags)] 238 | (assoc result :selected-method 239 | (mapv #(get-method-detail doc %) filtered))) 240 | result))) 241 | 242 | (defn print-javadoc [{:keys [classname class-description-md selected-method]}] 243 | (let [condense-lines (fn [s] (str/replace s #"\n{3,}" "\n\n"))] 244 | (cond 245 | selected-method (doseq [{:keys [method-description-md]} selected-method] 246 | (if method-description-md 247 | (println (condense-lines method-description-md)) 248 | (println "No javadoc description available for this method."))) 249 | class-description-md (println (condense-lines class-description-md)) 250 | :else (println (str "No javadoc description available for class: " classname))))) 251 | 252 | (defn print-signatures [{:keys [classname methods selected-method]}] 253 | (let [methods-to-print (or selected-method methods)] 254 | (if (seq methods-to-print) 255 | (doseq [{:keys [clojure-call]} methods-to-print] 256 | (println clojure-call)) 257 | (println (str "No method signatures available for: " classname))))) 258 | -------------------------------------------------------------------------------- /test/clojure/java/doc/impl_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure.java.doc.impl-test 2 | (:require 3 | [clojure.test :refer [deftest testing is]] 4 | [clojure.java.doc.impl :as sut]) 5 | (:import [org.jsoup Jsoup])) 6 | 7 | (deftest get-method-detail-test 8 | (testing "Getting method detail from HTML" 9 | (let [html " 10 |
11 |

valueOf

12 |
13 | public static String valueOf(int i) 14 |
15 |
Returns the string representation of the int argument.
16 |
17 |
18 |

valueOf

19 |
20 | public static String valueOf(char[] data) 21 |
22 |
Returns the string representation of the char array argument.
23 |
24 | " 25 | doc (Jsoup/parse html)] 26 | 27 | (testing "Finds correct method by exact signature match" 28 | (let [method {:signature "valueOf(int i)" :description "test"} 29 | actual (#'sut/get-method-detail doc method) 30 | expected-html "
31 |

valueOf

32 |
33 | public static String valueOf(int i) 34 |
35 |
36 | Returns the string representation of the int argument. 37 |
38 |
"] 39 | (is (= "valueOf(int i)" (:signature actual))) 40 | (is (= "test" (:description actual))) 41 | (is (= expected-html (:method-description-html actual))))) 42 | 43 | (testing "Finds correct overload with array parameter" 44 | (let [method {:signature "valueOf(char[] data)" :description "test"} 45 | actual (#'sut/get-method-detail doc method) 46 | expected-html "
47 |

valueOf

48 |
49 | public static String valueOf(char[] data) 50 |
51 |
52 | Returns the string representation of the char array argument. 53 |
54 |
"] 55 | (is (= "valueOf(char[] data)" (:signature actual))) 56 | (is (= "test" (:description actual))) 57 | (is (= expected-html (:method-description-html actual))))) 58 | 59 | (testing "Returns original method when detail not found" 60 | (let [method {:signature "nonExistent(int x)" :description "test"} 61 | actual (#'sut/get-method-detail doc method)] 62 | (is (= method actual))))))) 63 | 64 | (deftest resolve-class-name-test 65 | (testing "java.lang class resolves without import" 66 | (is (= "java.lang.Integer" (#'sut/resolve-class-name "Integer"))) 67 | (is (= "java.lang.Object" (#'sut/resolve-class-name "Object")))) 68 | 69 | (testing "unimported class throws exception" 70 | (is (thrown-with-msg? clojure.lang.ExceptionInfo 71 | #"Cannot resolve class: HashMap" 72 | (#'sut/resolve-class-name "HashMap")))) 73 | 74 | (testing "fully qualified class resolves" 75 | (is (= "java.util.HashMap" (#'sut/resolve-class-name "java.util.HashMap")))) 76 | 77 | (testing "unresolveable throws exception" 78 | (is (thrown-with-msg? clojure.lang.ExceptionInfo 79 | #"Cannot resolve class: com.example.MyClass" 80 | (#'sut/resolve-class-name "com.example.MyClass"))))) 81 | 82 | (deftest extract-params-test 83 | 84 | (testing "single parameter" 85 | (is (= ["int"] (#'sut/extract-params "valueOf(int i)")))) 86 | 87 | (testing "array parameter" 88 | (is (= ["char[]"] (#'sut/extract-params "valueOf(char[] data)")))) 89 | 90 | (testing "no parameters" 91 | (is (nil? (#'sut/extract-params "length()")))) 92 | 93 | (testing "type with generics stripped" 94 | (is (= ["List"] (#'sut/extract-params "addAll(List items)")))) 95 | 96 | (testing "generics with multiple type parameters" 97 | (is (= ["java.util.Map"] (#'sut/extract-params "run(java.util.Map inputs)")))) 98 | 99 | (testing "multiple params with generics" 100 | (is (= ["String" "Map" "int"] (#'sut/extract-params "process(String name, Map data, int count)")))) 101 | 102 | (testing "nested generics" 103 | (is (= ["Map"] (#'sut/extract-params "transform(Map> data)")))) 104 | 105 | (testing "multiple params with nested generics" 106 | (is (= ["java.util.Map" "java.util.Map"] 107 | (#'sut/extract-params "run(java.util.Map inputs, java.util.Map pinnedOutputs)")))) 108 | 109 | (testing "array types" 110 | (is (= ["int[]"] (#'sut/extract-params "sort(int[] array)")))) 111 | 112 | (testing "varargs" 113 | (is (= ["String" "Object..."] (#'sut/extract-params "format(String format, Object... args)"))))) 114 | 115 | (deftest expand-array-syntax-test 116 | 117 | (testing "array syntax" 118 | (is (= "String[]" (#'sut/expand-array-syntax "String/1"))) 119 | (is (= "String[][]" (#'sut/expand-array-syntax "String/2"))) 120 | (is (= "CharSequence[]" (#'sut/expand-array-syntax "CharSequence/1"))) 121 | (is (= "int[][][]" (#'sut/expand-array-syntax "int/3")))) 122 | 123 | (testing "varargs" 124 | (is (= "CharSequence[]" (#'sut/expand-array-syntax "CharSequence..."))) 125 | (is (= "Object[]" (#'sut/expand-array-syntax "Object...")))) 126 | 127 | (testing "no transformation needed" 128 | (is (= "int" (#'sut/expand-array-syntax "int"))) 129 | (is (= "String" (#'sut/expand-array-syntax "String"))) 130 | (is (= "int[]" (#'sut/expand-array-syntax "int[]"))) 131 | (is (= "String[][]" (#'sut/expand-array-syntax "String[][]"))))) 132 | 133 | (deftest compress-array-syntax-test 134 | 135 | (testing "primitive types unchanged" 136 | (is (= "int" (#'sut/compress-array-syntax "int"))) 137 | (is (= "String" (#'sut/compress-array-syntax "String")))) 138 | 139 | (testing "single dimension arrays" 140 | (is (= "char/1" (#'sut/compress-array-syntax "char[]"))) 141 | (is (= "String/1" (#'sut/compress-array-syntax "String[]"))) 142 | (is (= "int/1" (#'sut/compress-array-syntax "int[]")))) 143 | 144 | (testing "multi-dimension arrays" 145 | (is (= "String/2" (#'sut/compress-array-syntax "String[][]"))) 146 | (is (= "int/3" (#'sut/compress-array-syntax "int[][][]")))) 147 | 148 | (testing "varargs" 149 | (is (= "Object/1" (#'sut/compress-array-syntax "Object..."))) 150 | (is (= "String/1" (#'sut/compress-array-syntax "String..."))))) 151 | 152 | (deftest clojure-call-syntax-test 153 | 154 | (testing "instance method with single parameter" 155 | (is (= "^[CharSequence] String/.contains" 156 | (#'sut/clojure-call-syntax "String" "contains(CharSequence s)" false)))) 157 | 158 | (testing "instance method with multiple parameters" 159 | (is (= "^[int int] String/.substring" 160 | (#'sut/clojure-call-syntax "String" "substring(int beginIndex, int endIndex)" false)))) 161 | 162 | (testing "instance method with no parameters" 163 | (is (= "String/.length" 164 | (#'sut/clojure-call-syntax "String" "length()" false)))) 165 | 166 | (testing "static method with array parameters" 167 | (is (= "^[char/1] String/valueOf" 168 | (#'sut/clojure-call-syntax "String" "valueOf(char[] data)" true)))) 169 | 170 | (testing "static method with varargs" 171 | (is (= "^[String Object/1] String/format" 172 | (#'sut/clojure-call-syntax "String" "format(String format, Object... args)" true)))) 173 | 174 | (testing "instance method with arrays" 175 | (is (= "^[String/2] SomeClass/.someMethod" 176 | (#'sut/clojure-call-syntax "SomeClass" "someMethod(String[][] data)" false)))) 177 | 178 | (testing "static method with no parameters" 179 | (is (= "SomeClass/staticMethod" 180 | (#'sut/clojure-call-syntax "SomeClass" "staticMethod()" true))))) 181 | 182 | (deftest method-matches-test 183 | 184 | (testing "method name matching" 185 | (is (true? (#'sut/method-matches? "valueOf(int i)" "valueOf" nil))) 186 | (is (true? (#'sut/method-matches? "valueOf(char[] data)" "valueOf" nil))) 187 | (is (false? (#'sut/method-matches? "charAt(int index)" "valueOf" nil)))) 188 | 189 | (testing "method name with param-tags" 190 | (is (true? (#'sut/method-matches? "valueOf(int i)" "valueOf" '[int]))) 191 | (is (true? (#'sut/method-matches? "valueOf(char[] data)" "valueOf" '[char/1]))) 192 | (is (false? (#'sut/method-matches? "valueOf(int i)" "valueOf" '[char/1]))) 193 | (is (false? (#'sut/method-matches? "valueOf(char[] data)" "valueOf" '[int])))) 194 | 195 | (testing "method name matches but params do not" 196 | (is (false? (#'sut/method-matches? "valueOf(int i)" "valueOf" '[String]))) 197 | (is (false? (#'sut/method-matches? "contains(CharSequence s)" "contains" '[String])))) 198 | 199 | (testing "array syntax" 200 | (is (true? (#'sut/method-matches? "valueOf(char[] data)" "valueOf" '[char/1]))) 201 | (is (true? (#'sut/method-matches? "copyValueOf(char[] data, int offset, int count)" "copyValueOf" '[char/1 int int]))) 202 | (is (true? (#'sut/method-matches? "format(String format, Object... args)" "format" '[String Object/1]))) 203 | (is (false? (#'sut/method-matches? "format(String format, Object... args)" "format" '[String Object/2])))) 204 | 205 | (testing "with wildcards" 206 | (is (true? (#'sut/method-matches? "valueOf(int i)" "valueOf" '[_]))) 207 | (is (true? (#'sut/method-matches? "copyValueOf(char[] data, int offset, int count)" "copyValueOf" '[_ int int]))) 208 | (is (true? (#'sut/method-matches? "copyValueOf(char[] data, int offset, int count)" "copyValueOf" '[char/1 _ int]))))) 209 | 210 | (deftest params-match-test 211 | 212 | (testing "single parameter" 213 | (is (true? (#'sut/params-match? ["int"] '[int])))) 214 | 215 | (testing "multiple parameters" 216 | (is (true? (#'sut/params-match? ["int" "int"] '[int int])))) 217 | 218 | (testing "wrong count too few param-tags" 219 | (is (false? (#'sut/params-match? ["int" "int"] '[int])))) 220 | 221 | (testing "wrong count too many param-tags" 222 | (is (false? (#'sut/params-match? ["int"] '[int int])))) 223 | 224 | (testing "correct number, but wrong type" 225 | (is (false? (#'sut/params-match? ["String"] '[int])))) 226 | 227 | (testing "one of many is wrong" 228 | (is (false? (#'sut/params-match? ["int" "String"] '[int int])))) 229 | 230 | (testing "no args in signature" 231 | (is (nil? (#'sut/params-match? nil '[int])))) 232 | 233 | (testing "single dimension" 234 | (is (true? (#'sut/params-match? ["int[]"] '[int/1]))) 235 | (is (true? (#'sut/params-match? ["CharSequence[]"] '[CharSequence/1])))) 236 | 237 | (testing "multiple dimensions" 238 | (is (true? (#'sut/params-match? ["String[][]"] '[String/2])))) 239 | 240 | (testing "varargs matches array syntax" 241 | (is (true? (#'sut/params-match? ["CharSequence..."] '[CharSequence/1])))) 242 | 243 | (testing "wildcards" 244 | (is (true? (#'sut/params-match? ["int"] '[_]))) 245 | (is (true? (#'sut/params-match? ["String"] '[_]))) 246 | (is (true? (#'sut/params-match? ["int" "String"] '[_ _]))) 247 | (is (true? (#'sut/params-match? ["int" "String"] '[_ String]))) 248 | (is (true? (#'sut/params-match? ["int" "String"] '[int _]))) 249 | (is (false? (#'sut/params-match? ["int" "String"] '[_]))) 250 | (is (true? (#'sut/params-match? ["CharSequence..." "CharSequence..."] '[_ CharSequence/1]))))) 251 | 252 | (deftest extract-id-params-test 253 | 254 | (testing "no parameters" 255 | (is (= [] (#'sut/extract-id-params "valueOf()" "valueOf")))) 256 | 257 | (testing "single parameter" 258 | (is (= ["int"] (#'sut/extract-id-params "valueOf(int)" "valueOf")))) 259 | 260 | (testing "single fully qualified parameter" 261 | (is (= ["java.lang.String"] (#'sut/extract-id-params "valueOf(java.lang.String)" "valueOf")))) 262 | 263 | (testing "multiple simple parameters" 264 | (is (= ["int" "int"] (#'sut/extract-id-params "substring(int,int)" "substring")))) 265 | 266 | (testing "multiple fully qualified parameters" 267 | (is (= ["java.util.Map" "java.util.Set"] (#'sut/extract-id-params "run(java.util.Map,java.util.Set)" "run")))) 268 | 269 | (testing "array parameters" 270 | (is (= ["char[]"] (#'sut/extract-id-params "valueOf(char[])" "valueOf")))) 271 | 272 | (testing "mixed simple and qualified parameters" 273 | (is (= ["java.util.Map" "int"] (#'sut/extract-id-params "process(java.util.Map,int)" "process")))) 274 | 275 | (testing "inner class parameters with dot separator" 276 | (is (= ["ai.onnxruntime.OrtSession.RunOptions"] 277 | (#'sut/extract-id-params "run(ai.onnxruntime.OrtSession.RunOptions)" "run")))) 278 | 279 | (testing "non-matching method name returns nil" 280 | (is (nil? (#'sut/extract-id-params "valueOf(int)" "substring"))))) 281 | 282 | (deftest params-match-id-test 283 | 284 | (testing "exact match with simple types" 285 | (is (true? (#'sut/params-match-id? ["int"] ["int"]))) 286 | (is (true? (#'sut/params-match-id? ["int" "String"] ["int" "String"])))) 287 | 288 | (testing "simple type matches fully qualified type" 289 | (is (true? (#'sut/params-match-id? ["String"] ["java.lang.String"]))) 290 | (is (true? (#'sut/params-match-id? ["Map"] ["java.util.Map"]))) 291 | (is (true? (#'sut/params-match-id? ["Set"] ["java.util.Set"])))) 292 | 293 | (testing "multiple params with mixed simple and qualified" 294 | (is (true? (#'sut/params-match-id? ["Map" "Set"] ["java.util.Map" "java.util.Set"]))) 295 | (is (true? (#'sut/params-match-id? ["String" "int"] ["java.lang.String" "int"])))) 296 | 297 | (testing "array types match" 298 | (is (true? (#'sut/params-match-id? ["char[]"] ["char[]"]))) 299 | (is (true? (#'sut/params-match-id? ["String[]"] ["java.lang.String[]"])))) 300 | 301 | (testing "inner class with dot separator" 302 | (is (true? (#'sut/params-match-id? ["OrtSession.RunOptions"] ["ai.onnxruntime.OrtSession.RunOptions"])))) 303 | 304 | (testing "simple inner class name matches fully qualified" 305 | (is (true? (#'sut/params-match-id? ["RunOptions"] ["ai.onnxruntime.OrtSession.RunOptions"])))) 306 | 307 | (testing "wrong type does not match" 308 | (is (false? (#'sut/params-match-id? ["int"] ["String"]))) 309 | (is (false? (#'sut/params-match-id? ["Map"] ["java.util.Set"])))) 310 | 311 | (testing "wrong count does not match" 312 | (is (false? (#'sut/params-match-id? ["int"] ["int" "String"]))) 313 | (is (false? (#'sut/params-match-id? ["int" "String"] ["int"])))) 314 | 315 | (testing "empty params match" 316 | (is (true? (#'sut/params-match-id? [] []))))) 317 | 318 | (deftest find-method-section-test 319 | 320 | (testing "finds method with simple type parameters" 321 | (let [html " 322 |
323 |

valueOf

324 |
325 |
326 |

valueOf

327 |
328 | " 329 | doc (Jsoup/parse html) 330 | section-int (#'sut/find-method-section doc "valueOf" ["int"]) 331 | section-char-array (#'sut/find-method-section doc "valueOf" ["char[]"])] 332 | (is (= "valueOf(int)" (.attr section-int "id"))) 333 | (is (= "valueOf(char[])" (.attr section-char-array "id"))))) 334 | 335 | (testing "finds method with fully qualified type parameters" 336 | (let [html " 337 |
338 |

run

339 |
340 |
341 |

run

342 |
343 | " 344 | doc (Jsoup/parse html) 345 | section-map (#'sut/find-method-section doc "run" ["Map"]) 346 | section-map-set (#'sut/find-method-section doc "run" ["Map" "Set"])] 347 | (is (= "run(java.util.Map)" (.attr section-map "id"))) 348 | (is (= "run(java.util.Map,java.util.Set)" (.attr section-map-set "id"))))) 349 | 350 | (testing "finds method with no parameters, nil from extract-params" 351 | (let [html " 352 |
353 |

length

354 |
355 | " 356 | doc (Jsoup/parse html) 357 | section (#'sut/find-method-section doc "length" nil)] 358 | (is (= "length()" (.attr section "id"))))) 359 | 360 | (testing "finds method with no parameters, empty vector" 361 | (let [html " 362 |
363 |

toString

364 |
365 | " 366 | doc (Jsoup/parse html) 367 | section (#'sut/find-method-section doc "toString" [])] 368 | (is (= "toString()" (.attr section "id"))))) 369 | 370 | (testing "returns nil when method not found" 371 | (let [html " 372 |
373 |

valueOf

374 |
375 | " 376 | doc (Jsoup/parse html)] 377 | (is (nil? (#'sut/find-method-section doc "valueOf" ["String"]))) 378 | (is (nil? (#'sut/find-method-section doc "nonExistent" ["int"]))))) 379 | 380 | (testing "distinguishes between overloads with different param counts" 381 | (let [html " 382 |
383 |

run

384 |
385 |
386 |

run

387 |
388 |
389 |

run

390 |
391 | " 392 | doc (Jsoup/parse html) 393 | section-1 (#'sut/find-method-section doc "run" ["Map"]) 394 | section-2 (#'sut/find-method-section doc "run" ["Map" "Set"]) 395 | section-3 (#'sut/find-method-section doc "run" ["Map" "Set" "OrtSession.RunOptions"])] 396 | (is (= "run(java.util.Map)" (.attr section-1 "id"))) 397 | (is (= "run(java.util.Map,java.util.Set)" (.attr section-2 "id"))) 398 | (is (= "run(java.util.Map,java.util.Set,ai.onnxruntime.OrtSession.RunOptions)" (.attr section-3 "id")))))) 399 | --------------------------------------------------------------------------------