From 80fba7af5b3b1a9bd003edfa902afb05f744a491 Mon Sep 17 00:00:00 2001 From: Pete Bentley Date: Sun, 8 Sep 2024 22:36:09 +0100 Subject: [PATCH 1/4] Add a filter doclet which works with the Java 11 doclet API. MVP with some KIs but I ran out of weekend but allows us to bump the compiler version everywhere. Pros: * Kotlin * Has potential to be project independent without being as heavyweight as doclava * Nifty HTML DSL :) KI: * HTML DSL incomplete * Nested class paths are wrong * No links on types in signatures --- api-doclet/build.gradle | 25 +- .../org/conscrypt/doclet/FilterDoclet.java | 151 ---------- .../kotlin/org/conscrypt/doclet/ClassIndex.kt | 76 +++++ .../kotlin/org/conscrypt/doclet/ClassInfo.kt | 134 +++++++++ .../org/conscrypt/doclet/DocTreeUtils.kt | 117 ++++++++ .../org/conscrypt/doclet/ElementUtils.kt | 119 ++++++++ .../org/conscrypt/doclet/FilterDoclet.kt | 154 ++++++++++ .../org/conscrypt/doclet/HtmlBuilder.kt | 284 ++++++++++++++++++ .../kotlin/org/conscrypt/doclet/Options.kt | 53 ++++ api-doclet/src/main/resources/styles.css | 147 +++++++++ build.gradle | 21 +- openjdk/build.gradle | 15 +- .../src/main/java/org/conscrypt/Platform.java | 26 +- testing/build.gradle | 5 + 14 files changed, 1136 insertions(+), 191 deletions(-) delete mode 100644 api-doclet/src/main/java/org/conscrypt/doclet/FilterDoclet.java create mode 100644 api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassIndex.kt create mode 100644 api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassInfo.kt create mode 100644 api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt create mode 100644 api-doclet/src/main/kotlin/org/conscrypt/doclet/ElementUtils.kt create mode 100644 api-doclet/src/main/kotlin/org/conscrypt/doclet/FilterDoclet.kt create mode 100644 api-doclet/src/main/kotlin/org/conscrypt/doclet/HtmlBuilder.kt create mode 100644 api-doclet/src/main/kotlin/org/conscrypt/doclet/Options.kt create mode 100644 api-doclet/src/main/resources/styles.css diff --git a/api-doclet/build.gradle b/api-doclet/build.gradle index 2ada3d55c..d83fffcd6 100644 --- a/api-doclet/build.gradle +++ b/api-doclet/build.gradle @@ -1,21 +1,18 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '2.0.0' +} + description = 'Conscrypt: API Doclet' +kotlin { + jvmToolchain(11) +} -java { - toolchain { - // Force Java 8 for the doclet. - languageVersion = JavaLanguageVersion.of(8) - } - // Java 8 doclets depend on the JDK's tools.jar - def compilerMetadata = javaToolchains.compilerFor(toolchain).get().metadata - def jdkHome = compilerMetadata.getInstallationPath() - def toolsJar = jdkHome.file("lib/tools.jar") - dependencies { - implementation files(toolsJar) - } +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } -tasks.withType(Javadoc) { - // TODO(prb): Update doclet to Java 11. +tasks.withType(Javadoc).configureEach { + // No need to javadoc the Doclet.... enabled = false } diff --git a/api-doclet/src/main/java/org/conscrypt/doclet/FilterDoclet.java b/api-doclet/src/main/java/org/conscrypt/doclet/FilterDoclet.java deleted file mode 100644 index abf83397e..000000000 --- a/api-doclet/src/main/java/org/conscrypt/doclet/FilterDoclet.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (C) 2010 Google Inc. - * - * 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 - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * 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. - */ -/* - * Originally from Doclava project at - * https://android.googlesource.com/platform/external/doclava/+/master/src/com/google/doclava/Doclava.java - */ - -package org.conscrypt.doclet; - -import com.sun.javadoc.*; -import com.sun.tools.doclets.standard.Standard; -import com.sun.tools.javadoc.Main; -import java.io.FileNotFoundException; -import java.lang.reflect.Array; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.util.ArrayList; -import java.util.List; - -/** - * This Doclet filters out all classes, methods, fields, etc. that have the {@code @Internal} - * annotation on them. - */ -public class FilterDoclet extends com.sun.tools.doclets.standard.Standard { - public static void main(String[] args) throws FileNotFoundException { - String name = FilterDoclet.class.getName(); - Main.execute(name, args); - } - - public static boolean start(RootDoc rootDoc) { - return Standard.start((RootDoc) filterHidden(rootDoc, RootDoc.class)); - } - - /** - * Returns true if the given element has an @Internal annotation. - */ - private static boolean hasHideAnnotation(ProgramElementDoc doc) { - for (AnnotationDesc ann : doc.annotations()) { - if (ann.annotationType().qualifiedTypeName().equals("org.conscrypt.Internal")) { - return true; - } - } - return false; - } - - /** - * Returns true if the given element is hidden. - */ - private static boolean isHidden(Doc doc) { - // Methods, fields, constructors. - if (doc instanceof MemberDoc) { - return hasHideAnnotation((MemberDoc) doc); - } - // Classes, interfaces, enums, annotation types. - if (doc instanceof ClassDoc) { - // Check the class doc and containing class docs if this is a - // nested class. - ClassDoc current = (ClassDoc) doc; - do { - if (hasHideAnnotation(current)) { - return true; - } - current = current.containingClass(); - } while (current != null); - } - return false; - } - - /** - * Filters out hidden elements. - */ - private static Object filterHidden(Object o, Class expected) { - if (o == null) { - return null; - } - - Class type = o.getClass(); - if (type.getName().startsWith("com.sun.")) { - // TODO: Implement interfaces from superclasses, too. - return Proxy.newProxyInstance( - type.getClassLoader(), type.getInterfaces(), new HideHandler(o)); - } else if (o instanceof Object[]) { - Class componentType = expected.getComponentType(); - if (componentType == null) { - return o; - } - - Object[] array = (Object[]) o; - List list = new ArrayList(array.length); - for (Object entry : array) { - if ((entry instanceof Doc) && isHidden((Doc) entry)) { - continue; - } - list.add(filterHidden(entry, componentType)); - } - return list.toArray((Object[]) Array.newInstance(componentType, list.size())); - } else { - return o; - } - } - - /** - * Filters hidden elements. - */ - private static class HideHandler implements InvocationHandler { - private final Object target; - - public HideHandler(Object target) { - this.target = target; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - String methodName = method.getName(); - if (args != null) { - if (methodName.equals("compareTo") || methodName.equals("equals") - || methodName.equals("overrides") || methodName.equals("subclassOf")) { - args[0] = unwrap(args[0]); - } - } - - try { - return filterHidden(method.invoke(target, args), method.getReturnType()); - } catch (InvocationTargetException e) { - e.printStackTrace(); - throw e.getTargetException(); - } - } - - private static Object unwrap(Object proxy) { - if (proxy instanceof Proxy) - return ((HideHandler) Proxy.getInvocationHandler(proxy)).target; - return proxy; - } - } -} diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassIndex.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassIndex.kt new file mode 100644 index 000000000..811d13a35 --- /dev/null +++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassIndex.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +package org.conscrypt.doclet + +import javax.lang.model.element.Element +import javax.lang.model.element.TypeElement +import kotlin.streams.toList + +class ClassIndex { + private val index = mutableMapOf() + + private fun put(classInfo: ClassInfo) { + index[classInfo.qualifiedName] = classInfo + } + + fun put(element: Element) { + put(ClassInfo(element as TypeElement)) + } + + fun get(qualifiedName: String) = index[qualifiedName] + fun contains(qualifiedName: String) = index.containsKey(qualifiedName) + fun find(name: String) = if (contains(name)) get(name) else findSimple(name) + private fun findSimple(name: String) = classes().firstOrNull { it.simpleName == name } // XXX dups + + fun classes(): Collection = index.values + + fun addVisible(elements: Set) { + elements + .filterIsInstance() + .filter(Element::isVisibleType) + .forEach(::put) + } + + private fun packages(): List = index.values.stream() + .map { it.packageName } + .distinct() + .sorted() + .toList() + + private fun classesForPackage(packageName: String) = index.values.stream() + .filter { it.packageName == packageName } + .sorted() + .toList() + + fun generateHtml():String = html { + packages().forEach { packageName -> + div("package-section") { + h2("Package $packageName", "package-name") + ul("class-list") { + classesForPackage(packageName) + .forEach { c -> + li { + a(c.fileName, c.simpleName) + } + } + + } + } + } + } +} + diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassInfo.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassInfo.kt new file mode 100644 index 000000000..ffe58b53c --- /dev/null +++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassInfo.kt @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +package org.conscrypt.doclet + +import javax.lang.model.element.Element +import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.TypeElement + + +data class ClassInfo(val element: TypeElement) : Comparable { + val simpleName = element.simpleName.toString() + val qualifiedName = element.qualifiedName.toString() + val packageName = FilterDoclet.elementUtils.getPackageOf(element).qualifiedName.toString() + val fileName = qualifiedName.replace('.', '/') + ".html" + + override fun compareTo(other: ClassInfo) = qualifiedName.compareTo(other.qualifiedName) + + private fun description() = html { + div("class-description") { + compose { element.commentTree() + element.tagTree() } + } + } + + private fun fields() = html { + val fields = element.children(Element::isVisibleField) + if (fields.isNotEmpty()) { + h2("Fields") + fields.forEach { field -> + div("member") { + h4(field.simpleName.toString()) + compose { + field.commentTree() + field.tagTree() + } + } + } + } + } + + private fun nestedClasses() = html { + val nested = element.children(Element::isVisibleType) + nested.takeIf { it.isNotEmpty() }?.let { + h2("Nested Classes") + nested.forEach { cls -> + div("member") { + h4(cls.simpleName.toString()) + compose { + cls.commentTree() + cls.tagTree() + } + } + } + } + } + + private fun method(method: ExecutableElement) = html { + div("member") { + h4(method.simpleName.toString()) + pre(method.methodSignature(), "method-signature") + div("description") { + compose { + method.commentTree() + } + val params = method.paramTags() + val throwns = method.throwTags() + val returns = if (method.isConstructor()) + emptyList() + else + method.returnTag(method.returnType) + + if(params.size + returns.size + throwns.size > 0) { + div("params") { + table("params-table") { + rowGroup(params, title = "Parameters", colspan = 2) { + td {text(it.first)} + td {text(it.second)} + } + rowGroup(returns, title = "Returns", colspan = 2) { + td {text(it.first)} + td {text(it.second)} + } + rowGroup(throwns, title = "Throws", colspan = 2) { + td {text(it.first)} + td {text(it.second)} + } + } + } + } + } + } + } + + private fun executables(title: String, filter: (Element) -> Boolean) = html { + val methods = element.children(filter) + if (methods.isNotEmpty()) { + h2(title) + methods.forEach { + compose { + method(it as ExecutableElement) + } + } + } + } + + private fun constructors() = executables("Constructors", Element::isVisibleConstructor) + private fun methods() = executables("Public Methods", Element::isVisibleMethod) + + fun generateHtml() = html { + div("package-name") { text("Package: $packageName") } + h1(simpleName) + pre(element.signature(), "class-signature") + + compose { + description() + + fields() + + constructors() + + methods() + + nestedClasses() + } + } +} + diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt new file mode 100644 index 000000000..9a241f8e4 --- /dev/null +++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +package org.conscrypt.doclet + +import org.conscrypt.doclet.FilterDoclet.Companion.baseUrl +import com.sun.source.doctree.DocCommentTree +import com.sun.source.doctree.DocTree +import com.sun.source.doctree.EndElementTree +import com.sun.source.doctree.LinkTree +import com.sun.source.doctree.LiteralTree +import com.sun.source.doctree.ParamTree +import com.sun.source.doctree.ReturnTree +import com.sun.source.doctree.SeeTree +import com.sun.source.doctree.StartElementTree +import com.sun.source.doctree.TextTree +import com.sun.source.doctree.ThrowsTree +import org.conscrypt.doclet.FilterDoclet.Companion.classIndex +import org.conscrypt.doclet.FilterDoclet.Companion.docTrees +import javax.lang.model.element.Element +import javax.lang.model.type.TypeMirror + + +fun renderDocTreeList(treeList: List):String = + treeList.joinToString("\n", transform = ::renderDocTree) + +fun renderDocTree(docTree: DocTree): String = when (docTree) { + is TextTree -> docTree.body + is LinkTree -> { + val reference = docTree.reference.toString() + val label = if (docTree.label.isEmpty()) { + reference + } else { + renderDocTree(docTree.label[0]) + } + createLink(reference, label) + } + is StartElementTree, is EndElementTree -> docTree.toString() + is LiteralTree -> "${docTree.body}" + else -> error("[${docTree.javaClass} / ${docTree.kind} --- ${docTree}]") +} + +fun createLink(reference: String, label: String): String { + val parts = reference.split('#') + val className = parts[0] + val anchor = if (parts.size > 1) "#${parts[1]}" else "" + val classInfo = classIndex.find(className) + val href = if (classInfo != null) + "${classInfo.simpleName}.html$anchor" + else + "$baseUrl${className.replace('.', '/')}.html$anchor" + return html { + a(href, label) + } +} + +fun renderBlockTagList(tagList: List): String = + tagList.joinToString("\n", transform = ::renderBlockTag) + +fun renderBlockTag(tag: DocTree) = when (tag) { + is ParamTree, is ReturnTree, is ThrowsTree -> error("Unexpected block tag: $tag") + is SeeTree -> "

See: ${renderDocTreeList(tag.reference)}

" + else -> tag.toString() +} + +inline fun Element.filterTags() = + docTree()?.blockTags?.filterIsInstance() ?: emptyList() + +fun Element.paramTags() = filterTags() + .map { it.name.toString() to renderDocTreeList(it.description) } + .toList() + + +fun Element.returnTag(returnType: TypeMirror): List> { + val list = mutableListOf>() + val descriptions = filterTags() + .map { renderDocTreeList(it.description) } + .singleOrNull() + + if (descriptions != null) { + list.add(returnType.toString() to descriptions) + } + return list +} + +fun Element.throwTags() = filterTags() + .map { it.exceptionName.toString() to renderDocTreeList(it.description) } + .toList() + +fun Element.docTree(): DocCommentTree? { + return docTrees.getDocCommentTree(this) +} + +fun Element.commentTree() = html { + text { + docTree()?.let { renderDocTreeList(it.fullBody) } ?: "" + } +} + +fun Element.tagTree() = html { + text { + docTree()?.let { renderBlockTagList(it.blockTags) } ?: "" + } +} diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/ElementUtils.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/ElementUtils.kt new file mode 100644 index 000000000..a9fcd00e3 --- /dev/null +++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/ElementUtils.kt @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +package org.conscrypt.doclet + +import com.sun.source.doctree.UnknownBlockTagTree +import java.util.Locale +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind +import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.Modifier +import javax.lang.model.element.TypeElement +import javax.lang.model.element.VariableElement +import javax.lang.model.type.TypeMirror + +fun Element.isType() = isClass() || isInterface() || isEnum() +fun Element.isClass() = this is TypeElement && kind == ElementKind.CLASS +fun Element.isEnum() = this is TypeElement && kind == ElementKind.ENUM +fun Element.isInterface() = this is TypeElement && kind == ElementKind.INTERFACE +fun Element.isExecutable() = this is ExecutableElement +fun Element.isField() = this is VariableElement + +fun Element.isVisibleType() = isType() && isVisible() +fun Element.isVisibleMethod() = isExecutable() && isVisible() && kind == ElementKind.METHOD +fun Element.isVisibleConstructor() = isExecutable() && isVisible() && kind == ElementKind.CONSTRUCTOR +fun Element.isVisibleField() = isField() && isVisible() +fun Element.isPublic() = modifiers.contains(Modifier.PUBLIC) +fun Element.isPrivate() = !isPublic() // Ignore protected for now :) +fun Element.isHidden() = isPrivate() || hasHideMarker() || parentIsHidden() +fun Element.isVisible() = !isHidden() +fun Element.hasHideMarker() = hasAnnotation("org.conscrypt.Internal") || hasHideTag() +fun Element.children(filterFunction: (Element) -> Boolean) = enclosedElements + .filter(filterFunction) + .toList() + +fun Element.parentIsHidden(): Boolean + = if (enclosingElement.isType()) enclosingElement.isHidden() else false + +fun Element.hasAnnotation(annotationName: String): Boolean = annotationMirrors + .map { it.annotationType.toString() } + .any { it == annotationName } + + +fun Element.hasHideTag(): Boolean { + return docTree()?.blockTags?.any { + tag -> tag is UnknownBlockTagTree && tag.tagName == "hide" + } ?: false +} + +fun ExecutableElement.isConstructor() = kind == ElementKind.CONSTRUCTOR +fun ExecutableElement.name() = if (isConstructor()) parentName() else simpleName.toString() +fun ExecutableElement.parentName() = enclosingElement.simpleName.toString() + +fun ExecutableElement.methodSignature(): String { + val modifiers = modifiers.joinToString(" ") + val returnType = if (isConstructor()) "" else "${formatType(returnType)} " + + val typeParams = typeParameters.takeIf { it.isNotEmpty() } + ?.joinToString(separator = ", ", prefix = "<", postfix = ">") { + it.asType().toString() } ?: "" + + val parameters = parameters.joinToString(", ") { param -> + "${formatType(param.asType())} ${param.simpleName}" + } + + val exceptions = thrownTypes + .joinToString(", ") + .prefixIfNotEmpty(" throws ") + return "$modifiers $typeParams$returnType${simpleName}($parameters)$exceptions" +} + +fun formatType(typeMirror: TypeMirror): String { + return if (typeMirror.kind.isPrimitive) { + typeMirror.toString() + } else { + typeMirror.toString() + .split('.') + .last() + } +} + +fun TypeElement.signature(): String { + val modifiers = modifiers.joinToString(" ") + val kind = this.kind.toString().lowercase(Locale.getDefault()) + + val superName = superDisplayName(superclass) + + val interfaces = interfaces + .joinToString(", ") + .prefixIfNotEmpty(" implements ") + + return "$modifiers $kind $simpleName$superName$interfaces" +} + +fun superDisplayName(mirror: TypeMirror): String { + return when (mirror.toString()) { + "none", "java.lang.Object" -> "" + else -> " extends $mirror " + } +} + +private fun String.prefixIfNotEmpty(prefix: String): String + = if (isNotEmpty()) prefix + this else this + +private fun String.suffixIfNotEmpty(prefix: String): String + = if (isNotEmpty()) this + prefix else this \ No newline at end of file diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/FilterDoclet.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/FilterDoclet.kt new file mode 100644 index 000000000..77db33fff --- /dev/null +++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/FilterDoclet.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +package org.conscrypt.doclet + +import com.sun.source.util.DocTrees +import jdk.javadoc.doclet.Doclet +import jdk.javadoc.doclet.DocletEnvironment +import jdk.javadoc.doclet.Reporter +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.util.Locale +import javax.lang.model.SourceVersion +import javax.lang.model.util.Elements +import javax.lang.model.util.Types + +class FilterDoclet : Doclet { + companion object { + lateinit var docTrees: DocTrees + lateinit var elementUtils: Elements + lateinit var typeUtils: Types + lateinit var outputPath: Path + var baseUrl: String = "https://docs.oracle.com/javase/8/docs/api/" + val CSS_FILENAME = "styles.css" + var outputDir = "." + var docTitle = "DTITLE" + var windowTitle = "WTITLE" + var noTimestamp: Boolean = false + val classIndex = ClassIndex() + } + + override fun init(locale: Locale?, reporter: Reporter?) = Unit // TODO + override fun getName() = "FilterDoclet" + override fun getSupportedSourceVersion() = SourceVersion.latest() + + override fun run(environment: DocletEnvironment): Boolean { + docTrees = environment.docTrees + elementUtils = environment.elementUtils + typeUtils = environment.typeUtils + outputPath = Paths.get(outputDir) + Files.createDirectories(outputPath) + + classIndex.addVisible(environment.includedElements) + + try { + generateClassFiles() + generateIndex() + return true + } catch (e: Exception) { + System.err.println("Error generating documentation: " + e.message) + e.printStackTrace() + return false + } + } + + private fun generateClassFiles() = classIndex.classes().forEach(::generateClassFile) + + private fun generateIndex() { + val indexPath = outputPath.resolve("index.html") + + html { + body( + title = docTitle, + stylesheet = relativePath(indexPath, CSS_FILENAME), + ) { + div("index-container") { + h1(docTitle, "index-title") + compose { + classIndex.generateHtml() + } + } + } + }.let { + Files.newBufferedWriter(indexPath).use { writer -> + writer.write(it) + } + } + } + + private fun generateClassFile(classInfo: ClassInfo) { + val classFilePath = outputPath.resolve(classInfo.fileName) + Files.createDirectories(classFilePath.parent) + val simpleName = classInfo.simpleName + + html { + body( + title = "$simpleName - conscrypt-openjdk API", + stylesheet = relativePath(classFilePath, CSS_FILENAME), + ) { + compose { + classInfo.generateHtml() + } + } + }.let { + Files.newBufferedWriter(classFilePath).use { writer -> + writer.write(it) + } + } + } + + private fun relativePath(from: Path, to: String): String { + val fromDir = from.parent + val toPath = Paths.get(outputDir).resolve(to) + + if (fromDir == null) { + return to + } + + val relativePath = fromDir.relativize(toPath) + return relativePath.toString().replace('\\', '/') + } + + override fun getSupportedOptions(): Set { + return setOf( + StringOption( + "-d", + "", + "Destination directory for output files" + ) { d: String -> outputDir = d }, + StringOption( + "-doctitle", + "", + "Document title" + ) { t: String -> docTitle = t }, + StringOption( + "-windowtitle", + "<title>", + "Window title" + ) { w: String -> windowTitle = w }, + StringOption( + "-link", + "<link>", + "Link" + ) { l: String -> baseUrl = l }, + BooleanOption( + "-notimestamp", + "Something" + ) { noTimestamp = true }) + } +} \ No newline at end of file diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/HtmlBuilder.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/HtmlBuilder.kt new file mode 100644 index 000000000..0c2758b2a --- /dev/null +++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/HtmlBuilder.kt @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +package org.conscrypt.doclet + +private typealias Block = HtmlBuilder.() -> Unit +private fun Block.render(): String = HtmlBuilder().apply(this).toString() + +class HtmlBuilder { + private val content = StringBuilder() + override fun toString() = content.toString() + + fun text(fragment: () -> String): StringBuilder = text(fragment()) + fun text(text: String): StringBuilder = content.append(text) + fun compose(fragment: () -> String) { + content.append(fragment()) + } + + fun body(title: String, stylesheet: String, content: Block) { + text(""" + <!DOCTYPE html> + <html><head> + <link rel="stylesheet" type="text/css" href="$stylesheet"> + <meta charset="UTF-8"> + <title>$title + + """.trimIndent() + + content.render() + + "") + } + + private fun tagBlock( + tag: String, cssClass: String? = null, colspan: Int? = null, id: String? = null, block: Block) + { + content.append("\n<$tag") + cssClass?.let { content.append(""" class="$it"""") } + colspan?.let { content.append(""" colspan="$it"""") } + id?.let { content.append(""" id="$it"""") } + content.append(">") + content.append(block.render()) + content.append("\n") + } + + fun div(cssClass: String? = null, id: String? = null, block: Block) = + tagBlock("div", cssClass = cssClass, colspan = null, id, block) + fun ul(cssClass: String? = null, id: String? = null, block: Block) = + tagBlock("ul", cssClass = cssClass, colspan = null, id, block) + fun ol(cssClass: String? = null, id: String? = null, block: Block) = + tagBlock("ol", cssClass = cssClass, colspan = null, id, block) + fun table(cssClass: String? = null, id: String? = null, block: Block) = + tagBlock("table", cssClass = cssClass, colspan = null, id, block) + fun tr(cssClass: String? = null, id: String? = null, block: Block) = + tagBlock("tr", cssClass = cssClass, colspan = null, id, block) + fun th(cssClass: String? = null, colspan: Int? = null, id: String? = null, block: Block) = + tagBlock("th", cssClass, colspan, id, block) + fun td(cssClass: String? = null, colspan: Int? = null, id: String? = null, block: Block) = + tagBlock("td", cssClass, colspan, id, block) + + private fun tagValue(tag: String, value: String, cssClass: String? = null) { + val classText = cssClass?.let { """ class="$it"""" } ?: "" + content.append("<$tag$classText>$value\n") + } + + fun h1(heading: String, cssClass: String? = null) = tagValue("h1", heading, cssClass) + fun h1(cssClass: String? = null, block: Block) = h1(block.render(), cssClass) + fun h2(heading: String, cssClass: String? = null) = tagValue("h2", heading, cssClass) + fun h2(cssClass: String? = null, block: Block) = h2(block.render(), cssClass) + fun h3(heading: String, cssClass: String? = null) = tagValue("h3", heading, cssClass) + fun h3(cssClass: String? = null, block: Block) = h2(block.render(), cssClass) + fun h4(heading: String, cssClass: String? = null) = tagValue("h4", heading, cssClass) + fun h4(cssClass: String? = null, block: Block) = h2(block.render(), cssClass) + fun h5(heading: String, cssClass: String? = null) = tagValue("h5", heading, cssClass) + fun h5(cssClass: String? = null, block: Block) = h2(block.render(), cssClass) + + fun p(text: String, cssClass: String? = null) = tagValue("p", text, cssClass) + fun p(cssClass: String? = null, block: Block) = p(block.render(), cssClass) + fun b(text: String, cssClass: String? = null) = tagValue("b", text, cssClass) + fun b(cssClass: String? = null, block: Block) = b(block.render(), cssClass) + fun pre(text: String, cssClass: String? = null) = tagValue("pre", text, cssClass) + fun pre(cssClass: String? = null, block: Block) = pre(block.render(), cssClass) + fun code(text: String, cssClass: String? = null) = tagValue("code", text, cssClass) + fun code(cssClass: String? = null, block: Block) = code(block.render(), cssClass) + fun strong(text: String, cssClass: String? = null) = tagValue("strong", text, cssClass) + fun strong(cssClass: String? = null, block: Block) = strong(block.render(), cssClass) + + fun br() = content.append("
\n") + fun a(href: String, label: String) { + content.append("""$label""") + } + fun a(href: String, block: Block) = a(href, block.render()) + fun a(href: String) = a(href, href) + + fun li(text: String, cssClass: String? = null) = tagValue("li", text, cssClass) + fun li(cssClass: String? = null, block: Block) = li(block.render(), cssClass) + + fun items(collection: Iterable, cssClass: String? = null, + transform: HtmlBuilder.(T) -> Unit = { text(it.toString()) }) { + collection.forEach { + li(cssClass = cssClass) { transform(it) } + } + } + + fun row(item: T, rowClass: String? = null, cellClass: String? = null, + span: Int? = null, + transform: HtmlBuilder.(T) -> Unit = { td {it.toString() } }) { + tr(cssClass = rowClass) { + transform(item) + } + } + fun rowGroup(rows: Collection, title: String? = null, rowClass: String? = null, cellClass: String? = null, + colspan: Int? = null, + transform: HtmlBuilder.(T) -> Unit) { + if(rows.isNotEmpty()) { + title?.let { + tr { + th(colspan = colspan) { + strong(it) + } + } + } + rows.forEach { + tr { + transform(it) + } + } + } + } +} + +fun html(block: Block) = block.render() + +fun exampleSubfunction() = html { + h1("Headings from exampleSubfunction") + listOf("one", "two", "three").forEach { + h1(it) + } +} + +fun example() = html { + val fruits = listOf("Apple", "Banana", "Cherry") + body( + stylesheet = "path/to/stylesheet.css", + title = "Page Title" + ) { + div(cssClass = "example-class") { + text { + "This is a div" + } + h1 { + text("Heading1a") + } + h2 { + a("www.google.com", "Heading with a link") + } + h3("Heading with CSS class", "my-class") + h2("h2", "my-class") + p("Hello world") + compose { + exampleSubfunction() + } + br() + a("www.google.com") { + text("a link with ") + b("bold") + text(" text.") + } + + } + h1("Lists") + + h2("Unordered list:") + ul { + li("First item") + li("Second item") + li { + text { "Complex item with " } + b { text { "bold text" } } + } + ul { + li("First nested item") + li("Second nested item") + } + } + + h2("Ordered list:") + ol { + li("First item") + li("Second item") + li { + text { "Item with a " } + a(href = "https://example.com") { text { "link" } } + } + } + h2("List item iteration") + ul { + // Default + items(fruits) + // Text transform + items(fruits) { + text("I like ${it}.") + } + // HTML transform with a CSS class + items(fruits, "myclass") { + a("www.google.com") { + b(it) + } + } + } + ol("ol-class") { + items((1..5).asIterable()) { + text("Item $it") + } + } + } + val data1 = listOf(1, 2) + val data2 = "3" to "4" + val data3 = listOf( + "tag1" to "Some value", + "tag2" to "Next Value", + "tag3" to "Another value" + ) + + table("table-class") { + tr { + th { + text("First column") + } + th { + text("Second column") + + } + } + tr("tr-class") { + td("td-class") { + text("Data 1") + } + td(colspan = 2, id = "foo") { + text("Data 2") + } + } + tr { + td() { + text("Data 3") + } + } + row(data1, "c1") { + a(href="www.google.com") { text("$it") } + } + row(data2) { p:Pair -> + td { + text(p.first) + } + td { + text(p.second) + } + + } + rowGroup(data3, title = "Row Group", colspan=2) { p: Pair -> + td { + text(p.first) + } + td { + text(p.second) + } + } + } +} + +fun main() { + example().let(::println) +} diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/Options.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/Options.kt new file mode 100644 index 000000000..3fc57b0ff --- /dev/null +++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/Options.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +package org.conscrypt.doclet + +import jdk.javadoc.doclet.Doclet.Option +import java.util.function.Consumer + +abstract class BaseOption(private val name: String) : Option { + override fun getKind() = Option.Kind.STANDARD + override fun getNames(): List = listOf(name) +} + +class StringOption(name: String, + private val parameters: String, + private val description: String, + private val action: Consumer +) : BaseOption(name) { + override fun getArgumentCount() = 1 + override fun getDescription(): String = description + override fun getParameters(): String = parameters + + override fun process(option: String, arguments: MutableList): Boolean { + action.accept(arguments[0]) + return true + } +} + +class BooleanOption(name: String, + private val description: String, + private val action: Runnable): BaseOption(name) { + override fun getArgumentCount() = 0 + override fun getDescription(): String = description + override fun getParameters(): String = "" + + override fun process(option: String, arguments: MutableList): Boolean { + action.run() + return true + } +} diff --git a/api-doclet/src/main/resources/styles.css b/api-doclet/src/main/resources/styles.css new file mode 100644 index 000000000..262f64ed8 --- /dev/null +++ b/api-doclet/src/main/resources/styles.css @@ -0,0 +1,147 @@ +body { + font-family: Arial, sans-serif; + line-height: 1.2; + color: #333; + /* max-width: 800px; */ + margin: 0 auto; + padding: 10px; +} +.method { + margin-bottom: 30px; + border-bottom: 1px solid #eee; + padding-bottom: 20px; +} +.body h3 { + font-size: 24px; + underline: true +} +.method-name { + color: #2c3e50; + font-size: 24px; + margin-bottom: 10px; +} +.method-signature .class-signature { + background-color: #f7f9fa; + border: 1px solid #e1e4e8; + border-radius: 3px; + padding: 12px; + font-family: monospace; + font-size: 14px; + overflow-x: auto; +} +.description { + margin: 15px 0; + padding: 10px; + background-color: #f8f8f8; +} +.params { + margin-top: 20px; +} +.params h5 { + color: #2c3e50; + font-size: 16px; +} +.params-table { + border-collapse: collapse; +} +.params-table th, .params-table td { + border: 1px solid #ddd; + padding: 12px; + text-align: left; +} +.params-table th { + background-color: #f2f2f2; + font-weight: bold; +} +.params-table tr:nth-child(even) { + background-color: #f8f8f8; +} +.constructor { + margin-bottom: 30px; + border-bottom: 1px solid #eee; + padding-bottom: 20px; +} +.constructor-name { + color: #2c3e50; + font-size: 24px; + margin-bottom: 10px; +} +.constructor-signature { + background-color: #f7f9fa; + border: 1px solid #e1e4e8; + border-radius: 3px; + padding: 10px; + font-family: monospace; + font-size: 14px; + overflow-x: auto; +} +/* Index page styles */ +.index-container { + margin: 0 auto; + padding: 20px; +} +.index-title { + color: #2c3e50; + font-size: 32px; + margin-bottom: 20px; + border-bottom: 2px solid #3498db; + padding-bottom: 10px; +} +.package-section { + margin-bottom: 30px; +} +.package-name { + color: #2c3e50; + font-size: 12px; + margin-bottom: 10px; + padding: 10px; +} +.class-list { + list-style-type: none; + padding-left: 20px; +} +.class-list li { + margin-bottom: 5px; +} +.class-list a { + color: #3498db; + text-decoration: none; +} +.class-list a:hover { + text-decoration: underline; +} +.header { + font-size: 28px; + color: #2c3e50; + margin-bottom: 20px; +} + +.class-description { + margin: 20px 0; + padding: 15px; + background-color: #f8f9fa; + font-size: 16px; + line-height: 1.6; +} + +.class-description p { + margin-bottom: 10px; +} + +.class-description code { + background-color: #e9ecef; + padding: 2px 4px; + border-radius: 4px; + font-family: monospace; +} + +.package-name { + font-family: monospace; + font-size: 14px; + color: #6c757d; + background-color: #f1f3f5; + padding: 5px 10px; + border-radius: 4px; + margin-bottom: 20px; + display: inline-block; +} diff --git a/build.gradle b/build.gradle index 1a8bc5bb6..1f54bb9bd 100644 --- a/build.gradle +++ b/build.gradle @@ -3,8 +3,7 @@ import org.gradle.util.VersionNumber buildscript { ext.android_tools = 'com.android.tools.build:gradle:7.4.0' - ext.errorproneVersion = '2.4.0' - ext.errorproneJavacVersion = '9+181-r4173-1' + ext.errorproneVersion = '2.31.0' repositories { google() mavenCentral() @@ -20,7 +19,7 @@ plugins { // Add dependency for build script so we can access Git from our // build script. id 'org.ajoberstar.grgit' version '5.2.2' - id 'net.ltgt.errorprone' version '1.3.0' + id 'net.ltgt.errorprone' version '4.0.0' id "com.google.osdetector" version "1.7.3" id "biz.aQute.bnd.builder" version "6.4.0" apply false } @@ -139,7 +138,6 @@ subprojects { dependencies { errorprone("com.google.errorprone:error_prone_core:$errorproneVersion") - errorproneJavac("com.google.errorprone:javac:$errorproneJavacVersion") } tasks.register("generateProperties", WriteProperties) { @@ -156,9 +154,7 @@ subprojects { if (!androidProject) { java { toolchain { - // Compile with a real JDK 8 so we don't end up with accidental dependencies - // on Java 11 bootclasspath, e.g. ByteBuffer.flip(). - languageVersion = JavaLanguageVersion.of(8) + languageVersion = JavaLanguageVersion.of(11) } } @@ -166,6 +162,8 @@ subprojects { t.configure { options.compilerArgs += ["-Xlint:all", "-Xlint:-options", '-Xmaxwarns', '9999999'] options.encoding = "UTF-8" + options.release = 8 + if (rootProject.hasProperty('failOnWarnings') && rootProject.failOnWarnings.toBoolean()) { options.compilerArgs += ["-Werror"] } @@ -190,14 +188,7 @@ subprojects { javadoc.options { encoding = 'UTF-8' - links 'https://docs.oracle.com/javase/8/docs/api/' - } - - // All non-Android projects build with Java 8, so disable doclint as it's noisy. - allprojects { - tasks.withType(Javadoc) { - options.addStringOption('Xdoclint:none', '-quiet') - } + links 'https://docs.oracle.com/en/java/javase/21/docs/api/java.base/' } tasks.register("javadocJar", Jar) { diff --git a/openjdk/build.gradle b/openjdk/build.gradle index cda93c34d..2d8b4cfe8 100644 --- a/openjdk/build.gradle +++ b/openjdk/build.gradle @@ -129,7 +129,6 @@ ext { } sourceSets { - main { java { srcDirs += "${rootDir}/common/src/main/java" @@ -346,9 +345,17 @@ jacocoTestReport { javadoc { dependsOn(configurations.publicApiDocs) - // TODO(prb): Update doclet to Java 11. - // options.doclet = "org.conscrypt.doclet.FilterDoclet" - // options.docletpath = configurations.publicApiDocs.files as List + options.showFromPublic() + options.doclet = "org.conscrypt.doclet.FilterDoclet" + options.docletpath = configurations.publicApiDocs.files as List + failOnError false + + doLast { + copy { + from "$rootDir/api-doclet/src/main/resources/styles.css" + into "$buildDir/docs/javadoc" + } + } } def jniIncludeDir() { diff --git a/openjdk/src/main/java/org/conscrypt/Platform.java b/openjdk/src/main/java/org/conscrypt/Platform.java index a4bcc74eb..f5770caa9 100644 --- a/openjdk/src/main/java/org/conscrypt/Platform.java +++ b/openjdk/src/main/java/org/conscrypt/Platform.java @@ -80,7 +80,6 @@ import javax.net.ssl.X509TrustManager; import org.conscrypt.ct.CTLogStore; import org.conscrypt.ct.CTPolicy; -import sun.security.x509.AlgorithmId; /** * Platform-specific methods for OpenJDK. @@ -539,13 +538,26 @@ static void blockGuardOnNetwork() {} @SuppressWarnings("unused") static String oidToAlgorithmName(String oid) { try { - return AlgorithmId.get(oid).getName(); - } catch (Exception e) { - return oid; - } catch (IllegalAccessError e) { - // This can happen under JPMS because AlgorithmId isn't exported by java.base - return oid; + Class algorithmIdClass = Class.forName("sun.security.x509.AlgorithmId"); + Method getMethod = algorithmIdClass.getDeclaredMethod("get", String.class); + getMethod.setAccessible(true); + Method getNameMethod = algorithmIdClass.getDeclaredMethod("getName"); + getNameMethod.setAccessible(true); + + Object algIdObj = getMethod.invoke(null, oid); + return (String) getNameMethod.invoke(algIdObj); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException) { + throw(RuntimeException) cause; + } else if (cause instanceof Error) { + throw(Error) cause; + } + throw new RuntimeException(e); + } catch (Exception ignored) { + //Ignored } + return oid; } /* diff --git a/testing/build.gradle b/testing/build.gradle index 37969bc88..fafd5faa6 100644 --- a/testing/build.gradle +++ b/testing/build.gradle @@ -24,3 +24,8 @@ dependencies { libraries.bouncycastle_provider, libraries.junit } + +// No public methods here. +tasks.withType(Javadoc).configureEach { + enabled = false +} From 6eb0502bddc54dd7aa1ebc8484df6ad2fd7cbf35 Mon Sep 17 00:00:00 2001 From: Pete Bentley Date: Sun, 8 Sep 2024 23:04:13 +0100 Subject: [PATCH 2/4] Minor bugfixes. --- .../main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt index 9a241f8e4..6ad49eaab 100644 --- a/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt +++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt @@ -33,7 +33,6 @@ import org.conscrypt.doclet.FilterDoclet.Companion.docTrees import javax.lang.model.element.Element import javax.lang.model.type.TypeMirror - fun renderDocTreeList(treeList: List):String = treeList.joinToString("\n", transform = ::renderDocTree) @@ -44,7 +43,7 @@ fun renderDocTree(docTree: DocTree): String = when (docTree) { val label = if (docTree.label.isEmpty()) { reference } else { - renderDocTree(docTree.label[0]) + renderDocTreeList(docTree.label) } createLink(reference, label) } @@ -72,7 +71,13 @@ fun renderBlockTagList(tagList: List): String = fun renderBlockTag(tag: DocTree) = when (tag) { is ParamTree, is ReturnTree, is ThrowsTree -> error("Unexpected block tag: $tag") - is SeeTree -> "

See: ${renderDocTreeList(tag.reference)}

" + is SeeTree -> html { + br() + p { + strong("See: ") + text(renderDocTreeList(tag.reference)) + } + } else -> tag.toString() } From 12714d670b56592ba53f5305a7d4ed1904fc840f Mon Sep 17 00:00:00 2001 From: Pete Bentley Date: Sun, 8 Sep 2024 23:18:34 +0100 Subject: [PATCH 3/4] Another minor fix. --- .../src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt index 6ad49eaab..2b82f01b6 100644 --- a/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt +++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt @@ -52,7 +52,7 @@ fun renderDocTree(docTree: DocTree): String = when (docTree) { else -> error("[${docTree.javaClass} / ${docTree.kind} --- ${docTree}]") } -fun createLink(reference: String, label: String): String { +fun createLink(reference: String, label: String) = html { val parts = reference.split('#') val className = parts[0] val anchor = if (parts.size > 1) "#${parts[1]}" else "" @@ -61,9 +61,8 @@ fun createLink(reference: String, label: String): String { "${classInfo.simpleName}.html$anchor" else "$baseUrl${className.replace('.', '/')}.html$anchor" - return html { - a(href, label) - } + + a(href, label) } fun renderBlockTagList(tagList: List): String = From 3a586cb7fd3b0b1868b37d96473c2194b245a41c Mon Sep 17 00:00:00 2001 From: Pete Bentley Date: Mon, 9 Sep 2024 13:00:25 +0100 Subject: [PATCH 4/4] Remove some un-needed verbosity when processing DocTrees. --- .../kotlin/org/conscrypt/doclet/ClassInfo.kt | 8 +++++--- .../org/conscrypt/doclet/DocTreeUtils.kt | 19 ++++--------------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassInfo.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassInfo.kt index ffe58b53c..582885ba9 100644 --- a/api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassInfo.kt +++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/ClassInfo.kt @@ -31,7 +31,9 @@ data class ClassInfo(val element: TypeElement) : Comparable { private fun description() = html { div("class-description") { - compose { element.commentTree() + element.tagTree() } + compose { + element.commentsAndTagTrees() + } } } @@ -43,7 +45,7 @@ data class ClassInfo(val element: TypeElement) : Comparable { div("member") { h4(field.simpleName.toString()) compose { - field.commentTree() + field.tagTree() + field.commentsAndTagTrees() } } } @@ -58,7 +60,7 @@ data class ClassInfo(val element: TypeElement) : Comparable { div("member") { h4(cls.simpleName.toString()) compose { - cls.commentTree() + cls.tagTree() + cls.commentsAndTagTrees() } } } diff --git a/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt b/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt index 2b82f01b6..e3ccf8cc6 100644 --- a/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt +++ b/api-doclet/src/main/kotlin/org/conscrypt/doclet/DocTreeUtils.kt @@ -104,18 +104,7 @@ fun Element.throwTags() = filterTags() .map { it.exceptionName.toString() to renderDocTreeList(it.description) } .toList() -fun Element.docTree(): DocCommentTree? { - return docTrees.getDocCommentTree(this) -} - -fun Element.commentTree() = html { - text { - docTree()?.let { renderDocTreeList(it.fullBody) } ?: "" - } -} - -fun Element.tagTree() = html { - text { - docTree()?.let { renderBlockTagList(it.blockTags) } ?: "" - } -} +fun Element.docTree(): DocCommentTree? = docTrees.getDocCommentTree(this) +fun Element.commentTree() = docTree()?.let { renderDocTreeList(it.fullBody) } ?: "" +fun Element.tagTree() = docTree()?.let { renderBlockTagList(it.blockTags) } ?: "" +fun Element.commentsAndTagTrees() = commentTree() + tagTree()