From 664acc8f817bc984b5ff240497981fe1b59c82f9 Mon Sep 17 00:00:00 2001
From: Oleksandr Yakushev <alex@bytopia.org>
Date: Fri, 25 Oct 2024 14:18:24 +0300
Subject: [PATCH 1/3] [parser] Remove orchard.java.parser

---
 CHANGELOG.md                      |   3 +-
 project.clj                       |   2 -
 src/orchard/java.clj              |  60 ++------
 src/orchard/java/parser.clj       | 248 ------------------------------
 src/orchard/java/parser_next.clj  |  47 +++---
 src/orchard/java/parser_utils.clj |  36 +++++
 6 files changed, 74 insertions(+), 322 deletions(-)
 delete mode 100644 src/orchard/java/parser.clj

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 24a6026e..3d783a13 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,8 @@
 ## master (unreleased)
 
 * [#278](https://github.com/clojure-emacs/orchard/issues/278): Java: correctly display array arguments in docs.
-* [#278](https://github.com/clojure-emacs/orchard/issues/278): Java: make parser-next work without `--add-opens`.
+* [#297](https://github.com/clojure-emacs/orchard/issues/297): Java: make parser-next work without `--add-opens`.
+* [#298](https://github.com/clojure-emacs/orchard/issues/298): Java: remove `orchard.java.parser` in favor of parser-next.
 * [#293](https://github.com/clojure-emacs/orchard/issues/293): Inspector: show HashMap size in the header.
 * [#294](https://github.com/clojure-emacs/orchard/issues/294): Inspector: count small lazy collections.
 
diff --git a/project.clj b/project.clj
index ca772231..a49926c3 100644
--- a/project.clj
+++ b/project.clj
@@ -114,8 +114,6 @@
                                                                               orchard.java.parser-next-test true}}
                                     :exclude-namespaces ~(if jdk8?
                                                            '[orchard.java.modules
-                                                             orchard.java.parser
-                                                             orchard.java.parser-test
                                                              orchard.java.parser-utils
                                                              orchard.java.parser-next
                                                              orchard.java.parser-next-test]
diff --git a/src/orchard/java.clj b/src/orchard/java.clj
index 6aaeba4e..815c1331 100644
--- a/src/orchard/java.clj
+++ b/src/orchard/java.clj
@@ -70,38 +70,23 @@
 ;;; ## Source Analysis
 ;;
 ;; Java parser support is available for JDK11+ and JDK8 via separate
-;; namespaces, `java.parser` and `java.legacy-parser`. The former uses only
+;; namespaces, `java.parser-next` and `java.legacy-parser`. The former uses only
 ;; external JDK APIs and supports modular (Jigsaw) sources. The latter uses
 ;; internal APIs out of necessity. Once this project discontinues support for
 ;; JDK8, the legacy parser may be removed.
 
-(def parser-available-exception
-  "The exception found, if any, while trying to load any of the parser namespaces."
+(def parser-exception
+  "The exception found, if any, when running any parser."
   (atom nil))
 
 (def ^:private parser-next-source-info
   (delay
     (when (>= misc/java-api-version 11)
       (try (let [f (misc/require-and-resolve 'orchard.java.parser-next/source-info)]
-             (when (f `LruMap :throw)
-               f))
-           (catch Throwable e
-             (reset! parser-available-exception e)
-             nil)))))
-
-(def parser-next-available?
-  (delay ;; avoid the side-effects at compile-time
-    (atom ;; make the result mutable - this is helpful in case the detection below wasn't sufficient
-     (some? @parser-next-source-info))))
-
-(def ^:private jdk11-parser-source-info
-  (delay
-    (when (>= misc/java-api-version 9)
-      (try (let [f (misc/require-and-resolve 'orchard.java.parser/source-info)]
              (when (f `LruMap)
                f))
            (catch Throwable e
-             (reset! parser-available-exception e)
+             (reset! parser-exception e)
              nil)))))
 
 (def ^:private legacy-parser-source-info
@@ -111,35 +96,20 @@
              (when (f `LruMap)
                f))
            (catch Throwable e
-             (reset! parser-available-exception e)
+             (reset! parser-exception e)
              nil)))))
 
-(defn source-info*
-  "When a Java parser is available, return class info from its parsed source;
-  otherwise return nil."
-  [& args]
-  (let [choose (fn []
-                 (or (and @@parser-next-available? @parser-next-source-info)
-                     @jdk11-parser-source-info
-                     @legacy-parser-source-info))]
-    (try
-      (when-let [f (choose)]
-        (apply f args))
-      (catch IllegalAccessError e
-        (if-not @@parser-next-available?
-          (throw e)
-          (do
-            ;; if there was an IllegalAccessError, the parser was mistakenly detected as available,
-            ;; so we update the detection and retry:
-            (reset! @parser-next-available? false)
-            (reset! parser-available-exception e)
-            (apply (choose) args)))))))
-
 (defn source-info
-  "Ensure that JDK sources are visible on the classpath if present, and return
-  class info from its parsed source if available."
-  [class]
-  (source-info* class))
+  "Try to return class info from its parsed source if the source is available.
+  Returns nil in case of any errors."
+  [class-symbol]
+  (try
+    (when-let [f (or @parser-next-source-info @legacy-parser-source-info)]
+      (f class-symbol))
+    (catch Throwable e
+      (reset! parser-exception e)
+      (when (= (System/getProperty "orchard.internal.test-suite-running") "true")
+        (throw e)))))
 
 ;; As of Java 11, Javadoc URLs begin with the module name.
 (defn module-name
diff --git a/src/orchard/java/parser.clj b/src/orchard/java/parser.clj
deleted file mode 100644
index 728e8002..00000000
--- a/src/orchard/java/parser.clj
+++ /dev/null
@@ -1,248 +0,0 @@
-(ns orchard.java.parser
-  "Source and docstring info for Java classes and members.
-
-  Parses `:doc`s using Markdown.
-
-  This ns is automatically discarded if `orchard.java.parser-next` can be loaded."
-  {:author "Jeff Valk"}
-  (:require
-   [clojure.java.io :as io]
-   [clojure.string :as string]
-   [orchard.java.parser-utils :refer [module-name parse-java parse-variable-element position source-path typesym]]
-   [orchard.misc :as misc])
-  (:import
-   (java.io StringReader)
-   (javax.lang.model.element Element ElementKind ExecutableElement TypeElement VariableElement)
-   (javax.swing.text.html HTML$Tag HTMLEditorKit$ParserCallback)
-   (javax.swing.text.html.parser ParserDelegator)
-   (jdk.javadoc.doclet DocletEnvironment)))
-
-;;; ## JDK Compatibility
-;;
-;; This namespace requires JDK11+.
-
-;;; ## Java Source Analysis
-;;
-;; Any metadata not available via reflection can be had from the source code; we
-;; just need to parse it. In the case of docstrings, we actually need to parse
-;; it twice -- first from Java source, and then from Javadoc HTML.
-
-;;; ## Java Parsing
-;;
-;; The Java Compiler API provides in-process access to the Javadoc compiler.
-;; Unlike the standard Java compiler which it extends, the Javadoc compiler
-;; preserves docstrings (obviously), as well as source position and argument
-;; names in its parse tree -- pieces we're after to augment reflection info.
-;;
-;; A few notes:
-;;
-;; 1. The compiler API `call` method is side-effect oriented; it returns only a
-;;    boolean indicating success. To use the result parse tree, we store this in
-;;    an atom.
-;;
-;; 2. The `result` atom must be scoped at the namespace level because a Doclet
-;;    is specified by passing a class name rather than an instance; hence, we
-;;    can't close over a local varaible in reify: `result` must be in scope when
-;;    the methods of a *new* instance of the Doclet class are called.
-;;
-;; 3. To compile an individual source that is defined as part of a module, the
-;;    compiler must be told to "patch" the module, and the source
-;;    `JavaFileobject`'s location must match the argument to the
-;;    "--patch-module" option.
-;;
-;;    It's not clear how to make the "--patch-module" option work with a source
-;;    loaded from memory or a jar file; its syntax seems file system oriented.
-;;    Moreover, if the `StandardJavaFileManager` resolves a file, the
-;;    "--patch-module" option is matched, but if the exact same file is passed
-;;    as a proxy-ed `Simplejavafileobject` with an identical URI, the
-;;    compiler's internal `Enter` class doesn't see this as matcing the
-;;    "--patch-module" option. To accommodate this, the jar file entry is
-;;    written to a temp file and passed to the compiler from disk. Design-wise,
-;;    this is admittedly imperfect, but the performance cost is low and it works.
-
-;;; ## Docstring Parsing
-;;
-;; Unlike source metadata (line, position, etc) that's available directly from
-;; the compiler parse tree, docstrings are "some assembly required." Javadoc
-;; comments use both `@tags` and HTML <tags> for semantics and formatting. The
-;; latter could be passed through intact if our presentation layer could read
-;; it, but we want a pure text representation, so we'll parse html to markdown.
-;; This way it can either be rendered or displayed as text.
-
-;; Use GFM extensions for multiline code blocks and tables.
-(def markdown
-  "Syntax map from html tag to a tuple of tag type key, start, and end chars"
-  (let [char-map {:p     ["\n\n"]     :code  ["`" "`"]
-                  :br    ["\n"]       :code* ["\n\n```\n" "```\n\n"]
-                  :em    ["*" "*"]    :table ["\n|--" "\n|--"]
-                  :str   ["**" "**"]  :thead ["" "|--\n"]
-                  :list  ["\n"]       :tr    ["\n" "|"]
-                  :li    ["- "]       :td    ["|"]
-                  :dd    [": "]       :th    ["|"]}
-        tags     {HTML$Tag/P  :p           HTML$Tag/TT    :code
-                  HTML$Tag/BR :br          HTML$Tag/CODE  :code
-                  HTML$Tag/I  :em          HTML$Tag/VAR   :code
-                  HTML$Tag/EM :em          HTML$Tag/KBD   :code
-                  HTML$Tag/B  :str         HTML$Tag/PRE   :code*
-                  HTML$Tag/STRONG :str     HTML$Tag/BLOCKQUOTE :code*
-                  HTML$Tag/UL :list        HTML$Tag/TABLE :table
-                  HTML$Tag/OL :list        HTML$Tag/TR    :tr
-                  HTML$Tag/DL :list        HTML$Tag/TD    :td
-                  HTML$Tag/LI :li          HTML$Tag/TH    :th
-                  HTML$Tag/DT :li
-                  HTML$Tag/DD :dd}]
-    (-> (reduce (fn [tags [tag k]]
-                  (assoc tags tag (cons k (char-map k))))
-                {} tags)
-        (with-meta char-map))))
-
-;; The HTML parser and DTD classes are in the `javax.swing` package, and have
-;; internal references to the `sun.awt.AppContext` class. On Mac OS X, any use
-;; of this class causes a stray GUI window to pop up. Setting the system
-;; property below prevents this. We only set the property if it
-;; hasn't already been explicitly set.
-(when (nil? (System/getProperty "apple.awt.UIElement"))
-  (System/setProperty "apple.awt.UIElement" "true"))
-
-;; We parse html and emit text in a single pass -- there's no need to build a
-;; tree. The syntax map defines most of the output format, but a few stateful
-;; rules are applied:
-;;
-;; 1. List items are indented to their nested depth.
-;; 2. Nested elements with the same tag type key are coalesced (`<pre>` inside
-;;    of `<blockquote>` is common, for instance).
-;; 3. A border row is inserted between `<th>` and `<td>` table rows. Since
-;;    `<thead>` and `<tbody>` are optional, we look for the th/td transition.
-(defn parse-html
-  "Parse html to markdown text."
-  [html]
-  (let [sb (StringBuilder.)
-        sr (StringReader. html)
-        parser (ParserDelegator.)
-        stack (atom nil)
-        flags (atom #{})
-        handler (proxy [HTMLEditorKit$ParserCallback] []
-                  (handleText [^chars chars _]
-                    (.append sb (String. chars)))
-
-                  (handleStartTag [tag _ _]
-                    (let [[k start] (markdown tag)]
-                      (when (and k (not= k (peek @stack)))
-                        (swap! stack conj k)
-
-                        ;; Indent list items at the current depth.
-                        (when (#{:li} k)
-                          (let [depth (count (filter #{:list} @stack))]
-                            (.append sb "\n")
-                            (dotimes [_ (dec depth)]
-                              (.append sb "  "))))
-
-                        ;; Keep th/td state; emit border between th and td rows.
-                        (when (#{:th} k) (swap! flags conj :th))
-                        (when (and (#{:td} k) (@flags :th))
-                          (.append sb (-> markdown meta :thead last)))
-
-                        (when start (.append sb start)))))
-
-                  (handleEndTag [tag _]
-                    (let [[k _ end] (markdown tag)]
-                      (when (and k (= k (peek @stack)))
-                        (swap! stack pop)
-                        (when (#{:table :td} k) (swap! flags disj :th))
-                        (when end (.append sb end))))))]
-
-    (.parse parser sr handler false)
-    (-> (str sb)
-        (string/replace #"\n{3,}" "\n\n") ; normalize whitespace
-        (string/replace #" +```" "```"))))
-
-(defn docstring
-  "Get parsed docstring text of `Element` e using source information in env"
-  [e ^DocletEnvironment env]
-  ;;
-  ;; NOTE This returns tags (e.g. @link) literally. To parse and resolve these,
-  ;; we probably need to use: `(-> env .getDocTrees (.getDocCommentTree e))`.
-  ;; That's an enhancement for another day.
-  {:doc (some-> env .getElementUtils (.getDocComment e) parse-html)})
-
-;;; ## Java Parse Tree Traversal
-;;
-;; From the parse tree returned by the compiler, create a nested map structure
-;; as produced by `orchard.java/reflect-info`: class members
-;; are indexed first by name, then argument types.
-
-(defprotocol Parsed
-  (parse-info* [o env]))
-
-(defn parse-info
-  [o env]
-  (merge (parse-info* o env)
-         (docstring o env)
-         (position o env)))
-
-(defn parse-executable-element [^ExecutableElement m env]
-  (let [argtypes (mapv #(-> ^VariableElement % .asType (typesym env)) (.getParameters m))]
-    {:name (if (= (.getKind m) ElementKind/CONSTRUCTOR)
-             (-> m .getEnclosingElement (typesym env)) ; class name
-             (-> m .getSimpleName str symbol))         ; method name
-     :type (-> m .getReturnType (typesym env))
-     :argtypes argtypes
-     :non-generic-argtypes (->> argtypes (mapv (comp symbol misc/normalize-subclass misc/remove-type-param str)))
-     :argnames (mapv #(-> ^VariableElement % .getSimpleName str symbol) (.getParameters m))}))
-
-(extend-protocol Parsed
-  TypeElement ;; => class, interface, enum
-  (parse-info* [c env]
-    {:class   (typesym c env)
-     :members (->> (.getEnclosedElements c)
-                   (filter #(#{ElementKind/CONSTRUCTOR
-                               ElementKind/METHOD
-                               ElementKind/FIELD
-                               ElementKind/ENUM_CONSTANT}
-                             (.getKind ^Element %)))
-                   (keep #(parse-info % env))
-                   ;; Index by name, argtypes. Args for fields are nil.
-                   (group-by :name)
-                   (reduce (fn [ret [n ms]]
-                             (assoc ret n (zipmap (map :non-generic-argtypes ms) ms)))
-                           {}))})
-
-  ExecutableElement ;; => method, constructor
-  (parse-info* [o env]
-    (parse-executable-element o env))
-
-  VariableElement ;; => field, enum constant
-  (parse-info* [o env]
-    (parse-variable-element o env)))
-
-(def lock (Object.))
-
-(defn source-info
-  "If the source for the Java class is available on the classpath, parse it
-  and return info to supplement reflection. Specifically, this includes source
-  file and position, docstring, and argument name info. Info returned has the
-  same structure as that of `orchard.java/reflect-info`."
-  [klass]
-  {:pre [(symbol? klass)]}
-  (locking lock ;; the jdk.javadoc.doclet classes aren't meant for concurrent modification/access.
-    (try
-      (when-let [path (source-path klass)]
-        (when-let [^DocletEnvironment root (parse-java path (module-name klass))]
-          (try
-            (let [path-resource (io/resource path)]
-              (assoc (some #(when (#{ElementKind/CLASS
-                                     ElementKind/INTERFACE
-                                     ElementKind/ENUM}
-                                   (.getKind ^Element %))
-                              (let [info (parse-info % root)]
-                                (when (= (:class info) klass)
-                                  info)))
-                           (.getIncludedElements root))
-                     ;; relative path on the classpath
-                     :file path
-                     ;; Legacy key. Please do not remove - we don't do breaking changes!
-                     :path (.getPath path-resource)
-                     ;; Full URL, e.g. file:.. or jar:...
-                     :resource-url path-resource))
-            (finally (.close (.getJavaFileManager root))))))
-      (catch Throwable _))))
diff --git a/src/orchard/java/parser_next.clj b/src/orchard/java/parser_next.clj
index b8df8c94..ee04f428 100644
--- a/src/orchard/java/parser_next.clj
+++ b/src/orchard/java/parser_next.clj
@@ -1,5 +1,5 @@
 (ns orchard.java.parser-next
-  "Source and docstring info for Java classes and members.
+  "Source and docstring info for Java classes and members. Requires JDK11+.
 
   Leaves `:doc` untouched.
 
@@ -305,30 +305,25 @@
   and return info to supplement reflection. Specifically, this includes source
   file and position, docstring, and argument name info. Info returned has the
   same structure as that of `orchard.java/reflect-info`."
-  [klass & [throw?]]
+  [klass]
   {:pre [(symbol? klass)]}
   (locking lock ;; the jdk.javadoc.doclet classes aren't meant for concurrent modification/access.
-    (try
-      (when-let [path (source-path klass)]
-        (when-let [^DocletEnvironment root (parse-java path (module-name klass))]
-          (try
-            (let [path-resource (io/resource path)]
-              (assoc (some #(when (#{ElementKind/CLASS
-                                     ElementKind/INTERFACE
-                                     ElementKind/ENUM}
-                                   (.getKind ^Element %))
-                              (let [info (parse-info % root)]
-                                (when (= (:class info) klass)
-                                  info)))
-                           (.getIncludedElements root))
-                     ;; relative path on the classpath
-                     :file path
-                     ;; Legacy key. Please do not remove - we don't do breaking changes!
-                     :path (.getPath path-resource)
-                     ;; Full URL, e.g. file:.. or jar:...
-                     :resource-url path-resource))
-            (finally (.close (.getJavaFileManager root))))))
-      (catch Throwable e
-        (when (or throw?
-                  (= "true" (System/getProperty "orchard.internal.test-suite-running")))
-          (throw e))))))
+    (when-let [path (source-path klass)]
+      (when-let [^DocletEnvironment root (parse-java path (module-name klass))]
+        (try
+          (let [path-resource (io/resource path)]
+            (assoc (some #(when (#{ElementKind/CLASS
+                                   ElementKind/INTERFACE
+                                   ElementKind/ENUM}
+                                 (.getKind ^Element %))
+                            (let [info (parse-info % root)]
+                              (when (= (:class info) klass)
+                                info)))
+                         (.getIncludedElements root))
+                   ;; relative path on the classpath
+                   :file path
+                   ;; Legacy key. Please do not remove - we don't do breaking changes!
+                   :path (.getPath path-resource)
+                   ;; Full URL, e.g. file:.. or jar:...
+                   :resource-url path-resource))
+          (finally (.close (.getJavaFileManager root))))))))
diff --git a/src/orchard/java/parser_utils.clj b/src/orchard/java/parser_utils.clj
index 08875ea8..64380766 100644
--- a/src/orchard/java/parser_utils.clj
+++ b/src/orchard/java/parser_utils.clj
@@ -11,6 +11,42 @@
    (javax.tools DocumentationTool DocumentationTool$DocumentationTask ToolProvider)
    (jdk.javadoc.doclet Doclet DocletEnvironment)))
 
+;;; ## Java Parsing
+;;
+;; The Java Compiler API provides in-process access to the Javadoc compiler.
+;; Unlike the standard Java compiler which it extends, the Javadoc compiler
+;; preserves docstrings (obviously), as well as source position and argument
+;; names in its parse tree -- pieces we're after to augment reflection info.
+;;
+;; A few notes:
+;;
+;; 1. The compiler API `call` method is side-effect oriented; it returns only a
+;;    boolean indicating success. To use the result parse tree, we store this in
+;;    an atom.
+;;
+;; 2. The `result` atom must be scoped at the namespace level because a Doclet
+;;    is specified by passing a class name rather than an instance; hence, we
+;;    can't close over a local varaible in reify: `result` must be in scope when
+;;    the methods of a *new* instance of the Doclet class are called.
+;;
+;; 3. To compile an individual source that is defined as part of a module, the
+;;    compiler must be told to "patch" the module, and the source
+;;    `JavaFileobject`'s location must match the argument to the
+;;    "--patch-module" option.
+;;
+;;    It's not clear how to make the "--patch-module" option work with a source
+;;    loaded from memory or a jar file; its syntax seems file system oriented.
+;;    Moreover, if the `StandardJavaFileManager` resolves a file, the
+;;    "--patch-module" option is matched, but if the exact same file is passed
+;;    as a proxy-ed `Simplejavafileobject` with an identical URI, the
+;;    compiler's internal `Enter` class doesn't see this as matcing the
+;;    "--patch-module" option. To accommodate this, the jar file entry is
+;;    written to a temp file and passed to the compiler from disk. Design-wise,
+;;    this is admittedly imperfect, but the performance cost is low and it works.
+
+;; This atom must be in top-level, not a local in `parse-java`, otherwise the
+;; reify will capture it as a closure and thus will no longer have 0-arg
+;; constructor, and the latter is required.
 (def result (atom nil))
 
 (defn parse-java

From 4cca2e1d7492893c7e2487f97b30220e7be4b7f1 Mon Sep 17 00:00:00 2001
From: Oleksandr Yakushev <alex@bytopia.org>
Date: Fri, 25 Oct 2024 14:21:59 +0300
Subject: [PATCH 2/3] [misc] Add with-lock macro

---
 src/orchard/clojuredocs.clj      | 25 ++++++++++++-------------
 src/orchard/java/parser_next.clj |  5 +++--
 src/orchard/misc.clj             | 12 +++++++++++-
 3 files changed, 26 insertions(+), 16 deletions(-)

diff --git a/src/orchard/clojuredocs.clj b/src/orchard/clojuredocs.clj
index 3428f07a..d981b5d8 100644
--- a/src/orchard/clojuredocs.clj
+++ b/src/orchard/clojuredocs.clj
@@ -6,6 +6,7 @@
    [clojure.edn :as edn]
    [clojure.java.io :as io]
    [clojure.string :as string]
+   [orchard.misc :refer [with-lock]]
    [orchard.util.os :as os])
   (:import
    (java.net URI)
@@ -13,7 +14,7 @@
    (java.util.concurrent.locks ReentrantLock)))
 
 (def cache (atom {}))
-(def ^:private ^ReentrantLock lock
+(def ^:private lock
   "Lock to prevent concurrent loading and parsing of Clojuredocs data and writing
   it into cache. This lock provides only efficiency benefits and is not
   necessary for correct behavior as accessing atom that contains immutable data
@@ -66,14 +67,13 @@
   {:added "0.5"}
   []
   ;; Prevent multiple threads from trying to load the cache simultaneously.
-  (.lock lock)
-  (try (when (empty? @cache)
-         (let [cache-file (io/file cache-file-name)]
-           (load-cache-file!
-            (if (.exists cache-file)
-              cache-file
-              (io/resource "clojuredocs/export.edn")))))
-       (finally (.unlock lock))))
+  (with-lock lock
+    (when (empty? @cache)
+      (let [cache-file (io/file cache-file-name)]
+        (load-cache-file!
+         (if (.exists cache-file)
+           cache-file
+           (io/resource "clojuredocs/export.edn")))))))
 
 (defn update-cache!
   "Load exported docs file from ClojureDocs, and store it as a cache.
@@ -84,10 +84,9 @@
    (update-cache! default-edn-file-url))
   ([export-edn-url]
    (let [cache-file (io/file cache-file-name)]
-     (.lock lock)
-     (try (write-cache-file! export-edn-url)
-          (load-cache-file! cache-file)
-          (finally (.unlock lock))))))
+     (with-lock lock
+       (write-cache-file! export-edn-url)
+       (load-cache-file! cache-file)))))
 
 (defn clean-cache!
   "Clean the cached ClojureDocs export file and the in memory cache."
diff --git a/src/orchard/java/parser_next.clj b/src/orchard/java/parser_next.clj
index ee04f428..a3796989 100644
--- a/src/orchard/java/parser_next.clj
+++ b/src/orchard/java/parser_next.clj
@@ -28,6 +28,7 @@
    (com.sun.source.doctree BlockTagTree DocCommentTree EndElementTree
                            LinkTree LiteralTree ParamTree ReturnTree
                            StartElementTree TextTree ThrowsTree)
+   (java.util.concurrent.locks ReentrantLock)
    (javax.lang.model.element Element ElementKind ExecutableElement TypeElement VariableElement)
    (javax.lang.model.type ArrayType TypeKind TypeVariable)
    (jdk.javadoc.doclet DocletEnvironment)))
@@ -298,7 +299,7 @@
   (parse-info* [o env]
     (parse-variable-element o env)))
 
-(def lock (Object.))
+(def ^:private lock (ReentrantLock.))
 
 (defn source-info
   "If the source for the Java class is available on the classpath, parse it
@@ -307,7 +308,7 @@
   same structure as that of `orchard.java/reflect-info`."
   [klass]
   {:pre [(symbol? klass)]}
-  (locking lock ;; the jdk.javadoc.doclet classes aren't meant for concurrent modification/access.
+  (misc/with-lock lock ;; the jdk.javadoc.doclet classes aren't meant for concurrent modification/access.
     (when-let [path (source-path klass)]
       (when-let [^DocletEnvironment root (parse-java path (module-name klass))]
         (try
diff --git a/src/orchard/misc.clj b/src/orchard/misc.clj
index 66af469b..c8b4ffce 100644
--- a/src/orchard/misc.clj
+++ b/src/orchard/misc.clj
@@ -4,7 +4,9 @@
   (:require
    [clojure.java.io :as io]
    [clojure.string :as string]
-   [orchard.util.io :as util.io]))
+   [orchard.util.io :as util.io])
+  (:import
+   (java.util.concurrent.locks ReentrantLock)))
 
 (defn os-windows? []
   (.startsWith (System/getProperty "os.name") "Windows"))
@@ -68,6 +70,14 @@
     (as-sym n)
     sym))
 
+(defmacro with-lock
+  "Like `clojure.core/locking`, but for java.util.concurrent.locks.Lock."
+  [lock & body]
+  `(let [^ReentrantLock l# ~lock]
+     (.lock l#)
+     (try ~@body
+          (finally (.unlock l#)))))
+
 (defn update-vals
   "Update the values of map `m` via the function `f`."
   [f m]

From bb0df2544c5997d183e8f5b3c3da94317a49305c Mon Sep 17 00:00:00 2001
From: Oleksandr Yakushev <alex@bytopia.org>
Date: Fri, 25 Oct 2024 14:27:29 +0300
Subject: [PATCH 3/3] [test] Reorganize tests

---
 test/orchard/java/parser_next_test.clj | 16 ++++----
 test/orchard/java/parser_test.clj      | 55 --------------------------
 test/orchard/java_test.clj             | 18 ++++-----
 3 files changed, 17 insertions(+), 72 deletions(-)
 delete mode 100644 test/orchard/java/parser_test.clj

diff --git a/test/orchard/java/parser_next_test.clj b/test/orchard/java/parser_next_test.clj
index b07af98a..b55fd1d3 100644
--- a/test/orchard/java/parser_next_test.clj
+++ b/test/orchard/java/parser_next_test.clj
@@ -8,19 +8,21 @@
   (:import
    (orchard.java DummyClass)))
 
-(when (and (System/getenv "CI") (>= misc/java-api-version 11))
+(def ^:private jdk11+? (>= misc/java-api-version 11))
+
+(when (and jdk11+? (System/getenv "CI"))
   (deftest sources-should-be-present-on-ci
     (is util/jdk-sources-present?)))
 
 (def source-info
-  (when (>= misc/java-api-version 9)
+  (when jdk11+?
     (misc/require-and-resolve 'orchard.java.parser-next/source-info)))
 
 (def parse-java
-  (when (>= misc/java-api-version 9)
+  (when jdk11+?
     (misc/require-and-resolve 'orchard.java.parser-utils/parse-java)))
 
-(when @@java/parser-next-available?
+(when jdk11+?
   (deftest parse-java-test
     (testing "Throws an informative exception on invalid code"
       (try
@@ -29,7 +31,7 @@
         (catch Exception e
           (is (-> e ex-data :out (string/includes? "illegal start of expression"))))))))
 
-(when @@java/parser-next-available?
+(when jdk11+?
   (deftest source-info-test
     (is (class? DummyClass))
 
@@ -92,7 +94,7 @@
         (is (re-find #"jar:file:/.*/.m2/repository/org/clojure/clojure/.*/clojure-.*-sources.jar!/clojure/lang/RT.java"
                      (str (:resource-url rt-info))))))))
 
-(when (and @@java/parser-next-available? util/jdk-sources-present?)
+(when (and jdk11+? util/jdk-sources-present?)
   (deftest doc-fragments-test
     (is (= [{:type "text", :content "Inserts the specified element at the tail of this queue if it is
 possible to do so immediately without exceeding the queue's capacity,
@@ -132,7 +134,7 @@ returning "}
           (is (not (string/includes? s "<a")))
           (is (not (string/includes? s "<a href"))))))))
 
-(when (and @@java/parser-next-available? util/jdk-sources-present?)
+(when (and jdk11+? util/jdk-sources-present?)
   (deftest smoke-test
     (let [annotations #{'java.lang.Override
                         'java.lang.Deprecated
diff --git a/test/orchard/java/parser_test.clj b/test/orchard/java/parser_test.clj
deleted file mode 100644
index 803a4a58..00000000
--- a/test/orchard/java/parser_test.clj
+++ /dev/null
@@ -1,55 +0,0 @@
-(ns orchard.java.parser-test
-  (:require
-   [clojure.test :refer [deftest is testing]]
-   [orchard.misc :as misc])
-  (:import
-   (orchard.java DummyClass)))
-
-(def source-info
-  (when (>= misc/java-api-version 9)
-    (misc/require-and-resolve 'orchard.java.parser/source-info)))
-
-(when source-info
-  (deftest source-info-test
-    (is (class? DummyClass))
-
-    (testing "file on the filesystem"
-      (is (= {:class 'orchard.java.DummyClass,
-              :members
-              '{dummyMethod
-                {[]
-                 {:name dummyMethod,
-                  :type java.lang.String,
-                  :argtypes [],
-                  :argnames [],
-                  :non-generic-argtypes []
-                  :doc "Method-level docstring. @return the string \"hello\"",
-                  :line 18,
-                  :column 5}}
-                orchard.java.DummyClass
-                {[]
-                 {:name orchard.java.DummyClass,
-                  :type void,
-                  :argtypes [],
-                  :non-generic-argtypes [],
-                  :argnames [],
-                  :doc nil,
-                  :line 12,
-                  :column 8}}},
-              :doc
-              "Class level docstring.\n\n```\n   DummyClass dc = new DummyClass();\n```\n\n@author Arne Brasseur",
-              :line 12,
-              :column 1,
-              :file "orchard/java/DummyClass.java"
-              :resource-url (.toURL (java.net.URI. (str "file:"
-                                                        (System/getProperty "user.dir")
-                                                        "/test-java/orchard/java/DummyClass.java")))}
-             (dissoc (source-info 'orchard.java.DummyClass)
-                     :path))))
-
-    (testing "java file in a jar"
-      (let [rt-info (source-info 'clojure.lang.RT)]
-        (is (= {:file "clojure/lang/RT.java"}
-               (select-keys rt-info [:file])))
-        (is (re-find #"jar:file:/.*/.m2/repository/org/clojure/clojure/.*/clojure-.*-sources.jar!/clojure/lang/RT.java"
-                     (str (:resource-url rt-info))))))))
diff --git a/test/orchard/java_test.clj b/test/orchard/java_test.clj
index 2ba7fdfc..955f0ad1 100644
--- a/test/orchard/java_test.clj
+++ b/test/orchard/java_test.clj
@@ -11,6 +11,8 @@
   (:import
    (mx.cider.orchard LruMap)))
 
+(def ^:private jdk11+? (>= misc/java-api-version 11))
+
 (javadoc/add-remote-javadoc "com.amazonaws." "http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/")
 (javadoc/add-remote-javadoc "org.apache.kafka." "https://kafka.apache.org/090/javadoc/")
 
@@ -148,7 +150,7 @@
             (is (class-info sym))))
         (testing "that doesn't exist"
           (is (nil? c3))))
-      (when @@sut/parser-next-available?
+      (when jdk11+?
         (testing "Doc fragments"
           (is (seq (:doc-fragments thread-class-info)))
           (is (seq (:doc-first-sentence-fragments thread-class-info))))))))
@@ -190,7 +192,7 @@
           (testing (-> m6 :doc pr-str)
             (is (-> m6 :doc (string/starts-with? "Called by the garbage collector on an object when garbage collection"))
                 "Contains doc that is clearly defined in Object (the superclass)")))
-        (when @@sut/parser-next-available?
+        (when jdk11+?
           (testing "Doc fragments"
             (testing "For a field"
               (is (seq (:doc-fragments m4)))
@@ -452,8 +454,7 @@
                  (or returns
                      (= n class-symbol))))))
 
-(when (and util/jdk-sources-present?
-           @@sut/parser-next-available?)
+(when (and util/jdk-sources-present? jdk11+?)
   (deftest reflect-and-source-info-match
     (testing "reflect and source info structurally match, allowing a meaningful deep-merge of both"
       (let [extract-arities (fn [info]
@@ -500,8 +501,7 @@
               (is (not-any? nil? all-argnames)
                   "The deep-merge went ok"))))))))
 
-(when (and util/jdk-sources-present?
-           @@sut/parser-next-available?)
+(when (and util/jdk-sources-present? jdk11+?)
   (deftest annotated-arglists-test
     (doseq [class-symbol (class-corpus)
             :let [info (sut/class-info* class-symbol)
@@ -528,8 +528,7 @@
             (is (not (string/includes? s "^java.util.function.Function java.util.function.Function")))
             (is (not (string/includes? s "java.lang")))))))))
 
-(when (and util/jdk-sources-present?
-           @@sut/parser-next-available?)
+(when (and util/jdk-sources-present? jdk11+?)
   (deftest array-arg-doc-test
     (testing "Regression test for #278"
       (is (= "^Path [^String first, ^String[] more]"
@@ -537,8 +536,7 @@
                      [:members 'of ['java.lang.String (symbol "java.lang.String[]")]
                       :annotated-arglists]))))))
 
-(when (and util/jdk-sources-present?
-           @@sut/parser-next-available?)
+(when (and util/jdk-sources-present? jdk11+?)
   (deftest *analyze-sources*-test
     (with-redefs [cache (LruMap. 100)]
       (binding [sut/*analyze-sources* false]