Skip to content

Commit

Permalink
Rework java :doc (#185)
Browse files Browse the repository at this point in the history
Fixes #179
  • Loading branch information
vemv authored Sep 19, 2023
1 parent c1ce927 commit b33ae18
Show file tree
Hide file tree
Showing 12 changed files with 744 additions and 187 deletions.
18 changes: 10 additions & 8 deletions .clj-kondo/config.edn
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
{:output {:progress true
:exclude-files ["analysis.cljc" "meta.cljc" "inspect_test.clj"]}
:linters {:unused-private-var {:level :warning
:exclude [orchard.query-test/a-private orchard.query-test/docd-fn]}
:consistent-alias {:aliases {clojure.string string}}
;; Enable this opt-in linter:
:unsorted-required-namespaces
{:level :warning}}}
{:output {:progress true
:exclude-files ["analysis.cljc" "meta.cljc" "inspect_test.clj"]}
:config-in-comment {:linters {:unused-value {:level :off}
:unresolved-namespace {:level :off}}}
:linters {:unused-private-var {:level :warning
:exclude [orchard.query-test/a-private orchard.query-test/docd-fn]}
:consistent-alias {:aliases {clojure.string string}}
;; Enable this opt-in linter:
:unsorted-required-namespaces
{:level :warning}}}
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## master (unreleased)

### Changes

* [#189](https://github.com/clojure-emacs/orchard/issues/179): `info` for Java: return extra `:doc`-related attributes, as reflected in the new `orchard.java.parser-next` namespace, allowing clients to render HTML and non-HTML fragments with precision.
* This namespace needs the `--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED` JVM flag to be present, otherwise a fallback will be used.
* ([enrich-classpath](https://github.com/clojure-emacs/enrich-classpath) adds that flag when suitable)

### Bugs fixed

* [#182](https://github.com/clojure-emacs/orchard/issues/182): `info` on ClojureScript: don't mistakenly prioritize special form names over var names.
Expand Down
8 changes: 5 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: test quick-test docs eastwood cljfmt kondo install deploy clean lein-repl repl .EXPORT_ALL_VARIABLES
.PHONY: test quick-test docs eastwood cljfmt kondo install deploy clean lein-repl repl lint .EXPORT_ALL_VARIABLES
.DEFAULT_GOAL := install

HOME=$(shell echo $$HOME)
Expand All @@ -14,7 +14,7 @@ LEIN_PROFILES ?= "+dev,+test,+1.11"

# The enrich-classpath version to be injected.
# Feel free to upgrade this.
ENRICH_CLASSPATH_VERSION="1.16.0"
ENRICH_CLASSPATH_VERSION="1.17.0"

resources/clojuredocs/export.edn:
curl -o $@ https://github.com/clojure-emacs/clojuredocs-export-edn/raw/master/exports/export.compact.edn
Expand All @@ -35,9 +35,11 @@ cljfmt:
.make_kondo_prep: project.clj .clj-kondo/config.edn
lein with-profile -dev,+test,+clj-kondo,+deploy clj-kondo --copy-configs --dependencies --parallel --lint '$$classpath' > $@

kondo: .make_kondo_prep
kondo: .make_kondo_prep clean
lein with-profile -dev,+test,+clj-kondo,+deploy clj-kondo

lint: kondo cljfmt eastwood

# Deployment is performed via CI by creating a git tag prefixed with "v".
# Please do not deploy locally as it skips various measures.
deploy: check-env clean
Expand Down
21 changes: 14 additions & 7 deletions project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,15 @@
"does-not-exist.jar"]
:java-source-paths ["test-java"]
;; Initialize the cache verbosely, as usual, so that possible issues can be more easily diagnosed:
:jvm-opts ["-Dorchard.initialize-cache.silent=false"
"-Dorchard.internal.test-suite-running=true"
"-Dorchard.internal.has-enriched-classpath=false"]
:jvm-opts
~(cond-> ["-Dorchard.initialize-cache.silent=false"
"-Dorchard.internal.test-suite-running=true"]
(not jdk8?) (conj "--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED"))

:source-paths ["test" "src-spec-alpha-2/src/main/clojure"]}

:enrich-classpath {:plugins [[mx.cider/enrich-classpath "1.16.0"]]
:enrich-classpath {:plugins [[mx.cider/enrich-classpath "1.17.0"]]
:middleware [cider.enrich-classpath/middleware]
:jvm-opts ["-Dorchard.internal.has-enriched-classpath=true"]
:enrich-classpath {:shorten true}}

;; Development tools
Expand All @@ -82,12 +83,18 @@
:clj-kondo {:plugins [[com.github.clj-kondo/lein-clj-kondo "2023.07.13"]]}

:eastwood {:plugins [[jonase/eastwood "1.4.0"]]
:eastwood {:exclude-namespaces ~(cond-> '[clojure.alpha.spec
:eastwood {:ignored-faults {:unused-ret-vals-in-try {orchard.java {:line 84}
orchard.java.parser-next-test true}}
:exclude-namespaces ~(cond-> '[clojure.alpha.spec
clojure.alpha.spec.gen
clojure.alpha.spec.impl
clojure.alpha.spec.test]
jdk8?
(conj 'orchard.java.parser)
(conj 'orchard.java.parser
'orchard.java.parser-test
'orchard.java.parser-utils
'orchard.java.parser-next
'orchard.java.parser-next-test)

(or (not jdk8?)
(not (-> "TEST_PROFILES"
Expand Down
163 changes: 40 additions & 123 deletions src-newer-jdks/orchard/java/parser.clj
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
(ns orchard.java.parser
"Source and docstring info for Java classes and members"
"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"}
(:refer-clojure :exclude [resolve])
(:require
[clojure.java.io :as io]
[clojure.string :as string])
[clojure.string :as string]
[orchard.java.parser-utils :refer [module-name parse-executable-element parse-java parse-variable-element position source-path typesym]])
(:import
(java.io StringReader StringWriter)
(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)
(javax.tools ToolProvider)
(jdk.javadoc.doclet Doclet DocletEnvironment)))
(jdk.javadoc.doclet DocletEnvironment)))

;;; ## JDK Compatibility
;;
Expand Down Expand Up @@ -56,40 +59,6 @@
;; 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.

(def result (atom nil))

(defn parse-java
"Load and parse the resource path, returning a `DocletEnvironment` object."
[path module]
(when-let [res (io/resource path)]
(let [tmpdir (System/getProperty "java.io.tmpdir")
tmpfile (io/file tmpdir (.getName (io/file path)))
compiler (ToolProvider/getSystemDocumentationTool)
sources (-> (.getStandardFileManager compiler nil nil nil)
(.getJavaFileObjectsFromFiles [tmpfile]))
doclet (class (reify Doclet
(init [_this _ _]
(reset! result nil))

(run [_this root]
(reset! result root)
true)

(getSupportedOptions [_this]
#{})))
out (StringWriter.) ; discard compiler messages
opts (apply conj ["--show-members" "private"
"--show-types" "private"
"--show-packages" "all"
"--show-module-contents" "all"
"-quiet"]
(when module
["--patch-module" (str module "=" tmpdir)]))]
(spit tmpfile (slurp res))
(.call (.getTask compiler out nil nil doclet opts sources))
(.delete tmpfile)
@result)))

;;; ## Docstring Parsing
;;
;; Unlike source metadata (line, position, etc) that's available directly from
Expand Down Expand Up @@ -201,31 +170,6 @@
;; as produced by `orchard.java/reflect-info`: class members
;; are indexed first by name, then argument types.

(defn typesym
"Using parse tree info, return the type's name equivalently to the `typesym`
function in `orchard.java`."
([n ^DocletEnvironment env]
(let [t (string/replace (str n) #"<.*>" "") ; drop generics
util (.getElementUtils env)]
(if-let [c (.getTypeElement util t)]
(let [pkg (str (.getPackageOf util c) ".")
cls (-> (string/replace-first t pkg "")
(string/replace "." "$"))]
(symbol (str pkg cls))) ; classes
(symbol t))))) ; primitives

(defn position
"Get line and column of `Element` e using parsed source information in env"
[e ^DocletEnvironment env]
(let [trees (.getDocTrees env)]
(when-let [path (.getPath trees e)]
(let [file (.getCompilationUnit path)
lines (.getLineMap file)
pos (.getStartPosition (.getSourcePositions trees)
file (.getLeaf path))]
{:line (.getLineNumber lines pos)
:column (.getColumnNumber lines pos)}))))

(defprotocol Parsed
(parse-info* [o env]))

Expand All @@ -236,21 +180,7 @@
(position o env)))

(extend-protocol Parsed
ExecutableElement ; => method, constructor
(parse-info* [m env]
{:name (if (= (.getKind m) ElementKind/CONSTRUCTOR)
(-> m .getEnclosingElement (typesym env)) ; class name
(-> m .getSimpleName str symbol)) ; method name
:type (-> m .getReturnType (typesym env))
:argtypes (mapv #(-> ^VariableElement % .asType (typesym env)) (.getParameters m))
:argnames (mapv #(-> ^VariableElement % .getSimpleName str symbol) (.getParameters m))})

VariableElement ; => field, enum constant
(parse-info* [f env]
{:name (-> f .getSimpleName str symbol)
:type (-> f .asType (typesym env))})

TypeElement ; => class, interface, enum
TypeElement ;; => class, interface, enum
(parse-info* [c env]
{:class (typesym c env)
:members (->> (.getEnclosedElements c)
Expand All @@ -264,31 +194,17 @@
(group-by :name)
(reduce (fn [ret [n ms]]
(assoc ret n (zipmap (map :argtypes ms) ms)))
{}))}))
{}))})

(defn- resolve
"Workaround for CLJ-1403, fixed in Clojure 1.10. Once 1.9 support is
discontinued, this function may simply be removed."
[sym]
(try (clojure.core/resolve sym)
(catch Exception _)))
ExecutableElement ;; => method, constructor
(parse-info* [o env]
(parse-executable-element o env))

(defn module-name
"Return the module name, or nil if modular"
[klass]
(some-> klass ^Class resolve .getModule .getName))
VariableElement ;; => field, enum constant
(parse-info* [o env]
(parse-variable-element o env)))

(defn source-path
"Return the relative `.java` source path for the top-level class."
[klass]
(when-let [^Class cls (resolve klass)]
(let [path (-> (.getName cls)
(string/replace #"\$.*" "")
(string/replace "." "/")
(str ".java"))]
(if-let [module (-> cls .getModule .getName)]
(str module "/" path)
path))))
(def lock (Object.))

(defn source-info
"If the source for the Java class is available on the classpath, parse it
Expand All @@ -297,24 +213,25 @@
same structure as that of `orchard.java/reflect-info`."
[klass]
{:pre [(symbol? klass)]}
(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 (->> (.getIncludedElements root)
(filter #(#{ElementKind/CLASS
ElementKind/INTERFACE
ElementKind/ENUM}
(.getKind ^Element %)))
(map #(parse-info % root))
(filter #(= klass (:class %)))
(first))
;; 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 _)))
(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 (->> (.getIncludedElements root)
(filter #(#{ElementKind/CLASS
ElementKind/INTERFACE
ElementKind/ENUM}
(.getKind ^Element %)))
(map #(parse-info % root))
(filter #(= klass (:class %)))
(first))
;; 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 _))))
Loading

0 comments on commit b33ae18

Please sign in to comment.