aboutsummaryrefslogtreecommitdiff
path: root/subprojects/analysis-java-psi/src/main/kotlin
diff options
context:
space:
mode:
authorIgnat Beresnev <ignat.beresnev@jetbrains.com>2023-07-05 10:04:55 +0200
committerGitHub <noreply@github.com>2023-07-05 10:04:55 +0200
commit9559158bfeeb274e9ccf1b4563f1b23b42afc493 (patch)
tree3ece0887623cfe2b7148af23001867a1dd5e6597 /subprojects/analysis-java-psi/src/main/kotlin
parentcbd9733d3dd2f52992e98e7cebd072091a572529 (diff)
downloaddokka-9559158bfeeb274e9ccf1b4563f1b23b42afc493.tar.gz
dokka-9559158bfeeb274e9ccf1b4563f1b23b42afc493.tar.bz2
dokka-9559158bfeeb274e9ccf1b4563f1b23b42afc493.zip
Decompose Kotlin/Java analysis (#3034)
* Extract analysis into separate modules
Diffstat (limited to 'subprojects/analysis-java-psi/src/main/kotlin')
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/DefaultPsiToDocumentableTranslator.kt83
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavaAnalysisPlugin.kt106
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavadocTag.kt48
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/SynheticElementDocumentationProvider.kt42
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocComment.kt14
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentCreator.kt9
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFactory.kt20
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFinder.kt64
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocumentationContent.kt11
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocComment.kt84
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocCommentCreator.kt11
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/PsiDocumentationContent.kt22
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/CommentResolutionContext.kt9
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DocCommentParser.kt12
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DokkaPsiParser.kt797
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavaDocCommentParser.kt228
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavadocParser.kt24
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/DocTagParserContext.kt47
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/HtmlToDocTagConverter.kt114
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagContentProvider.kt10
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagResolver.kt114
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/PsiDocTagParser.kt39
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/PsiElementToHtmlConverter.kt214
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/CoreCopyPaste.kt20
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/NoopIntellijLogger.kt43
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PropertiesConventionUtil.kt101
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiAccessorConventionUtil.kt98
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiCommentsUtils.kt49
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiUtil.kt119
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/StdlibUtil.kt33
-rw-r--r--subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/resolveToGetDri.kt7
31 files changed, 2592 insertions, 0 deletions
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/DefaultPsiToDocumentableTranslator.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/DefaultPsiToDocumentableTranslator.kt
new file mode 100644
index 00000000..72cf45d9
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/DefaultPsiToDocumentableTranslator.kt
@@ -0,0 +1,83 @@
+package org.jetbrains.dokka.analysis.java
+
+import com.intellij.openapi.vfs.VirtualFileManager
+import com.intellij.psi.PsiJavaFile
+import com.intellij.psi.PsiKeyword
+import com.intellij.psi.PsiManager
+import com.intellij.psi.PsiModifierListOwner
+import kotlinx.coroutines.coroutineScope
+import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet
+import org.jetbrains.dokka.analysis.java.parsers.DokkaPsiParser
+import org.jetbrains.dokka.analysis.java.parsers.JavaPsiDocCommentParser
+import org.jetbrains.dokka.analysis.java.parsers.JavadocParser
+import org.jetbrains.dokka.model.DModule
+import org.jetbrains.dokka.model.JavaVisibility
+import org.jetbrains.dokka.plugability.DokkaContext
+import org.jetbrains.dokka.plugability.plugin
+import org.jetbrains.dokka.plugability.query
+import org.jetbrains.dokka.plugability.querySingle
+import org.jetbrains.dokka.transformers.sources.AsyncSourceToDocumentableTranslator
+import org.jetbrains.dokka.utilities.parallelMap
+import org.jetbrains.dokka.utilities.parallelMapNotNull
+
+internal class DefaultPsiToDocumentableTranslator : AsyncSourceToDocumentableTranslator {
+
+ override suspend fun invokeSuspending(sourceSet: DokkaSourceSet, context: DokkaContext): DModule {
+ return coroutineScope {
+ val projectProvider = context.plugin<JavaAnalysisPlugin>().querySingle { projectProvider }
+ val project = projectProvider.getProject(sourceSet, context)
+
+ val sourceRootsExtractor = context.plugin<JavaAnalysisPlugin>().querySingle { sourceRootsExtractor }
+ val sourceRoots = sourceRootsExtractor.extract(sourceSet, context)
+
+ val localFileSystem = VirtualFileManager.getInstance().getFileSystem("file")
+
+ val psiFiles = sourceRoots.parallelMap { sourceRoot ->
+ sourceRoot.absoluteFile.walkTopDown().mapNotNull {
+ localFileSystem.findFileByPath(it.path)?.let { vFile ->
+ PsiManager.getInstance(project).findFile(vFile) as? PsiJavaFile
+ }
+ }.toList()
+ }.flatten()
+
+ val docParser = createPsiParser(sourceSet, context)
+
+ DModule(
+ name = context.configuration.moduleName,
+ packages = psiFiles.parallelMapNotNull { it }.groupBy { it.packageName }.toList()
+ .parallelMap { (packageName: String, psiFiles: List<PsiJavaFile>) ->
+ docParser.parsePackage(packageName, psiFiles)
+ },
+ documentation = emptyMap(),
+ expectPresentInSet = null,
+ sourceSets = setOf(sourceSet)
+ )
+ }
+ }
+
+ private fun createPsiParser(sourceSet: DokkaSourceSet, context: DokkaContext): DokkaPsiParser {
+ val projectProvider = context.plugin<JavaAnalysisPlugin>().querySingle { projectProvider }
+ val docCommentParsers = context.plugin<JavaAnalysisPlugin>().query { docCommentParsers }
+ return DokkaPsiParser(
+ sourceSetData = sourceSet,
+ project = projectProvider.getProject(sourceSet, context),
+ logger = context.logger,
+ javadocParser = JavadocParser(
+ docCommentParsers = docCommentParsers,
+ docCommentFinder = context.plugin<JavaAnalysisPlugin>().docCommentFinder
+ ),
+ javaPsiDocCommentParser = docCommentParsers.single { it is JavaPsiDocCommentParser } as JavaPsiDocCommentParser,
+ lightMethodChecker = context.plugin<JavaAnalysisPlugin>().querySingle { kotlinLightMethodChecker }
+ )
+ }
+}
+
+internal fun PsiModifierListOwner.getVisibility() = modifierList?.let {
+ val ml = it.children.toList()
+ when {
+ ml.any { it.text == PsiKeyword.PUBLIC } || it.hasModifierProperty("public") -> JavaVisibility.Public
+ ml.any { it.text == PsiKeyword.PROTECTED } || it.hasModifierProperty("protected") -> JavaVisibility.Protected
+ ml.any { it.text == PsiKeyword.PRIVATE } || it.hasModifierProperty("private") -> JavaVisibility.Private
+ else -> JavaVisibility.Default
+ }
+} ?: JavaVisibility.Default
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavaAnalysisPlugin.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavaAnalysisPlugin.kt
new file mode 100644
index 00000000..8884d444
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavaAnalysisPlugin.kt
@@ -0,0 +1,106 @@
+package org.jetbrains.dokka.analysis.java
+
+import com.intellij.lang.jvm.annotation.JvmAnnotationAttribute
+import com.intellij.openapi.diagnostic.Logger
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiAnnotation
+import org.jetbrains.dokka.CoreExtensions
+import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet
+import org.jetbrains.dokka.InternalDokkaApi
+import org.jetbrains.dokka.analysis.java.doccomment.DocCommentCreator
+import org.jetbrains.dokka.analysis.java.doccomment.DocCommentFactory
+import org.jetbrains.dokka.analysis.java.doccomment.DocCommentFinder
+import org.jetbrains.dokka.analysis.java.doccomment.JavaDocCommentCreator
+import org.jetbrains.dokka.analysis.java.parsers.DocCommentParser
+import org.jetbrains.dokka.analysis.java.parsers.doctag.InheritDocTagContentProvider
+import org.jetbrains.dokka.analysis.java.parsers.JavaPsiDocCommentParser
+import org.jetbrains.dokka.analysis.java.parsers.doctag.InheritDocTagResolver
+import org.jetbrains.dokka.analysis.java.parsers.doctag.PsiDocTagParser
+import org.jetbrains.dokka.analysis.java.util.NoopIntellijLoggerFactory
+import org.jetbrains.dokka.plugability.*
+import java.io.File
+
+
+@InternalDokkaApi
+interface ProjectProvider {
+ fun getProject(sourceSet: DokkaSourceSet, context: DokkaContext): Project
+}
+
+@InternalDokkaApi
+interface SourceRootsExtractor {
+ fun extract(sourceSet: DokkaSourceSet, context: DokkaContext): List<File>
+}
+
+@InternalDokkaApi
+interface BreakingAbstractionKotlinLightMethodChecker {
+ // TODO [beresnev] not even sure it's needed, but left for compatibility and to preserve behaviour
+ fun isLightAnnotation(annotation: PsiAnnotation): Boolean
+ fun isLightAnnotationAttribute(attribute: JvmAnnotationAttribute): Boolean
+}
+
+@InternalDokkaApi
+class JavaAnalysisPlugin : DokkaPlugin() {
+
+ // single
+ val projectProvider by extensionPoint<ProjectProvider>()
+
+ // single
+ val sourceRootsExtractor by extensionPoint<SourceRootsExtractor>()
+
+ // multiple
+ val docCommentCreators by extensionPoint<DocCommentCreator>()
+
+ // multiple
+ val docCommentParsers by extensionPoint<DocCommentParser>()
+
+ // none or more
+ val inheritDocTagContentProviders by extensionPoint<InheritDocTagContentProvider>()
+
+ // TODO [beresnev] figure out a better way depending on what it's used for
+ val kotlinLightMethodChecker by extensionPoint<BreakingAbstractionKotlinLightMethodChecker>()
+
+ private val docCommentFactory by lazy {
+ DocCommentFactory(query { docCommentCreators }.reversed())
+ }
+
+ val docCommentFinder by lazy {
+ DocCommentFinder(logger, docCommentFactory)
+ }
+
+ internal val javaDocCommentCreator by extending {
+ docCommentCreators providing { JavaDocCommentCreator() }
+ }
+
+ private val psiDocTagParser by lazy {
+ PsiDocTagParser(
+ inheritDocTagResolver = InheritDocTagResolver(
+ docCommentFactory = docCommentFactory,
+ docCommentFinder = docCommentFinder,
+ contentProviders = query { inheritDocTagContentProviders }
+ )
+ )
+ }
+
+ internal val javaDocCommentParser by extending {
+ docCommentParsers providing {
+ JavaPsiDocCommentParser(
+ psiDocTagParser
+ )
+ }
+ }
+
+ internal val psiToDocumentableTranslator by extending {
+ CoreExtensions.sourceToDocumentableTranslator providing { DefaultPsiToDocumentableTranslator() }
+ }
+
+ @OptIn(DokkaPluginApiPreview::class)
+ override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement
+
+ private companion object {
+ init {
+ // Suppress messages emitted by the IntelliJ logger since
+ // there's not much the end user can do about it
+ Logger.setFactory(NoopIntellijLoggerFactory())
+ }
+ }
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavadocTag.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavadocTag.kt
new file mode 100644
index 00000000..f5cd550f
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavadocTag.kt
@@ -0,0 +1,48 @@
+package org.jetbrains.dokka.analysis.java
+
+import com.intellij.psi.PsiMethod
+import org.jetbrains.dokka.InternalDokkaApi
+
+@InternalDokkaApi
+sealed class JavadocTag(val name: String)
+
+object AuthorJavadocTag : JavadocTag("author")
+object DeprecatedJavadocTag : JavadocTag("deprecated")
+object DescriptionJavadocTag : JavadocTag("description")
+object ReturnJavadocTag : JavadocTag("return")
+object SinceJavadocTag : JavadocTag("since")
+
+class ParamJavadocTag(
+ val method: PsiMethod,
+ val paramName: String,
+ val paramIndex: Int
+) : JavadocTag(name) {
+ companion object {
+ const val name: String = "param"
+ }
+}
+
+class SeeJavadocTag(
+ val qualifiedReference: String
+) : JavadocTag(name) {
+ companion object {
+ const val name: String = "see"
+ }
+}
+
+sealed class ThrowingExceptionJavadocTag(
+ name: String,
+ val exceptionQualifiedName: String?
+) : JavadocTag(name)
+
+class ThrowsJavadocTag(exceptionQualifiedName: String?) : ThrowingExceptionJavadocTag(name, exceptionQualifiedName) {
+ companion object {
+ const val name: String = "throws"
+ }
+}
+
+class ExceptionJavadocTag(exceptionQualifiedName: String?) : ThrowingExceptionJavadocTag(name, exceptionQualifiedName) {
+ companion object {
+ const val name: String = "exception"
+ }
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/SynheticElementDocumentationProvider.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/SynheticElementDocumentationProvider.kt
new file mode 100644
index 00000000..d780bb40
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/SynheticElementDocumentationProvider.kt
@@ -0,0 +1,42 @@
+package org.jetbrains.dokka.analysis.java
+
+import com.intellij.openapi.project.Project
+import com.intellij.psi.JavaPsiFacade
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.SyntheticElement
+import com.intellij.psi.javadoc.PsiDocComment
+import org.jetbrains.dokka.analysis.java.parsers.JavaPsiDocCommentParser
+import org.jetbrains.dokka.model.doc.DocumentationNode
+
+private const val ENUM_VALUEOF_TEMPLATE_PATH = "/dokka/docs/javadoc/EnumValueOf.java.template"
+private const val ENUM_VALUES_TEMPLATE_PATH = "/dokka/docs/javadoc/EnumValues.java.template"
+
+internal class SyntheticElementDocumentationProvider(
+ private val javadocParser: JavaPsiDocCommentParser,
+ private val project: Project
+) {
+ fun isDocumented(psiElement: PsiElement): Boolean = psiElement is PsiMethod
+ && (psiElement.isSyntheticEnumValuesMethod() || psiElement.isSyntheticEnumValueOfMethod())
+
+ fun getDocumentation(psiElement: PsiElement): DocumentationNode? {
+ val psiMethod = psiElement as? PsiMethod ?: return null
+ val templatePath = when {
+ psiMethod.isSyntheticEnumValuesMethod() -> ENUM_VALUES_TEMPLATE_PATH
+ psiMethod.isSyntheticEnumValueOfMethod() -> ENUM_VALUEOF_TEMPLATE_PATH
+ else -> return null
+ }
+ val docComment = loadSyntheticDoc(templatePath) ?: return null
+ return javadocParser.parsePsiDocComment(docComment, psiElement)
+ }
+
+ private fun loadSyntheticDoc(path: String): PsiDocComment? {
+ val text = javaClass.getResource(path)?.readText() ?: return null
+ return JavaPsiFacade.getElementFactory(project).createDocCommentFromText(text)
+ }
+}
+
+private fun PsiMethod.isSyntheticEnumValuesMethod() = this.isSyntheticEnumFunction() && this.name == "values"
+private fun PsiMethod.isSyntheticEnumValueOfMethod() = this.isSyntheticEnumFunction() && this.name == "valueOf"
+private fun PsiMethod.isSyntheticEnumFunction() = this is SyntheticElement && this.containingClass?.isEnum == true
+
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocComment.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocComment.kt
new file mode 100644
index 00000000..6cc32233
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocComment.kt
@@ -0,0 +1,14 @@
+package org.jetbrains.dokka.analysis.java.doccomment
+
+import org.jetbrains.dokka.InternalDokkaApi
+import org.jetbrains.dokka.analysis.java.JavadocTag
+
+/**
+ * MUST override equals and hashcode
+ */
+@InternalDokkaApi
+interface DocComment {
+ fun hasTag(tag: JavadocTag): Boolean
+
+ fun resolveTag(tag: JavadocTag): List<DocumentationContent>
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentCreator.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentCreator.kt
new file mode 100644
index 00000000..3d7d4247
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentCreator.kt
@@ -0,0 +1,9 @@
+package org.jetbrains.dokka.analysis.java.doccomment
+
+import com.intellij.psi.PsiNamedElement
+import org.jetbrains.dokka.InternalDokkaApi
+
+@InternalDokkaApi
+interface DocCommentCreator {
+ fun create(element: PsiNamedElement): DocComment?
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFactory.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFactory.kt
new file mode 100644
index 00000000..96245ac2
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFactory.kt
@@ -0,0 +1,20 @@
+package org.jetbrains.dokka.analysis.java.doccomment
+
+import com.intellij.psi.PsiNamedElement
+import org.jetbrains.dokka.InternalDokkaApi
+
+@InternalDokkaApi
+class DocCommentFactory(
+ private val docCommentCreators: List<DocCommentCreator>
+) {
+ fun fromElement(element: PsiNamedElement): DocComment? {
+ docCommentCreators.forEach { creator ->
+ val comment = creator.create(element)
+ if (comment != null) {
+ return comment
+ }
+ }
+ return null
+ }
+}
+
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFinder.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFinder.kt
new file mode 100644
index 00000000..32c8dc65
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFinder.kt
@@ -0,0 +1,64 @@
+package org.jetbrains.dokka.analysis.java.doccomment
+
+import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiNamedElement
+import com.intellij.psi.javadoc.PsiDocComment
+import org.jetbrains.dokka.InternalDokkaApi
+import org.jetbrains.dokka.analysis.java.util.from
+import org.jetbrains.dokka.links.DRI
+import org.jetbrains.dokka.utilities.DokkaLogger
+
+@InternalDokkaApi
+class DocCommentFinder(
+ private val logger: DokkaLogger,
+ private val docCommentFactory: DocCommentFactory,
+) {
+ fun findClosestToElement(element: PsiNamedElement): DocComment? {
+ val docComment = docCommentFactory.fromElement(element)
+ if (docComment != null) {
+ return docComment
+ }
+
+ return if (element is PsiMethod) {
+ findClosestToMethod(element)
+ } else {
+ element.children
+ .filterIsInstance<PsiDocComment>()
+ .firstOrNull()
+ ?.let { JavaDocComment(it) }
+ }
+ }
+
+ private fun findClosestToMethod(method: PsiMethod): DocComment? {
+ val superMethods = method.findSuperMethods()
+ if (superMethods.isEmpty()) return null
+
+ if (superMethods.size == 1) {
+ return findClosestToElement(superMethods.single())
+ }
+
+ val superMethodDocumentation = superMethods.map { superMethod -> findClosestToElement(superMethod) }.distinct()
+ if (superMethodDocumentation.size == 1) {
+ return superMethodDocumentation.single()
+ }
+
+ logger.debug(
+ "Conflicting documentation for ${DRI.from(method)}" +
+ "${superMethods.map { DRI.from(it) }}"
+ )
+
+ /* Prioritize super class over interface */
+ val indexOfSuperClass = superMethods.indexOfFirst { superMethod ->
+ val parent = superMethod.parent
+ if (parent is PsiClass) !parent.isInterface
+ else false
+ }
+
+ return if (indexOfSuperClass >= 0) {
+ superMethodDocumentation[indexOfSuperClass]
+ } else {
+ superMethodDocumentation.first()
+ }
+ }
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocumentationContent.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocumentationContent.kt
new file mode 100644
index 00000000..c06ed496
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocumentationContent.kt
@@ -0,0 +1,11 @@
+package org.jetbrains.dokka.analysis.java.doccomment
+
+import org.jetbrains.dokka.InternalDokkaApi
+import org.jetbrains.dokka.analysis.java.JavadocTag
+
+@InternalDokkaApi
+interface DocumentationContent {
+ val tag: JavadocTag
+
+ fun resolveSiblings(): List<DocumentationContent>
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocComment.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocComment.kt
new file mode 100644
index 00000000..5c9be887
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocComment.kt
@@ -0,0 +1,84 @@
+package org.jetbrains.dokka.analysis.java.doccomment
+
+import com.intellij.psi.PsiElement
+import com.intellij.psi.javadoc.PsiDocComment
+import com.intellij.psi.javadoc.PsiDocTag
+import org.jetbrains.dokka.analysis.java.*
+import org.jetbrains.dokka.analysis.java.util.contentElementsWithSiblingIfNeeded
+import org.jetbrains.dokka.analysis.java.util.getKotlinFqName
+import org.jetbrains.dokka.analysis.java.util.hasTag
+import org.jetbrains.dokka.analysis.java.util.resolveToElement
+import org.jetbrains.dokka.utilities.firstIsInstanceOrNull
+
+internal class JavaDocComment(val comment: PsiDocComment) : DocComment {
+ override fun hasTag(tag: JavadocTag): Boolean {
+ return when (tag) {
+ is ThrowingExceptionJavadocTag -> hasTag(tag)
+ else -> comment.hasTag(tag)
+ }
+ }
+
+ private fun hasTag(tag: ThrowingExceptionJavadocTag): Boolean =
+ comment.hasTag(tag) && comment.resolveTag(tag).firstIsInstanceOrNull<PsiDocTag>()
+ ?.resolveToElement()
+ ?.getKotlinFqName() == tag.exceptionQualifiedName
+
+ override fun resolveTag(tag: JavadocTag): List<DocumentationContent> {
+ return when (tag) {
+ is ParamJavadocTag -> resolveParamTag(tag)
+ is ThrowingExceptionJavadocTag -> resolveThrowingTag(tag)
+ else -> comment.resolveTag(tag).map { PsiDocumentationContent(it, tag) }
+ }
+ }
+
+ private fun resolveParamTag(tag: ParamJavadocTag): List<DocumentationContent> {
+ val resolvedParamElements = comment.resolveTag(tag)
+ .filterIsInstance<PsiDocTag>()
+ .map { it.contentElementsWithSiblingIfNeeded() }
+ .firstOrNull {
+ it.firstOrNull()?.text == tag.method.parameterList.parameters[tag.paramIndex].name
+ }.orEmpty()
+
+ return resolvedParamElements
+ .withoutReferenceLink()
+ .map { PsiDocumentationContent(it, tag) }
+ }
+
+ private fun resolveThrowingTag(tag: ThrowingExceptionJavadocTag): List<DocumentationContent> {
+ val resolvedElements = comment.resolveTag(tag)
+ .flatMap {
+ when (it) {
+ is PsiDocTag -> it.contentElementsWithSiblingIfNeeded()
+ else -> listOf(it)
+ }
+ }
+
+ return resolvedElements
+ .withoutReferenceLink()
+ .map { PsiDocumentationContent(it, tag) }
+ }
+
+ private fun PsiDocComment.resolveTag(tag: JavadocTag): List<PsiElement> {
+ return when (tag) {
+ DescriptionJavadocTag -> this.descriptionElements.toList()
+ else -> this.findTagsByName(tag.name).toList()
+ }
+ }
+
+ private fun List<PsiElement>.withoutReferenceLink(): List<PsiElement> = drop(1)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as JavaDocComment
+
+ if (comment != other.comment) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return comment.hashCode()
+ }
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocCommentCreator.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocCommentCreator.kt
new file mode 100644
index 00000000..00efeb0a
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocCommentCreator.kt
@@ -0,0 +1,11 @@
+package org.jetbrains.dokka.analysis.java.doccomment
+
+import com.intellij.psi.PsiDocCommentOwner
+import com.intellij.psi.PsiNamedElement
+
+internal class JavaDocCommentCreator : DocCommentCreator {
+ override fun create(element: PsiNamedElement): DocComment? {
+ val psiDocComment = (element as? PsiDocCommentOwner)?.docComment ?: return null
+ return JavaDocComment(psiDocComment)
+ }
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/PsiDocumentationContent.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/PsiDocumentationContent.kt
new file mode 100644
index 00000000..c36ce50d
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/PsiDocumentationContent.kt
@@ -0,0 +1,22 @@
+package org.jetbrains.dokka.analysis.java.doccomment
+
+import com.intellij.psi.PsiElement
+import com.intellij.psi.javadoc.PsiDocTag
+import org.jetbrains.dokka.analysis.java.JavadocTag
+import org.jetbrains.dokka.analysis.java.util.contentElementsWithSiblingIfNeeded
+
+internal data class PsiDocumentationContent(
+ val psiElement: PsiElement,
+ override val tag: JavadocTag
+) : DocumentationContent {
+
+ override fun resolveSiblings(): List<DocumentationContent> {
+ return if (psiElement is PsiDocTag) {
+ psiElement.contentElementsWithSiblingIfNeeded()
+ .map { content -> PsiDocumentationContent(content, tag) }
+ } else {
+ listOf(this)
+ }
+ }
+
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/CommentResolutionContext.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/CommentResolutionContext.kt
new file mode 100644
index 00000000..1e193648
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/CommentResolutionContext.kt
@@ -0,0 +1,9 @@
+package org.jetbrains.dokka.analysis.java.parsers
+
+import com.intellij.psi.javadoc.PsiDocComment
+import org.jetbrains.dokka.analysis.java.JavadocTag
+
+internal data class CommentResolutionContext(
+ val comment: PsiDocComment,
+ val tag: JavadocTag?
+)
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DocCommentParser.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DocCommentParser.kt
new file mode 100644
index 00000000..3f691799
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DocCommentParser.kt
@@ -0,0 +1,12 @@
+package org.jetbrains.dokka.analysis.java.parsers
+
+import com.intellij.psi.PsiNamedElement
+import org.jetbrains.dokka.InternalDokkaApi
+import org.jetbrains.dokka.analysis.java.doccomment.DocComment
+import org.jetbrains.dokka.model.doc.DocumentationNode
+
+@InternalDokkaApi
+interface DocCommentParser {
+ fun canParse(docComment: DocComment): Boolean
+ fun parse(docComment: DocComment, context: PsiNamedElement): DocumentationNode
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DokkaPsiParser.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DokkaPsiParser.kt
new file mode 100644
index 00000000..45f44338
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DokkaPsiParser.kt
@@ -0,0 +1,797 @@
+package org.jetbrains.dokka.analysis.java.parsers
+
+import com.intellij.lang.jvm.JvmModifier
+import com.intellij.lang.jvm.annotation.JvmAnnotationAttribute
+import com.intellij.lang.jvm.annotation.JvmAnnotationAttributeValue
+import com.intellij.lang.jvm.annotation.JvmAnnotationConstantValue
+import com.intellij.lang.jvm.annotation.JvmAnnotationEnumFieldValue
+import com.intellij.lang.jvm.types.JvmReferenceType
+import com.intellij.openapi.project.Project
+import com.intellij.psi.*
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import org.jetbrains.dokka.DokkaConfiguration
+import org.jetbrains.dokka.analysis.java.BreakingAbstractionKotlinLightMethodChecker
+import org.jetbrains.dokka.analysis.java.SyntheticElementDocumentationProvider
+import org.jetbrains.dokka.analysis.java.getVisibility
+import org.jetbrains.dokka.analysis.java.util.*
+import org.jetbrains.dokka.links.DRI
+import org.jetbrains.dokka.links.nextTarget
+import org.jetbrains.dokka.links.withClass
+import org.jetbrains.dokka.links.withEnumEntryExtra
+import org.jetbrains.dokka.model.*
+import org.jetbrains.dokka.model.AnnotationTarget
+import org.jetbrains.dokka.model.doc.DocumentationNode
+import org.jetbrains.dokka.model.doc.Param
+import org.jetbrains.dokka.model.properties.PropertyContainer
+import org.jetbrains.dokka.utilities.DokkaLogger
+import org.jetbrains.dokka.utilities.parallelForEach
+import org.jetbrains.dokka.utilities.parallelMap
+import org.jetbrains.dokka.utilities.parallelMapNotNull
+
+internal class DokkaPsiParser(
+ private val sourceSetData: DokkaConfiguration.DokkaSourceSet,
+ private val project: Project,
+ private val logger: DokkaLogger,
+ private val javadocParser: JavadocParser,
+ private val javaPsiDocCommentParser: JavaPsiDocCommentParser,
+ private val lightMethodChecker: BreakingAbstractionKotlinLightMethodChecker,
+) {
+ private val syntheticDocProvider = SyntheticElementDocumentationProvider(javaPsiDocCommentParser, project)
+
+ private val cachedBounds = hashMapOf<String, Bound>()
+
+ private val PsiMethod.hash: Int
+ get() = "$returnType $name$parameterList".hashCode()
+
+ private val PsiField.hash: Int
+ get() = "$type $name".hashCode()
+
+ private val PsiClassType.shouldBeIgnored: Boolean
+ get() = isClass("java.lang.Enum") || isClass("java.lang.Object")
+
+ private fun PsiClassType.isClass(qName: String): Boolean {
+ val shortName = qName.substringAfterLast('.')
+ if (className == shortName) {
+ val psiClass = resolve()
+ return psiClass?.qualifiedName == qName
+ }
+ return false
+ }
+
+ private fun <T> T.toSourceSetDependent() = mapOf(sourceSetData to this)
+
+ suspend fun parsePackage(packageName: String, psiFiles: List<PsiJavaFile>): DPackage = coroutineScope {
+ val dri = DRI(packageName = packageName)
+ val packageInfo = psiFiles.singleOrNull { it.name == "package-info.java" }
+ val documentation = packageInfo?.let {
+ javadocParser.parseDocumentation(it).toSourceSetDependent()
+ }.orEmpty()
+ val annotations = packageInfo?.packageStatement?.annotationList?.annotations
+
+ DPackage(
+ dri = dri,
+ functions = emptyList(),
+ properties = emptyList(),
+ classlikes = psiFiles.parallelMap { psiFile ->
+ coroutineScope {
+ psiFile.classes.asIterable().parallelMap { parseClasslike(it, dri) }
+ }
+ }.flatten(),
+ typealiases = emptyList(),
+ documentation = documentation,
+ expectPresentInSet = null,
+ sourceSets = setOf(sourceSetData),
+ extra = PropertyContainer.withAll(
+ annotations?.toList().orEmpty().toListOfAnnotations().toSourceSetDependent().toAnnotations()
+ )
+ )
+ }
+
+ private suspend fun parseClasslike(psi: PsiClass, parent: DRI): DClasslike = coroutineScope {
+ with(psi) {
+ val dri = parent.withClass(name.toString())
+ val superMethodsKeys = hashSetOf<Int>()
+ val superMethods = mutableListOf<Pair<PsiMethod, DRI>>()
+ val superFieldsKeys = hashSetOf<Int>()
+ val superFields = mutableListOf<Pair<PsiField, DRI>>()
+ methods.asIterable().parallelForEach { superMethodsKeys.add(it.hash) }
+
+ /**
+ * Caution! This method mutates
+ * - superMethodsKeys
+ * - superMethods
+ * - superFieldsKeys
+ * - superKeys
+ */
+ /**
+ * Caution! This method mutates
+ * - superMethodsKeys
+ * - superMethods
+ * - superFieldsKeys
+ * - superKeys
+ */
+ fun Array<PsiClassType>.getSuperTypesPsiClasses(): List<Pair<PsiClass, JavaClassKindTypes>> {
+ forEach { type ->
+ (type as? PsiClassType)?.resolve()?.let {
+ val definedAt = DRI.from(it)
+ it.methods.forEach { method ->
+ val hash = method.hash
+ if (!method.isConstructor && !superMethodsKeys.contains(hash) &&
+ method.getVisibility() != JavaVisibility.Private
+ ) {
+ superMethodsKeys.add(hash)
+ superMethods.add(Pair(method, definedAt))
+ }
+ }
+ it.fields.forEach { field ->
+ val hash = field.hash
+ if (!superFieldsKeys.contains(hash)) {
+ superFieldsKeys.add(hash)
+ superFields.add(Pair(field, definedAt))
+ }
+ }
+ }
+ }
+ return filter { !it.shouldBeIgnored }.mapNotNull { supertypePsi ->
+ supertypePsi.resolve()?.let { supertypePsiClass ->
+ val javaClassKind = when {
+ supertypePsiClass.isInterface -> JavaClassKindTypes.INTERFACE
+ else -> JavaClassKindTypes.CLASS
+ }
+ supertypePsiClass to javaClassKind
+ }
+ }
+ }
+
+ fun traversePsiClassForAncestorsAndInheritedMembers(psiClass: PsiClass): AncestryNode {
+ val (classes, interfaces) = psiClass.superTypes.getSuperTypesPsiClasses()
+ .partition { it.second == JavaClassKindTypes.CLASS }
+
+ return AncestryNode(
+ typeConstructor = GenericTypeConstructor(
+ DRI.from(psiClass),
+ psiClass.typeParameters.map { typeParameter ->
+ TypeParameter(
+ dri = DRI.from(typeParameter),
+ name = typeParameter.name.orEmpty(),
+ extra = typeParameter.annotations()
+ )
+ }
+ ),
+ superclass = classes.singleOrNull()?.first?.let(::traversePsiClassForAncestorsAndInheritedMembers),
+ interfaces = interfaces.map { traversePsiClassForAncestorsAndInheritedMembers(it.first) }
+ )
+ }
+
+ val ancestry: AncestryNode = traversePsiClassForAncestorsAndInheritedMembers(this)
+
+ val (regularFunctions, accessors) = splitFunctionsAndAccessors(psi.fields, psi.methods)
+ val (regularSuperFunctions, superAccessors) = splitFunctionsAndAccessors(
+ fields = superFields.map { it.first }.toTypedArray(),
+ methods = superMethods.map { it.first }.toTypedArray()
+ )
+
+ val regularSuperFunctionsKeys = regularSuperFunctions.map { it.hash }.toSet()
+ val regularSuperFunctionsWithDRI = superMethods.filter { it.first.hash in regularSuperFunctionsKeys }
+
+ val superAccessorsWithDRI = superAccessors.mapValues { (field, methods) ->
+ val containsJvmField = field.annotations.mapNotNull { it.toAnnotation() }.any { it.isJvmField() }
+ if (containsJvmField) {
+ emptyList()
+ } else {
+ methods.mapNotNull { method -> superMethods.find { it.first.hash == method.hash } }
+ }
+ }
+
+ val overridden = regularFunctions.flatMap { it.findSuperMethods().toList() }
+ val documentation = javadocParser.parseDocumentation(this).toSourceSetDependent()
+ val allFunctions = async {
+ val parsedRegularFunctions = regularFunctions.parallelMapNotNull {
+ if (!it.isConstructor) parseFunction(
+ it,
+ parentDRI = dri
+ ) else null
+ }
+ val parsedSuperFunctions = regularSuperFunctionsWithDRI
+ .filter { it.first !in overridden }
+ .parallelMap { parseFunction(it.first, inheritedFrom = it.second) }
+
+ parsedRegularFunctions + parsedSuperFunctions
+ }
+ val allFields = async {
+ val parsedFields = fields.toList().parallelMapNotNull {
+ parseField(it, accessors[it].orEmpty())
+ }
+ val parsedSuperFields = superFields.parallelMapNotNull { (field, dri) ->
+ parseFieldWithInheritingAccessors(
+ field,
+ superAccessorsWithDRI[field].orEmpty(),
+ inheritedFrom = dri
+ )
+ }
+ parsedFields + parsedSuperFields
+ }
+ val source = parseSources()
+ val classlikes = async { innerClasses.asIterable().parallelMap { parseClasslike(it, dri) } }
+ val visibility = getVisibility().toSourceSetDependent()
+ val ancestors = (listOfNotNull(ancestry.superclass?.let {
+ it.typeConstructor.let { typeConstructor ->
+ TypeConstructorWithKind(
+ typeConstructor,
+ JavaClassKindTypes.CLASS
+ )
+ }
+ }) + ancestry.interfaces.map {
+ TypeConstructorWithKind(
+ it.typeConstructor,
+ JavaClassKindTypes.INTERFACE
+ )
+ }).toSourceSetDependent()
+ val modifiers = getModifier().toSourceSetDependent()
+ val implementedInterfacesExtra =
+ ImplementedInterfaces(ancestry.allImplementedInterfaces().toSourceSetDependent())
+
+ when {
+ isAnnotationType ->
+ DAnnotation(
+ name = name.orEmpty(),
+ dri = dri,
+ documentation = documentation,
+ expectPresentInSet = null,
+ sources = source,
+ functions = allFunctions.await(),
+ properties = allFields.await(),
+ classlikes = classlikes.await(),
+ visibility = visibility,
+ companion = null,
+ constructors = parseConstructors(dri),
+ generics = mapTypeParameters(dri),
+ sourceSets = setOf(sourceSetData),
+ isExpectActual = false,
+ extra = PropertyContainer.withAll(
+ implementedInterfacesExtra,
+ annotations.toList().toListOfAnnotations().toSourceSetDependent()
+ .toAnnotations()
+ )
+ )
+
+ isEnum -> DEnum(
+ dri = dri,
+ name = name.orEmpty(),
+ entries = fields.filterIsInstance<PsiEnumConstant>().map { entry ->
+ DEnumEntry(
+ dri = dri.withClass(entry.name).withEnumEntryExtra(),
+ name = entry.name,
+ documentation = javadocParser.parseDocumentation(entry).toSourceSetDependent(),
+ expectPresentInSet = null,
+ functions = emptyList(),
+ properties = emptyList(),
+ classlikes = emptyList(),
+ sourceSets = setOf(sourceSetData),
+ extra = PropertyContainer.withAll(
+ implementedInterfacesExtra,
+ annotations.toList().toListOfAnnotations().toSourceSetDependent()
+ .toAnnotations()
+ )
+ )
+ },
+ documentation = documentation,
+ expectPresentInSet = null,
+ sources = source,
+ functions = allFunctions.await(),
+ properties = fields.filter { it !is PsiEnumConstant }
+ .map { parseField(it, accessors[it].orEmpty()) },
+ classlikes = classlikes.await(),
+ visibility = visibility,
+ companion = null,
+ constructors = parseConstructors(dri),
+ supertypes = ancestors,
+ sourceSets = setOf(sourceSetData),
+ isExpectActual = false,
+ extra = PropertyContainer.withAll(
+ implementedInterfacesExtra,
+ annotations.toList().toListOfAnnotations().toSourceSetDependent()
+ .toAnnotations()
+ )
+ )
+
+ isInterface -> DInterface(
+ dri = dri,
+ name = name.orEmpty(),
+ documentation = documentation,
+ expectPresentInSet = null,
+ sources = source,
+ functions = allFunctions.await(),
+ properties = allFields.await(),
+ classlikes = classlikes.await(),
+ visibility = visibility,
+ companion = null,
+ generics = mapTypeParameters(dri),
+ supertypes = ancestors,
+ sourceSets = setOf(sourceSetData),
+ isExpectActual = false,
+ extra = PropertyContainer.withAll(
+ implementedInterfacesExtra,
+ annotations.toList().toListOfAnnotations().toSourceSetDependent()
+ .toAnnotations()
+ )
+ )
+
+ else -> DClass(
+ dri = dri,
+ name = name.orEmpty(),
+ constructors = parseConstructors(dri),
+ functions = allFunctions.await(),
+ properties = allFields.await(),
+ classlikes = classlikes.await(),
+ sources = source,
+ visibility = visibility,
+ companion = null,
+ generics = mapTypeParameters(dri),
+ supertypes = ancestors,
+ documentation = documentation,
+ expectPresentInSet = null,
+ modifier = modifiers,
+ sourceSets = setOf(sourceSetData),
+ isExpectActual = false,
+ extra = PropertyContainer.withAll(
+ implementedInterfacesExtra,
+ annotations.toList().toListOfAnnotations().toSourceSetDependent()
+ .toAnnotations(),
+ ancestry.exceptionInSupertypesOrNull()
+ )
+ )
+ }
+ }
+ }
+
+ /*
+ * Parameter `parentDRI` required for substitute package name:
+ * in the case of synthetic constructor, it will return empty from [DRI.Companion.from].
+ */
+ private fun PsiClass.parseConstructors(parentDRI: DRI): List<DFunction> {
+ val constructors = when {
+ isAnnotationType || isInterface -> emptyArray()
+ isEnum -> this.constructors
+ else -> this.constructors.takeIf { it.isNotEmpty() } ?: arrayOf(createDefaultConstructor())
+ }
+ return constructors.map { parseFunction(psi = it, isConstructor = true, parentDRI = parentDRI) }
+ }
+
+ /**
+ * PSI doesn't return a default constructor if class doesn't contain an explicit one.
+ * This method create synthetic constructor
+ * Visibility modifier is preserved from the class.
+ */
+ private fun PsiClass.createDefaultConstructor(): PsiMethod {
+ val psiElementFactory = JavaPsiFacade.getElementFactory(project)
+ val signature = when (val classVisibility = getVisibility()) {
+ JavaVisibility.Default -> name.orEmpty()
+ else -> "${classVisibility.name} $name"
+ }
+ return psiElementFactory.createConstructor(signature, this)
+ }
+
+ private fun AncestryNode.exceptionInSupertypesOrNull(): ExceptionInSupertypes? =
+ typeConstructorsBeingExceptions().takeIf { it.isNotEmpty() }
+ ?.let { ExceptionInSupertypes(it.toSourceSetDependent()) }
+
+ private fun parseFunction(
+ psi: PsiMethod,
+ isConstructor: Boolean = false,
+ inheritedFrom: DRI? = null,
+ parentDRI: DRI? = null,
+ ): DFunction {
+ val dri = parentDRI?.let { dri ->
+ DRI.from(psi).copy(packageName = dri.packageName, classNames = dri.classNames)
+ } ?: DRI.from(psi)
+ val docs = psi.getDocumentation()
+ return DFunction(
+ dri = dri,
+ name = psi.name,
+ isConstructor = isConstructor,
+ parameters = psi.parameterList.parameters.map { psiParameter ->
+ DParameter(
+ dri = dri.copy(target = dri.target.nextTarget()),
+ name = psiParameter.name,
+ documentation = DocumentationNode(
+ listOfNotNull(docs.firstChildOfTypeOrNull<Param> {
+ it.name == psiParameter.name
+ })
+ ).toSourceSetDependent(),
+ expectPresentInSet = null,
+ type = getBound(psiParameter.type),
+ sourceSets = setOf(sourceSetData),
+ extra = PropertyContainer.withAll(
+ psiParameter.annotations.toList().toListOfAnnotations().toSourceSetDependent()
+ .toAnnotations()
+ )
+ )
+ },
+ documentation = docs.toSourceSetDependent(),
+ expectPresentInSet = null,
+ sources = psi.parseSources(),
+ visibility = psi.getVisibility().toSourceSetDependent(),
+ type = psi.returnType?.let { getBound(type = it) } ?: Void,
+ generics = psi.mapTypeParameters(dri),
+ receiver = null,
+ modifier = psi.getModifier().toSourceSetDependent(),
+ sourceSets = setOf(sourceSetData),
+ isExpectActual = false,
+ extra = psi.additionalExtras().let {
+ PropertyContainer.withAll(
+ inheritedFrom?.let { InheritedMember(it.toSourceSetDependent()) },
+ it.toSourceSetDependent().toAdditionalModifiers(),
+ (psi.annotations.toList()
+ .toListOfAnnotations() + it.toListOfAnnotations()).toSourceSetDependent()
+ .toAnnotations(),
+ ObviousMember.takeIf { psi.isObvious(inheritedFrom) },
+ psi.throwsList.toDriList().takeIf { it.isNotEmpty() }
+ ?.let { CheckedExceptions(it.toSourceSetDependent()) }
+ )
+ }
+ )
+ }
+
+ private fun PsiNamedElement.parseSources(): SourceSetDependent<DocumentableSource> {
+ return when {
+ // `isPhysical` detects the virtual declarations without real sources.
+ // Otherwise, `PsiDocumentableSource` initialization will fail: non-physical declarations doesn't have `virtualFile`.
+ // This check protects from accidentally requesting sources for synthetic / virtual declarations.
+ isPhysical -> PsiDocumentableSource(this).toSourceSetDependent()
+ else -> emptyMap()
+ }
+ }
+
+ private fun PsiMethod.getDocumentation(): DocumentationNode =
+ this.takeIf { it is SyntheticElement }?.let { syntheticDocProvider.getDocumentation(it) }
+ ?: javadocParser.parseDocumentation(this)
+
+ private fun PsiMethod.isObvious(inheritedFrom: DRI? = null): Boolean {
+ return (this is SyntheticElement && !syntheticDocProvider.isDocumented(this))
+ || inheritedFrom?.isObvious() == true
+ }
+
+ private fun DRI.isObvious(): Boolean {
+ return packageName == "java.lang" && (classNames == "Object" || classNames == "Enum")
+ }
+
+ private fun PsiReferenceList.toDriList() = referenceElements.mapNotNull { it?.resolve()?.let { DRI.from(it) } }
+
+ private fun PsiModifierListOwner.additionalExtras() = listOfNotNull(
+ ExtraModifiers.JavaOnlyModifiers.Static.takeIf { hasModifier(JvmModifier.STATIC) },
+ ExtraModifiers.JavaOnlyModifiers.Native.takeIf { hasModifier(JvmModifier.NATIVE) },
+ ExtraModifiers.JavaOnlyModifiers.Synchronized.takeIf { hasModifier(JvmModifier.SYNCHRONIZED) },
+ ExtraModifiers.JavaOnlyModifiers.StrictFP.takeIf { hasModifier(JvmModifier.STRICTFP) },
+ ExtraModifiers.JavaOnlyModifiers.Transient.takeIf { hasModifier(JvmModifier.TRANSIENT) },
+ ExtraModifiers.JavaOnlyModifiers.Volatile.takeIf { hasModifier(JvmModifier.VOLATILE) },
+ ExtraModifiers.JavaOnlyModifiers.Transitive.takeIf { hasModifier(JvmModifier.TRANSITIVE) }
+ ).toSet()
+
+ private fun Set<ExtraModifiers>.toListOfAnnotations() = map {
+ if (it !is ExtraModifiers.JavaOnlyModifiers.Static)
+ Annotations.Annotation(DRI("kotlin.jvm", it.name.toLowerCase().capitalize()), emptyMap())
+ else
+ Annotations.Annotation(DRI("kotlin.jvm", "JvmStatic"), emptyMap())
+ }
+
+ /**
+ * Workaround for getting JvmField Kotlin annotation in PSIs
+ */
+ private fun Collection<PsiAnnotation>.findJvmFieldAnnotation(): Annotations.Annotation? {
+ val anyJvmFieldAnnotation = this.any {
+ it.qualifiedName == "$JVM_FIELD_PACKAGE_NAME.$JVM_FIELD_CLASS_NAMES"
+ }
+ return if (anyJvmFieldAnnotation) {
+ Annotations.Annotation(DRI(JVM_FIELD_PACKAGE_NAME, JVM_FIELD_CLASS_NAMES), emptyMap())
+ } else {
+ null
+ }
+ }
+
+ private fun <T : AnnotationTarget> PsiTypeParameter.annotations(): PropertyContainer<T> = this.annotations.toList().toListOfAnnotations().annotations()
+ private fun <T : AnnotationTarget> PsiType.annotations(): PropertyContainer<T> = this.annotations.toList().toListOfAnnotations().annotations()
+
+ private fun <T : AnnotationTarget> List<Annotations.Annotation>.annotations(): PropertyContainer<T> =
+ this.takeIf { it.isNotEmpty() }?.let { annotations ->
+ PropertyContainer.withAll(annotations.toSourceSetDependent().toAnnotations())
+ } ?: PropertyContainer.empty()
+
+ private fun getBound(type: PsiType): Bound {
+ //We would like to cache most of the bounds since it is not common to annotate them,
+ //but if this is the case, we treat them as 'one of'
+ fun PsiType.cacheBoundIfHasNoAnnotation(f: (List<Annotations.Annotation>) -> Bound): Bound {
+ val annotations = this.annotations.toList().toListOfAnnotations()
+ return if (annotations.isNotEmpty()) f(annotations)
+ else cachedBounds.getOrPut(canonicalText) {
+ f(annotations)
+ }
+ }
+
+ return when (type) {
+ is PsiClassType ->
+ type.resolve()?.let { resolved ->
+ when {
+ resolved.qualifiedName == "java.lang.Object" -> type.cacheBoundIfHasNoAnnotation { annotations -> JavaObject(annotations.annotations()) }
+ resolved is PsiTypeParameter -> {
+ TypeParameter(
+ dri = DRI.from(resolved),
+ name = resolved.name.orEmpty(),
+ extra = type.annotations()
+ )
+ }
+
+ Regex("kotlin\\.jvm\\.functions\\.Function.*").matches(resolved.qualifiedName ?: "") ||
+ Regex("java\\.util\\.function\\.Function.*").matches(
+ resolved.qualifiedName ?: ""
+ ) -> FunctionalTypeConstructor(
+ DRI.from(resolved),
+ type.parameters.map { getProjection(it) },
+ extra = type.annotations()
+ )
+
+ else -> {
+ // cache types that have no annotation and no type parameter
+ // since we cache only by name and type parameters depend on context
+ val typeParameters = type.parameters.map { getProjection(it) }
+ if (typeParameters.isEmpty())
+ type.cacheBoundIfHasNoAnnotation { annotations ->
+ GenericTypeConstructor(
+ DRI.from(resolved),
+ typeParameters,
+ extra = annotations.annotations()
+ )
+ }
+ else
+ GenericTypeConstructor(
+ DRI.from(resolved),
+ typeParameters,
+ extra = type.annotations()
+ )
+ }
+ }
+ } ?: UnresolvedBound(type.presentableText, type.annotations())
+
+ is PsiArrayType -> GenericTypeConstructor(
+ DRI("kotlin", "Array"),
+ listOf(getProjection(type.componentType)),
+ extra = type.annotations()
+ )
+
+ is PsiPrimitiveType -> if (type.name == "void") Void
+ else type.cacheBoundIfHasNoAnnotation { annotations -> PrimitiveJavaType(type.name, annotations.annotations()) }
+ else -> throw IllegalStateException("${type.presentableText} is not supported by PSI parser")
+ }
+ }
+
+
+ private fun getVariance(type: PsiWildcardType): Projection = when {
+ type.extendsBound != PsiType.NULL -> Covariance(getBound(type.extendsBound))
+ type.superBound != PsiType.NULL -> Contravariance(getBound(type.superBound))
+ else -> throw IllegalStateException("${type.presentableText} has incorrect bounds")
+ }
+
+ private fun getProjection(type: PsiType): Projection = when (type) {
+ is PsiEllipsisType -> Star
+ is PsiWildcardType -> getVariance(type)
+ else -> getBound(type)
+ }
+
+ private fun PsiModifierListOwner.getModifier() = when {
+ hasModifier(JvmModifier.ABSTRACT) -> JavaModifier.Abstract
+ hasModifier(JvmModifier.FINAL) -> JavaModifier.Final
+ else -> JavaModifier.Empty
+ }
+
+ private fun PsiTypeParameterListOwner.mapTypeParameters(dri: DRI): List<DTypeParameter> {
+ fun mapBounds(bounds: Array<JvmReferenceType>): List<Bound> =
+ if (bounds.isEmpty()) emptyList() else bounds.mapNotNull {
+ (it as? PsiClassType)?.let { classType -> Nullable(getBound(classType)) }
+ }
+ return typeParameters.map { type ->
+ DTypeParameter(
+ dri = dri.copy(target = dri.target.nextTarget()),
+ name = type.name.orEmpty(),
+ presentableName = null,
+ documentation = javadocParser.parseDocumentation(type).toSourceSetDependent(),
+ expectPresentInSet = null,
+ bounds = mapBounds(type.bounds),
+ sourceSets = setOf(sourceSetData),
+ extra = PropertyContainer.withAll(
+ type.annotations.toList().toListOfAnnotations().toSourceSetDependent()
+ .toAnnotations()
+ )
+ )
+ }
+ }
+
+ private fun parseFieldWithInheritingAccessors(
+ psi: PsiField,
+ accessors: List<Pair<PsiMethod, DRI>>,
+ inheritedFrom: DRI
+ ): DProperty {
+ val getter = accessors
+ .firstOrNull { (method, _) -> method.isGetterFor(psi) }
+ ?.let { (method, dri) -> parseFunction(method, inheritedFrom = dri) }
+
+ val setter = accessors
+ .firstOrNull { (method, _) -> method.isSetterFor(psi) }
+ ?.let { (method, dri) -> parseFunction(method, inheritedFrom = dri) }
+
+ return parseField(
+ psi = psi,
+ getter = getter,
+ setter = setter,
+ inheritedFrom = inheritedFrom
+ )
+ }
+
+ private fun parseField(psi: PsiField, accessors: List<PsiMethod>, inheritedFrom: DRI? = null): DProperty {
+ val getter = accessors.firstOrNull { it.isGetterFor(psi) }?.let { parseFunction(it) }
+ val setter = accessors.firstOrNull { it.isSetterFor(psi) }?.let { parseFunction(it) }
+ return parseField(
+ psi = psi,
+ getter = getter,
+ setter = setter,
+ inheritedFrom = inheritedFrom
+ )
+ }
+
+ private fun parseField(psi: PsiField, getter: DFunction?, setter: DFunction?, inheritedFrom: DRI? = null): DProperty {
+ val dri = DRI.from(psi)
+
+ // non-final java field without accessors should be a var
+ // setter should be not null when inheriting kotlin vars
+ val isMutable = !psi.hasModifierProperty("final")
+ val isVar = (isMutable && getter == null && setter == null) || (getter != null && setter != null)
+
+ return DProperty(
+ dri = dri,
+ name = psi.name,
+ documentation = javadocParser.parseDocumentation(psi).toSourceSetDependent(),
+ expectPresentInSet = null,
+ sources = psi.parseSources(),
+ visibility = psi.getVisibility(getter).toSourceSetDependent(),
+ type = getBound(psi.type),
+ receiver = null,
+ setter = setter,
+ getter = getter,
+ modifier = psi.getModifier().toSourceSetDependent(),
+ sourceSets = setOf(sourceSetData),
+ generics = emptyList(),
+ isExpectActual = false,
+ extra = psi.additionalExtras().let {
+ val psiAnnotations = psi.annotations.toList()
+ val parsedAnnotations = psiAnnotations.toListOfAnnotations()
+ val extraModifierAnnotations = it.toListOfAnnotations()
+ val jvmFieldAnnotation = psiAnnotations.findJvmFieldAnnotation()
+ val annotations = parsedAnnotations + extraModifierAnnotations + listOfNotNull(jvmFieldAnnotation)
+
+ PropertyContainer.withAll(
+ inheritedFrom?.let { inheritedFrom -> InheritedMember(inheritedFrom.toSourceSetDependent()) },
+ it.toSourceSetDependent().toAdditionalModifiers(),
+ annotations.toSourceSetDependent().toAnnotations(),
+ psi.getConstantExpression()?.let { DefaultValue(it.toSourceSetDependent()) },
+ takeIf { isVar }?.let { IsVar }
+ )
+ }
+ )
+ }
+
+ private fun PsiField.getVisibility(getter: DFunction?): Visibility {
+ return getter?.visibility?.get(sourceSetData) ?: this.getVisibility()
+ }
+
+ private fun Collection<PsiAnnotation>.toListOfAnnotations() =
+ filter { !lightMethodChecker.isLightAnnotation(it) }.mapNotNull { it.toAnnotation() }
+
+ private fun PsiField.getConstantExpression(): Expression? {
+ val constantValue = this.computeConstantValue() ?: return null
+ return when (constantValue) {
+ is Byte -> IntegerConstant(constantValue.toLong())
+ is Short -> IntegerConstant(constantValue.toLong())
+ is Int -> IntegerConstant(constantValue.toLong())
+ is Long -> IntegerConstant(constantValue)
+ is Char -> StringConstant(constantValue.toString())
+ is String -> StringConstant(constantValue)
+ is Double -> DoubleConstant(constantValue)
+ is Float -> FloatConstant(constantValue)
+ is Boolean -> BooleanConstant(constantValue)
+ else -> ComplexExpression(constantValue.toString())
+ }
+ }
+
+ private fun JvmAnnotationAttribute.toValue(): AnnotationParameterValue = when (this) {
+ is PsiNameValuePair -> value?.toValue() ?: attributeValue?.toValue() ?: StringValue("")
+ else -> StringValue(this.attributeName)
+ }.let { annotationValue ->
+ if (annotationValue is StringValue) annotationValue.copy(annotationValue.value.removeSurrounding("\""))
+ else annotationValue
+ }
+
+ /**
+ * This is a workaround for static imports from JDK like RetentionPolicy
+ * For some reason they are not represented in the same way than using normal import
+ */
+ private fun JvmAnnotationAttributeValue.toValue(): AnnotationParameterValue? {
+ return when (this) {
+ is JvmAnnotationEnumFieldValue -> (field as? PsiElement)?.let { EnumValue(fieldName ?: "", DRI.from(it)) }
+ // static import of a constant is resolved to constant value instead of a field/link
+ is JvmAnnotationConstantValue -> this.constantValue?.toAnnotationLiteralValue()
+ else -> null
+ }
+ }
+
+ private fun Any.toAnnotationLiteralValue() = when (this) {
+ is Byte -> IntValue(this.toInt())
+ is Short -> IntValue(this.toInt())
+ is Char -> StringValue(this.toString())
+ is Int -> IntValue(this)
+ is Long -> LongValue(this)
+ is Boolean -> BooleanValue(this)
+ is Float -> FloatValue(this)
+ is Double -> DoubleValue(this)
+ else -> StringValue(this.toString())
+ }
+
+ private fun PsiAnnotationMemberValue.toValue(): AnnotationParameterValue? = when (this) {
+ is PsiAnnotation -> toAnnotation()?.let { AnnotationValue(it) }
+ is PsiArrayInitializerMemberValue -> ArrayValue(initializers.mapNotNull { it.toValue() })
+ is PsiReferenceExpression -> psiReference?.let { EnumValue(text ?: "", DRI.from(it)) }
+ is PsiClassObjectAccessExpression -> {
+ val parameterType = (type as? PsiClassType)?.parameters?.firstOrNull()
+ val classType = when (parameterType) {
+ is PsiClassType -> parameterType.resolve()
+ // Notice: Array<String>::class will be passed down as String::class
+ // should probably be Array::class instead but this reflects behaviour for Kotlin sources
+ is PsiArrayType -> (parameterType.componentType as? PsiClassType)?.resolve()
+ else -> null
+ }
+ classType?.let { ClassValue(it.name ?: "", DRI.from(it)) }
+ }
+ is PsiLiteralExpression -> toValue()
+ else -> StringValue(text ?: "")
+ }
+
+ private fun PsiLiteralExpression.toValue(): AnnotationParameterValue? = when (type) {
+ PsiType.INT -> (value as? Int)?.let { IntValue(it) }
+ PsiType.LONG -> (value as? Long)?.let { LongValue(it) }
+ PsiType.FLOAT -> (value as? Float)?.let { FloatValue(it) }
+ PsiType.DOUBLE -> (value as? Double)?.let { DoubleValue(it) }
+ PsiType.BOOLEAN -> (value as? Boolean)?.let { BooleanValue(it) }
+ PsiType.NULL -> NullValue
+ else -> StringValue(text ?: "")
+ }
+
+ private fun PsiAnnotation.toAnnotation(): Annotations.Annotation? {
+ // TODO Mitigating workaround for issue https://github.com/Kotlin/dokka/issues/1341
+ // Tracking https://youtrack.jetbrains.com/issue/KT-41234
+ // Needs to be removed once this issue is fixed in light classes
+ fun PsiElement.getAnnotationsOrNull(): Array<PsiAnnotation>? {
+ this as PsiClass
+ return try {
+ this.annotations
+ } catch (e: Exception) {
+ logger.warn("Failed to get annotations from ${this.qualifiedName}")
+ null
+ }
+ }
+
+ return psiReference?.let { psiElement ->
+ Annotations.Annotation(
+ dri = DRI.from(psiElement),
+ params = attributes
+ .filter { !lightMethodChecker.isLightAnnotationAttribute(it) }
+ .mapNotNull { it.attributeName to it.toValue() }
+ .toMap(),
+ mustBeDocumented = psiElement.getAnnotationsOrNull().orEmpty().any { annotation ->
+ annotation.hasQualifiedName("java.lang.annotation.Documented")
+ }
+ )
+ }
+ }
+
+ private val PsiElement.psiReference
+ get() = getChildOfType<PsiJavaCodeReferenceElement>()?.resolve()
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavaDocCommentParser.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavaDocCommentParser.kt
new file mode 100644
index 00000000..9c475054
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavaDocCommentParser.kt
@@ -0,0 +1,228 @@
+package org.jetbrains.dokka.analysis.java.parsers
+
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiNamedElement
+import com.intellij.psi.PsiWhiteSpace
+import com.intellij.psi.impl.source.tree.JavaDocElementType
+import com.intellij.psi.impl.source.tree.LazyParseablePsiElement
+import com.intellij.psi.javadoc.PsiDocComment
+import com.intellij.psi.javadoc.PsiDocTag
+
+import org.jetbrains.dokka.analysis.java.*
+import org.jetbrains.dokka.analysis.java.doccomment.DocComment
+import org.jetbrains.dokka.analysis.java.doccomment.JavaDocComment
+import org.jetbrains.dokka.analysis.java.parsers.doctag.PsiDocTagParser
+import org.jetbrains.dokka.analysis.java.util.*
+import org.jetbrains.dokka.analysis.markdown.jb.MARKDOWN_ELEMENT_FILE_NAME
+import org.jetbrains.dokka.links.DRI
+import org.jetbrains.dokka.model.doc.*
+import org.jetbrains.dokka.model.doc.Deprecated
+
+internal class JavaPsiDocCommentParser(
+ private val psiDocTagParser: PsiDocTagParser,
+) : DocCommentParser {
+
+ override fun canParse(docComment: DocComment): Boolean {
+ return docComment is JavaDocComment
+ }
+
+ override fun parse(docComment: DocComment, context: PsiNamedElement): DocumentationNode {
+ val javaDocComment = docComment as JavaDocComment
+ return parsePsiDocComment(javaDocComment.comment, context)
+ }
+
+ internal fun parsePsiDocComment(docComment: PsiDocComment, context: PsiNamedElement): DocumentationNode {
+ val description = listOfNotNull(docComment.getDescription())
+ val tags = docComment.tags.mapNotNull { tag ->
+ parseDocTag(tag, docComment, context)
+ }
+ return DocumentationNode(description + tags)
+ }
+
+ private fun PsiDocComment.getDescription(): Description? {
+ val docTags = psiDocTagParser.parseAsParagraph(
+ psiElements = descriptionElements.asIterable(),
+ commentResolutionContext = CommentResolutionContext(this, DescriptionJavadocTag),
+ )
+ return docTags.takeIf { it.isNotEmpty() }?.let {
+ Description(wrapTagIfNecessary(it))
+ }
+ }
+
+ private fun parseDocTag(tag: PsiDocTag, docComment: PsiDocComment, analysedElement: PsiNamedElement): TagWrapper? {
+ return when (tag.name) {
+ ParamJavadocTag.name -> parseParamTag(tag, docComment, analysedElement)
+ ThrowsJavadocTag.name, ExceptionJavadocTag.name -> parseThrowsTag(tag, docComment)
+ ReturnJavadocTag.name -> parseReturnTag(tag, docComment)
+ SinceJavadocTag.name -> parseSinceTag(tag, docComment)
+ AuthorJavadocTag.name -> parseAuthorTag(tag, docComment)
+ SeeJavadocTag.name -> parseSeeTag(tag, docComment)
+ DeprecatedJavadocTag.name -> parseDeprecatedTag(tag, docComment)
+ else -> emptyTagWrapper(tag, docComment)
+ }
+ }
+
+ private fun parseParamTag(
+ tag: PsiDocTag,
+ docComment: PsiDocComment,
+ analysedElement: PsiNamedElement
+ ): TagWrapper? {
+ val paramName = tag.dataElements.firstOrNull()?.text.orEmpty()
+
+ // can be a PsiClass if @param is referencing class generics, like here:
+ // https://github.com/biojava/biojava/blob/2417c230be36e4ba73c62bb3631b60f876265623/biojava-core/src/main/java/org/biojava/nbio/core/alignment/SimpleProfilePair.java#L43
+ // not supported at the moment
+ val method = analysedElement as? PsiMethod ?: return null
+ val paramIndex = method.parameterList.parameters.map { it.name }.indexOf(paramName)
+
+ val docTags = psiDocTagParser.parseAsParagraph(
+ psiElements = tag.contentElementsWithSiblingIfNeeded().drop(1),
+ commentResolutionContext = CommentResolutionContext(
+ comment = docComment,
+ tag = ParamJavadocTag(method, paramName, paramIndex)
+ )
+ )
+ return Param(root = wrapTagIfNecessary(docTags), name = paramName)
+ }
+
+ private fun parseThrowsTag(
+ tag: PsiDocTag,
+ docComment: PsiDocComment
+ ): Throws {
+ val resolvedElement = tag.resolveToElement()
+ val exceptionAddress = resolvedElement?.let { DRI.from(it) }
+
+ /* we always would like to have a fully qualified name as name,
+ * because it will be used as a display name later and we would like to have those unified
+ * even if documentation states shortened version
+ * Only if dri search fails we should use the provided phrase (since then we are not able to get a fq name)
+ */
+ val fullyQualifiedExceptionName =
+ resolvedElement?.getKotlinFqName() ?: tag.dataElements.firstOrNull()?.text.orEmpty()
+
+ val javadocTag = when (tag.name) {
+ ThrowsJavadocTag.name -> ThrowsJavadocTag(fullyQualifiedExceptionName)
+ ExceptionJavadocTag.name -> ExceptionJavadocTag(fullyQualifiedExceptionName)
+ else -> throw IllegalArgumentException("Expected @throws or @exception")
+ }
+
+ val docTags = psiDocTagParser.parseAsParagraph(
+ psiElements = tag.dataElements.drop(1),
+ commentResolutionContext = CommentResolutionContext(
+ comment = docComment,
+ tag = javadocTag
+ ),
+ )
+ return Throws(
+ root = wrapTagIfNecessary(docTags),
+ name = fullyQualifiedExceptionName,
+ exceptionAddress = exceptionAddress
+ )
+ }
+
+ private fun parseReturnTag(
+ tag: PsiDocTag,
+ docComment: PsiDocComment
+ ): Return {
+ val docTags = psiDocTagParser.parseAsParagraph(
+ psiElements = tag.contentElementsWithSiblingIfNeeded(),
+ commentResolutionContext = CommentResolutionContext(comment = docComment, tag = ReturnJavadocTag),
+ )
+ return Return(root = wrapTagIfNecessary(docTags))
+ }
+
+ private fun parseSinceTag(
+ tag: PsiDocTag,
+ docComment: PsiDocComment
+ ): Since {
+ val docTags = psiDocTagParser.parseAsParagraph(
+ psiElements = tag.contentElementsWithSiblingIfNeeded(),
+ commentResolutionContext = CommentResolutionContext(comment = docComment, tag = ReturnJavadocTag),
+ )
+ return Since(root = wrapTagIfNecessary(docTags))
+ }
+
+ private fun parseAuthorTag(
+ tag: PsiDocTag,
+ docComment: PsiDocComment
+ ): Author {
+ // TODO [beresnev] see what the hell this is
+ // Workaround: PSI returns first word after @author tag as a `DOC_TAG_VALUE_ELEMENT`,
+ // then the rest as a `DOC_COMMENT_DATA`, so for `Name Surname` we get them parted
+ val docTags = psiDocTagParser.parseAsParagraph(
+ psiElements = tag.contentElementsWithSiblingIfNeeded(),
+ commentResolutionContext = CommentResolutionContext(comment = docComment, tag = AuthorJavadocTag),
+ )
+ return Author(root = wrapTagIfNecessary(docTags))
+ }
+
+ private fun parseSeeTag(
+ tag: PsiDocTag,
+ docComment: PsiDocComment
+ ): See {
+ val referenceElement = tag.referenceElement()
+ val fullyQualifiedSeeReference = tag.resolveToElement()?.getKotlinFqName()
+ ?: referenceElement?.text.orEmpty().removePrefix("#")
+
+ val context = CommentResolutionContext(comment = docComment, tag = SeeJavadocTag(fullyQualifiedSeeReference))
+ val docTags = psiDocTagParser.parseAsParagraph(
+ psiElements = tag.dataElements.dropWhile {
+ it is PsiWhiteSpace || it.isDocReferenceHolder() || it == referenceElement
+ },
+ commentResolutionContext = context,
+ )
+
+ return See(
+ root = wrapTagIfNecessary(docTags),
+ name = fullyQualifiedSeeReference,
+ address = referenceElement?.toDocumentationLink(context = context)?.dri
+ )
+ }
+
+ private fun PsiElement.isDocReferenceHolder(): Boolean {
+ return (this as? LazyParseablePsiElement)?.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER
+ }
+
+ private fun parseDeprecatedTag(
+ tag: PsiDocTag,
+ docComment: PsiDocComment
+ ): Deprecated {
+ val docTags = psiDocTagParser.parseAsParagraph(
+ tag.contentElementsWithSiblingIfNeeded(),
+ CommentResolutionContext(comment = docComment, tag = DeprecatedJavadocTag),
+ )
+ return Deprecated(root = wrapTagIfNecessary(docTags))
+ }
+
+ private fun wrapTagIfNecessary(tags: List<DocTag>): CustomDocTag {
+ val isFile = (tags.singleOrNull() as? CustomDocTag)?.name == MARKDOWN_ELEMENT_FILE_NAME
+ return if (isFile) {
+ tags.first() as CustomDocTag
+ } else {
+ CustomDocTag(tags, name = MARKDOWN_ELEMENT_FILE_NAME)
+ }
+ }
+
+ // Wrapper for unsupported tags https://github.com/Kotlin/dokka/issues/1618
+ private fun emptyTagWrapper(
+ tag: PsiDocTag,
+ docComment: PsiDocComment,
+ ): CustomTagWrapper {
+ val docTags = psiDocTagParser.parseAsParagraph(
+ psiElements = tag.contentElementsWithSiblingIfNeeded(),
+ commentResolutionContext = CommentResolutionContext(docComment, null),
+ )
+ return CustomTagWrapper(
+ root = wrapTagIfNecessary(docTags),
+ name = tag.name
+ )
+ }
+
+ private fun PsiElement.toDocumentationLink(labelElement: PsiElement? = null, context: CommentResolutionContext): DocumentationLink? {
+ val resolvedElement = this.resolveToGetDri() ?: return null
+ val label = labelElement ?: defaultLabel()
+ val docTags = psiDocTagParser.parse(listOfNotNull(label), context)
+ return DocumentationLink(dri = DRI.from(resolvedElement), children = docTags)
+ }
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavadocParser.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavadocParser.kt
new file mode 100644
index 00000000..b8eba878
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavadocParser.kt
@@ -0,0 +1,24 @@
+package org.jetbrains.dokka.analysis.java.parsers
+
+import com.intellij.psi.PsiNamedElement
+import org.jetbrains.dokka.InternalDokkaApi
+import org.jetbrains.dokka.analysis.java.doccomment.DocCommentFinder
+import org.jetbrains.dokka.model.doc.DocumentationNode
+
+internal fun interface JavaDocumentationParser {
+ fun parseDocumentation(element: PsiNamedElement): DocumentationNode
+}
+
+@InternalDokkaApi
+class JavadocParser(
+ private val docCommentParsers: List<DocCommentParser>,
+ private val docCommentFinder: DocCommentFinder
+) : JavaDocumentationParser {
+
+ override fun parseDocumentation(element: PsiNamedElement): DocumentationNode {
+ val comment = docCommentFinder.findClosestToElement(element) ?: return DocumentationNode(emptyList())
+ return docCommentParsers
+ .first { it.canParse(comment) }
+ .parse(comment, element)
+ }
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/DocTagParserContext.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/DocTagParserContext.kt
new file mode 100644
index 00000000..050736f0
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/DocTagParserContext.kt
@@ -0,0 +1,47 @@
+package org.jetbrains.dokka.analysis.java.parsers.doctag
+
+import org.jetbrains.dokka.InternalDokkaApi
+import org.jetbrains.dokka.links.DRI
+import org.jetbrains.dokka.model.doc.DocumentationNode
+import java.util.*
+
+@InternalDokkaApi
+class DocTagParserContext {
+ /**
+ * exists for resolving `@link element` links, where the referenced
+ * PSI element is mapped as DRI
+ *
+ * only used in the context of parsing to html and then from html to doctag
+ */
+ private val driMap = mutableMapOf<String, DRI>()
+
+ /**
+ * Cache created to make storing entries from kotlin easier.
+ *
+ * It has to be mutable to allow for adding entries when @inheritDoc resolves to kotlin code,
+ * from which we get a DocTags not descriptors.
+ */
+ private val inheritDocSections = mutableMapOf<String, DocumentationNode>()
+
+ /**
+ * @return key of the stored DRI
+ */
+ fun store(dri: DRI): String {
+ val id = dri.toString()
+ driMap[id] = dri
+ return id
+ }
+
+ /**
+ * @return key of the stored documentation node
+ */
+ fun store(documentationNode: DocumentationNode): String {
+ val id = UUID.randomUUID().toString()
+ inheritDocSections[id] = documentationNode
+ return id
+ }
+
+ fun getDri(id: String): DRI? = driMap[id]
+
+ fun getDocumentationNode(id: String): DocumentationNode? = inheritDocSections[id]
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/HtmlToDocTagConverter.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/HtmlToDocTagConverter.kt
new file mode 100644
index 00000000..ea1997b8
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/HtmlToDocTagConverter.kt
@@ -0,0 +1,114 @@
+package org.jetbrains.dokka.analysis.java.parsers.doctag
+
+import org.jetbrains.dokka.analysis.markdown.jb.parseHtmlEncodedWithNormalisedSpaces
+import org.jetbrains.dokka.model.doc.*
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Comment
+import org.jsoup.nodes.Element
+import org.jsoup.nodes.Node
+import org.jsoup.nodes.TextNode
+
+internal class HtmlToDocTagConverter(
+ private val docTagParserContext: DocTagParserContext
+) {
+ fun convertToDocTag(html: String): List<DocTag> {
+ return Jsoup.parseBodyFragment(html)
+ .body()
+ .childNodes()
+ .flatMap { convertHtmlNode(it) }
+ }
+
+ private fun convertHtmlNode(node: Node, keepFormatting: Boolean = false): List<DocTag> = when (node) {
+ is TextNode -> (if (keepFormatting) {
+ node.wholeText.takeIf { it.isNotBlank() }?.let { listOf(Text(body = it)) }
+ } else {
+ node.wholeText.parseHtmlEncodedWithNormalisedSpaces(renderWhiteCharactersAsSpaces = true)
+ }).orEmpty()
+ is Comment -> listOf(Text(body = node.outerHtml(), params = DocTag.contentTypeParam("html")))
+ is Element -> createBlock(node, keepFormatting)
+ else -> emptyList()
+ }
+
+ private fun createBlock(element: Element, keepFormatting: Boolean = false): List<DocTag> {
+ val tagName = element.tagName()
+ val children = element.childNodes()
+ .flatMap { convertHtmlNode(it, keepFormatting = keepFormatting || tagName == "pre" || tagName == "code") }
+
+ fun ifChildrenPresent(operation: () -> DocTag): List<DocTag> {
+ return if (children.isNotEmpty()) listOf(operation()) else emptyList()
+ }
+ return when (tagName) {
+ "blockquote" -> ifChildrenPresent { BlockQuote(children) }
+ "p" -> ifChildrenPresent { P(children) }
+ "b" -> ifChildrenPresent { B(children) }
+ "strong" -> ifChildrenPresent { Strong(children) }
+ "index" -> listOf(Index(children))
+ "i" -> ifChildrenPresent { I(children) }
+ "img" -> listOf(
+ Img(
+ children,
+ element.attributes().associate { (if (it.key == "src") "href" else it.key) to it.value })
+ )
+ "em" -> listOf(Em(children))
+ "code" -> ifChildrenPresent { if(keepFormatting) CodeBlock(children) else CodeInline(children) }
+ "pre" -> if(children.size == 1) {
+ when(children.first()) {
+ is CodeInline -> listOf(CodeBlock(children.first().children))
+ is CodeBlock -> listOf(children.first())
+ else -> listOf(Pre(children))
+ }
+ } else {
+ listOf(Pre(children))
+ }
+ "ul" -> ifChildrenPresent { Ul(children) }
+ "ol" -> ifChildrenPresent { Ol(children) }
+ "li" -> listOf(Li(children))
+ "dl" -> ifChildrenPresent { Dl(children) }
+ "dt" -> listOf(Dt(children))
+ "dd" -> listOf(Dd(children))
+ "a" -> listOf(createLink(element, children))
+ "table" -> ifChildrenPresent { Table(children) }
+ "tr" -> ifChildrenPresent { Tr(children) }
+ "td" -> listOf(Td(children))
+ "thead" -> listOf(THead(children))
+ "tbody" -> listOf(TBody(children))
+ "tfoot" -> listOf(TFoot(children))
+ "caption" -> ifChildrenPresent { Caption(children) }
+ "inheritdoc" -> {
+ // TODO [beresnev] describe how it works
+ val id = element.attr("id")
+ val section = docTagParserContext.getDocumentationNode(id)
+ val parsed = section?.children?.flatMap { it.root.children }.orEmpty()
+ if(parsed.size == 1 && parsed.first() is P){
+ parsed.first().children
+ } else {
+ parsed
+ }
+ }
+ "h1" -> ifChildrenPresent { H1(children) }
+ "h2" -> ifChildrenPresent { H2(children) }
+ "h3" -> ifChildrenPresent { H3(children) }
+ "var" -> ifChildrenPresent { Var(children) }
+ "u" -> ifChildrenPresent { U(children) }
+ else -> listOf(Text(body = element.ownText()))
+ }
+ }
+
+ private fun createLink(element: Element, children: List<DocTag>): DocTag {
+ return when {
+ element.hasAttr("docref") ->
+ A(children, params = mapOf("docref" to element.attr("docref")))
+ element.hasAttr("href") ->
+ A(children, params = mapOf("href" to element.attr("href")))
+ element.hasAttr("data-dri") && docTagParserContext.getDri(element.attr("data-dri")) != null -> {
+ val referencedDriId = element.attr("data-dri")
+ DocumentationLink(
+ dri = docTagParserContext.getDri(referencedDriId)
+ ?: error("docTagParserContext.getDri is null, TODO"), // TODO [beresnev] handle
+ children = children
+ )
+ }
+ else -> Text(body = children.filterIsInstance<Text>().joinToString { it.body })
+ }
+ }
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagContentProvider.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagContentProvider.kt
new file mode 100644
index 00000000..31149898
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagContentProvider.kt
@@ -0,0 +1,10 @@
+package org.jetbrains.dokka.analysis.java.parsers.doctag
+
+import org.jetbrains.dokka.InternalDokkaApi
+import org.jetbrains.dokka.analysis.java.doccomment.DocumentationContent
+
+@InternalDokkaApi
+interface InheritDocTagContentProvider {
+ fun canConvert(content: DocumentationContent): Boolean
+ fun convertToHtml(content: DocumentationContent, docTagParserContext: DocTagParserContext): String
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagResolver.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagResolver.kt
new file mode 100644
index 00000000..031a7b32
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagResolver.kt
@@ -0,0 +1,114 @@
+package org.jetbrains.dokka.analysis.java.parsers.doctag
+
+import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.javadoc.PsiDocComment
+import org.jetbrains.dokka.analysis.java.*
+import org.jetbrains.dokka.analysis.java.doccomment.*
+import org.jetbrains.dokka.analysis.java.doccomment.JavaDocComment
+import org.jetbrains.dokka.analysis.java.parsers.CommentResolutionContext
+
+internal class InheritDocTagResolver(
+ private val docCommentFactory: DocCommentFactory,
+ private val docCommentFinder: DocCommentFinder,
+ private val contentProviders: List<InheritDocTagContentProvider>
+) {
+ internal fun convertToHtml(content: DocumentationContent, docTagParserContext: DocTagParserContext): String? {
+ return contentProviders
+ .firstOrNull { it.canConvert(content) }
+ ?.convertToHtml(content, docTagParserContext)
+ }
+
+ internal fun resolveContent(context: CommentResolutionContext): List<DocumentationContent>? {
+ val javadocTag = context.tag ?: return null
+
+ return when (javadocTag) {
+ is ThrowingExceptionJavadocTag -> {
+ javadocTag.exceptionQualifiedName?.let { _ ->
+ resolveThrowsTag(
+ javadocTag,
+ context.comment,
+ )
+ } ?: return null
+ }
+ is ParamJavadocTag -> resolveParamTag(context.comment, javadocTag)
+ is DeprecatedJavadocTag -> resolveGenericTag(context.comment, DescriptionJavadocTag)
+ is SeeJavadocTag -> emptyList()
+ else -> resolveGenericTag(context.comment, javadocTag)
+ }
+ }
+
+ private fun resolveGenericTag(currentElement: PsiDocComment, tag: JavadocTag): List<DocumentationContent> {
+ val docComment = when (val owner = currentElement.owner) {
+ is PsiClass -> lowestClassWithTag(owner, tag)
+ is PsiMethod -> lowestMethodWithTag(owner, tag)
+ else -> null
+ }
+ return docComment?.resolveTag(tag)?.flatMap {
+ it.resolveSiblings()
+ }.orEmpty()
+ }
+
+ /**
+ * Main resolution point for exception like tags
+ *
+ * This should be used only with [ThrowsJavadocTag] or [ExceptionJavadocTag] as their resolution path should be the same
+ */
+ private fun resolveThrowsTag(
+ tag: ThrowingExceptionJavadocTag,
+ currentElement: PsiDocComment,
+ ): List<DocumentationContent> {
+ val closestDocsWithThrows =
+ (currentElement.owner as? PsiMethod)?.let { method -> lowestMethodsWithTag(method, tag) }
+ .orEmpty().firstOrNull {
+ docCommentFinder.findClosestToElement(it)?.hasTag(tag) == true
+ } ?: return emptyList()
+
+ return docCommentFactory.fromElement(closestDocsWithThrows)
+ ?.resolveTag(tag)
+ ?: emptyList()
+ }
+
+ private fun resolveParamTag(
+ currentElement: PsiDocComment,
+ paramTag: ParamJavadocTag,
+ ): List<DocumentationContent> {
+ val parameterIndex = paramTag.paramIndex
+
+ val methods = (currentElement.owner as? PsiMethod)
+ ?.let { method -> lowestMethodsWithTag(method, paramTag) }
+ .orEmpty()
+
+ return methods.flatMap {
+ if (parameterIndex >= it.parameterList.parametersCount || parameterIndex < 0) {
+ return@flatMap emptyList()
+ }
+
+ val closestTag = docCommentFinder.findClosestToElement(it)
+ val hasTag = closestTag?.hasTag(paramTag) ?: false
+ closestTag?.takeIf { hasTag }?.resolveTag(ParamJavadocTag(it, "", parameterIndex)) ?: emptyList()
+ }
+ }
+
+ //if we are in psi class javadoc only inherits docs from classes and not from interfaces
+ private fun lowestClassWithTag(baseClass: PsiClass, javadocTag: JavadocTag): DocComment? =
+ baseClass.superClass?.let {
+ docCommentFinder.findClosestToElement(it)?.takeIf { tag -> tag.hasTag(javadocTag) } ?: lowestClassWithTag(
+ it,
+ javadocTag
+ )
+ }
+
+ private fun lowestMethodWithTag(
+ baseMethod: PsiMethod,
+ javadocTag: JavadocTag,
+ ): DocComment? {
+ val methodsWithTag = lowestMethodsWithTag(baseMethod, javadocTag).firstOrNull()
+ return methodsWithTag?.let {
+ it.docComment?.let { JavaDocComment(it) } ?: docCommentFinder.findClosestToElement(it)
+ }
+ }
+
+ private fun lowestMethodsWithTag(baseMethod: PsiMethod, javadocTag: JavadocTag): List<PsiMethod> =
+ baseMethod.findSuperMethods().filter { docCommentFinder.findClosestToElement(it)?.hasTag(javadocTag) == true }
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/PsiDocTagParser.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/PsiDocTagParser.kt
new file mode 100644
index 00000000..803eabcb
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/PsiDocTagParser.kt
@@ -0,0 +1,39 @@
+package org.jetbrains.dokka.analysis.java.parsers.doctag
+
+import com.intellij.psi.PsiElement
+import com.intellij.psi.javadoc.PsiDocTag
+import org.jetbrains.dokka.analysis.java.parsers.CommentResolutionContext
+import org.jetbrains.dokka.model.doc.*
+
+/**
+ * Parses [PsiElement] of [PsiDocTag] into Dokka's [DocTag]
+ */
+internal class PsiDocTagParser(
+ private val inheritDocTagResolver: InheritDocTagResolver
+) {
+ fun parse(
+ psiElements: Iterable<PsiElement>,
+ commentResolutionContext: CommentResolutionContext
+ ): List<DocTag> = parse(asParagraph = false, psiElements, commentResolutionContext)
+
+ fun parseAsParagraph(
+ psiElements: Iterable<PsiElement>,
+ commentResolutionContext: CommentResolutionContext
+ ): List<DocTag> = parse(asParagraph = true, psiElements, commentResolutionContext)
+
+ private fun parse(
+ asParagraph: Boolean,
+ psiElements: Iterable<PsiElement>,
+ commentResolutionContext: CommentResolutionContext
+ ): List<DocTag> {
+ val docTagParserContext = DocTagParserContext()
+
+ val psiToHtmlConverter = PsiElementToHtmlConverter(inheritDocTagResolver)
+ val elementsHtml = psiToHtmlConverter.convert(psiElements, docTagParserContext, commentResolutionContext)
+ ?: return emptyList()
+
+ val htmlToDocTagConverter = HtmlToDocTagConverter(docTagParserContext)
+ val html = if (asParagraph) "<p>$elementsHtml</p>" else elementsHtml
+ return htmlToDocTagConverter.convertToDocTag(html)
+ }
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/PsiElementToHtmlConverter.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/PsiElementToHtmlConverter.kt
new file mode 100644
index 00000000..0c20a9b7
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/PsiElementToHtmlConverter.kt
@@ -0,0 +1,214 @@
+package org.jetbrains.dokka.analysis.java.parsers.doctag
+
+import com.intellij.lexer.JavaDocTokenTypes
+import com.intellij.psi.*
+import com.intellij.psi.impl.source.javadoc.PsiDocParamRef
+import com.intellij.psi.impl.source.tree.LeafPsiElement
+import com.intellij.psi.javadoc.PsiDocTagValue
+import com.intellij.psi.javadoc.PsiDocToken
+import com.intellij.psi.javadoc.PsiInlineDocTag
+import org.jetbrains.dokka.analysis.java.doccomment.DocumentationContent
+import org.jetbrains.dokka.analysis.java.JavadocTag
+import org.jetbrains.dokka.analysis.java.doccomment.PsiDocumentationContent
+import org.jetbrains.dokka.analysis.java.parsers.CommentResolutionContext
+import org.jetbrains.dokka.analysis.java.util.*
+import org.jetbrains.dokka.links.DRI
+import org.jetbrains.dokka.utilities.htmlEscape
+
+private const val UNRESOLVED_PSI_ELEMENT = "UNRESOLVED_PSI_ELEMENT"
+
+private data class HtmlParserState(
+ val currentJavadocTag: JavadocTag?,
+ val previousElement: PsiElement? = null,
+ val openPreTags: Int = 0,
+ val closedPreTags: Int = 0
+)
+
+private data class HtmlParsingResult(val newState: HtmlParserState, val parsedLine: String? = null) {
+ constructor(tag: JavadocTag?) : this(HtmlParserState(tag))
+
+ operator fun plus(other: HtmlParsingResult): HtmlParsingResult {
+ return HtmlParsingResult(
+ newState = other.newState,
+ parsedLine = listOfNotNull(parsedLine, other.parsedLine).joinToString(separator = "")
+ )
+ }
+}
+
+internal class PsiElementToHtmlConverter(
+ private val inheritDocTagResolver: InheritDocTagResolver
+) {
+ private val preOpeningTagRegex = "<pre(\\s+.*)?>".toRegex()
+ private val preClosingTagRegex = "</pre>".toRegex()
+
+ fun convert(
+ psiElements: Iterable<PsiElement>,
+ docTagParserContext: DocTagParserContext,
+ commentResolutionContext: CommentResolutionContext
+ ): String? {
+ return WithContext(docTagParserContext, commentResolutionContext)
+ .convert(psiElements)
+ }
+
+ private inner class WithContext(
+ private val docTagParserContext: DocTagParserContext,
+ private val commentResolutionContext: CommentResolutionContext
+ ) {
+ fun convert(psiElements: Iterable<PsiElement>): String? {
+ val parsingResult =
+ psiElements.fold(HtmlParsingResult(commentResolutionContext.tag)) { resultAccumulator, psiElement ->
+ resultAccumulator + parseHtml(psiElement, resultAccumulator.newState)
+ }
+ return parsingResult.parsedLine?.trim()
+ }
+
+ private fun parseHtml(psiElement: PsiElement, state: HtmlParserState): HtmlParsingResult =
+ when (psiElement) {
+ is PsiReference -> psiElement.children.fold(HtmlParsingResult(state)) { acc, e ->
+ acc + parseHtml(e, acc.newState)
+ }
+ else -> parseHtmlOfSimpleElement(psiElement, state)
+ }
+
+ private fun parseHtmlOfSimpleElement(psiElement: PsiElement, state: HtmlParserState): HtmlParsingResult {
+ val text = psiElement.text
+
+ val openPre = state.openPreTags + preOpeningTagRegex.findAll(text).count()
+ val closedPre = state.closedPreTags + preClosingTagRegex.findAll(text).count()
+ val isInsidePre = openPre > closedPre
+
+ val parsed = when (psiElement) {
+ is PsiInlineDocTag -> psiElement.toHtml(state.currentJavadocTag)
+ is PsiDocParamRef -> psiElement.toDocumentationLinkString()
+ is PsiDocTagValue, is LeafPsiElement -> {
+ psiElement.stringifyElementAsText(isInsidePre, state.previousElement)
+ }
+ else -> null
+ }
+ val previousElement = if (text.trim() == "") state.previousElement else psiElement
+ return HtmlParsingResult(
+ state.copy(
+ previousElement = previousElement,
+ closedPreTags = closedPre,
+ openPreTags = openPre
+ ), parsed
+ )
+ }
+
+ /**
+ * Inline tags can be met in the middle of some text. Example of an inline tag usage:
+ *
+ * ```java
+ * Use the {@link #getComponentAt(int, int) getComponentAt} method.
+ * ```
+ */
+ private fun PsiInlineDocTag.toHtml(javadocTag: JavadocTag?): String? =
+ when (this.name) {
+ "link", "linkplain" -> this.referenceElement()
+ ?.toDocumentationLinkString(this.dataElements.filterIsInstance<PsiDocToken>().joinToString(" ") {
+ it.stringifyElementAsText(keepFormatting = false).orEmpty()
+ })
+
+ "code" -> "<code data-inline>${dataElementsAsText(this)}</code>"
+ "literal" -> "<literal>${dataElementsAsText(this)}</literal>"
+ "index" -> "<index>${this.children.filterIsInstance<PsiDocTagValue>().joinToString { it.text }}</index>"
+ "inheritDoc" -> {
+ val inheritDocContent = inheritDocTagResolver.resolveContent(commentResolutionContext)
+ val html = inheritDocContent?.fold(HtmlParsingResult(javadocTag)) { result, content ->
+ result + content.toInheritDocHtml(result.newState, docTagParserContext)
+ }?.parsedLine.orEmpty()
+ html
+ }
+
+ else -> this.text
+ }
+
+ private fun DocumentationContent.toInheritDocHtml(
+ parserState: HtmlParserState,
+ docTagParserContext: DocTagParserContext
+ ): HtmlParsingResult {
+ // TODO [beresnev] comment
+ return if (this is PsiDocumentationContent) {
+ parseHtml(this.psiElement, parserState)
+ } else {
+ HtmlParsingResult(parserState, inheritDocTagResolver.convertToHtml(this, docTagParserContext))
+ }
+ }
+
+ private fun dataElementsAsText(tag: PsiInlineDocTag): String {
+ return tag.dataElements.joinToString("") {
+ it.stringifyElementAsText(keepFormatting = true).orEmpty()
+ }.htmlEscape()
+ }
+
+ private fun PsiElement.toDocumentationLinkString(label: String = ""): String {
+ val driId = reference?.resolve()?.takeIf { it !is PsiParameter }?.let {
+ val dri = DRI.from(it)
+ val id = docTagParserContext.store(dri)
+ id
+ } ?: UNRESOLVED_PSI_ELEMENT // TODO [beresnev] log this somewhere maybe?
+
+ // TODO [beresnev] data-dri into a constant
+ return """<a data-dri="${driId.htmlEscape()}">${label.ifBlank { defaultLabel().text }}</a>"""
+ }
+ }
+}
+
+private fun PsiElement.stringifyElementAsText(keepFormatting: Boolean, previousElement: PsiElement? = null) =
+ if (keepFormatting) {
+ /*
+ For values in the <pre> tag we try to keep formatting, so only the leading space is trimmed,
+ since it is there because it separates this line from the leading asterisk
+ */
+ text.let {
+ if (((prevSibling as? PsiDocToken)?.isLeadingAsterisk() == true || (prevSibling as? PsiDocToken)?.isTagName() == true) && it.firstOrNull() == ' ')
+ it.drop(1) else it
+ }.let {
+ if ((nextSibling as? PsiDocToken)?.isLeadingAsterisk() == true) it.dropLastWhile { it == ' ' } else it
+ }
+ } else {
+ /*
+ Outside of the <pre> we would like to trim everything from the start and end of a line since
+ javadoc doesn't care about it.
+ */
+ text.let {
+ if ((prevSibling as? PsiDocToken)?.isLeadingAsterisk() == true && text.isNotBlank() && previousElement !is PsiInlineDocTag) it?.trimStart() else it
+ }?.let {
+ if ((nextSibling as? PsiDocToken)?.isLeadingAsterisk() == true && text.isNotBlank()) it.trimEnd() else it
+ }?.let {
+ if (shouldHaveSpaceAtTheEnd()) "$it " else it
+ }
+ }
+
+private fun PsiDocToken.isLeadingAsterisk() = tokenType == JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS
+
+private fun PsiDocToken.isTagName() = tokenType == JavaDocTokenType.DOC_TAG_NAME
+
+/**
+ * We would like to know if we need to have a space after a this tag
+ *
+ * The space is required when:
+ * - tag spans multiple lines, between every line we would need a space
+ *
+ * We wouldn't like to render a space if:
+ * - tag is followed by an end of comment
+ * - after a tag there is another tag (eg. multiple @author tags)
+ * - they end with an html tag like: <a href="...">Something</a> since then the space will be displayed in the following text
+ * - next line starts with a <p> or <pre> token
+ */
+private fun PsiElement.shouldHaveSpaceAtTheEnd(): Boolean {
+ val siblings = siblings(withItself = false).toList().filterNot { it.text.trim() == "" }
+ val nextNotEmptySibling = (siblings.firstOrNull() as? PsiDocToken)
+ val furtherNotEmptySibling =
+ (siblings.drop(1).firstOrNull { it is PsiDocToken && !it.isLeadingAsterisk() } as? PsiDocToken)
+ val lastHtmlTag = text.trim().substringAfterLast("<")
+ val endsWithAnUnclosedTag = lastHtmlTag.endsWith(">") && !lastHtmlTag.startsWith("</")
+
+ return (nextSibling as? PsiWhiteSpace)?.text?.startsWith("\n ") == true &&
+ (getNextSiblingIgnoringWhitespace() as? PsiDocToken)?.tokenType != JavaDocTokenTypes.INSTANCE.commentEnd() &&
+ nextNotEmptySibling?.isLeadingAsterisk() == true &&
+ furtherNotEmptySibling?.tokenType == JavaDocTokenTypes.INSTANCE.commentData() &&
+ !endsWithAnUnclosedTag
+}
+
+
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/CoreCopyPaste.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/CoreCopyPaste.kt
new file mode 100644
index 00000000..d8702336
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/CoreCopyPaste.kt
@@ -0,0 +1,20 @@
+package org.jetbrains.dokka.analysis.java.util
+
+import org.jetbrains.dokka.links.DRI
+import org.jetbrains.dokka.model.AncestryNode
+import org.jetbrains.dokka.model.TypeConstructor
+
+// TODO [beresnev] copy-pasted
+internal fun AncestryNode.typeConstructorsBeingExceptions(): List<TypeConstructor> {
+ fun traverseSupertypes(ancestry: AncestryNode): List<TypeConstructor> =
+ listOf(ancestry.typeConstructor) + (ancestry.superclass?.let(::traverseSupertypes) ?: emptyList())
+
+ return superclass?.let(::traverseSupertypes)?.filter { type -> type.dri.isDirectlyAnException() } ?: emptyList()
+}
+
+// TODO [beresnev] copy-pasted
+internal fun DRI.isDirectlyAnException(): Boolean =
+ toString().let { stringed ->
+ stringed == "kotlin/Exception///PointingToDeclaration/" ||
+ stringed == "java.lang/Exception///PointingToDeclaration/"
+ }
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/NoopIntellijLogger.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/NoopIntellijLogger.kt
new file mode 100644
index 00000000..82482e35
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/NoopIntellijLogger.kt
@@ -0,0 +1,43 @@
+package org.jetbrains.dokka.analysis.java.util
+
+import com.intellij.openapi.diagnostic.Attachment
+import com.intellij.openapi.diagnostic.DefaultLogger
+import com.intellij.openapi.diagnostic.Logger
+
+internal class NoopIntellijLoggerFactory : Logger.Factory {
+ override fun getLoggerInstance(p0: String): Logger = NoopIntellijLogger
+}
+
+/**
+ * Ignores all messages passed to it
+ */
+internal object NoopIntellijLogger : DefaultLogger(null) {
+ override fun isDebugEnabled(): Boolean = false
+ override fun isTraceEnabled(): Boolean = false
+
+ override fun debug(message: String?) {}
+ override fun debug(t: Throwable?) {}
+ override fun debug(message: String?, t: Throwable?) {}
+ override fun debug(message: String, vararg details: Any?) {}
+ override fun debugValues(header: String, values: MutableCollection<*>) {}
+
+ override fun trace(message: String?) {}
+ override fun trace(t: Throwable?) {}
+
+ override fun info(message: String?) {}
+ override fun info(message: String?, t: Throwable?) {}
+ override fun info(t: Throwable) {}
+
+ override fun warn(message: String?, t: Throwable?) {}
+ override fun warn(message: String?) {}
+ override fun warn(t: Throwable) {}
+
+ override fun error(message: String?, t: Throwable?, vararg details: String?) {}
+ override fun error(message: String?) {}
+ override fun error(message: Any?) {}
+ override fun error(message: String?, vararg attachments: Attachment?) {}
+ override fun error(message: String?, t: Throwable?, vararg attachments: Attachment?) {}
+ override fun error(message: String?, vararg details: String?) {}
+ override fun error(message: String?, t: Throwable?) {}
+ override fun error(t: Throwable) {}
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PropertiesConventionUtil.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PropertiesConventionUtil.kt
new file mode 100644
index 00000000..137f0792
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PropertiesConventionUtil.kt
@@ -0,0 +1,101 @@
+package org.jetbrains.dokka.analysis.java.util
+
+// TODO [beresnev] copy-paste
+
+internal fun propertyNamesBySetMethodName(methodName: String): List<String> =
+ listOfNotNull(propertyNameBySetMethodName(methodName, false), propertyNameBySetMethodName(methodName, true))
+
+internal fun propertyNameByGetMethodName(methodName: String): String? =
+ propertyNameFromAccessorMethodName(methodName, "get") ?: propertyNameFromAccessorMethodName(methodName, "is", removePrefix = false)
+
+private fun propertyNameBySetMethodName(methodName: String, withIsPrefix: Boolean): String? =
+ propertyNameFromAccessorMethodName(methodName, "set", addPrefix = if (withIsPrefix) "is" else null)
+
+private fun propertyNameFromAccessorMethodName(
+ methodName: String,
+ prefix: String,
+ removePrefix: Boolean = true,
+ addPrefix: String? = null
+): String? {
+ val isSpecial = methodName.startsWith("<") // see special in org.jetbrains.kotlin.Name
+ if (isSpecial) return null
+ if (!methodName.startsWith(prefix)) return null
+ if (methodName.length == prefix.length) return null
+ if (methodName[prefix.length] in 'a'..'z') return null
+
+ if (addPrefix != null) {
+ assert(removePrefix)
+ return addPrefix + methodName.removePrefix(prefix)
+ }
+
+ if (!removePrefix) return methodName
+ val name = methodName.removePrefix(prefix).decapitalizeSmartForCompiler(asciiOnly = true)
+ if (!isValidIdentifier(name)) return null
+ return name
+}
+
+/**
+ * "FooBar" -> "fooBar"
+ * "FOOBar" -> "fooBar"
+ * "FOO" -> "foo"
+ * "FOO_BAR" -> "foO_BAR"
+ */
+private fun String.decapitalizeSmartForCompiler(asciiOnly: Boolean = false): String {
+ if (isEmpty() || !isUpperCaseCharAt(0, asciiOnly)) return this
+
+ if (length == 1 || !isUpperCaseCharAt(1, asciiOnly)) {
+ return if (asciiOnly) decapitalizeAsciiOnly() else replaceFirstChar(Char::lowercaseChar)
+ }
+
+ val secondWordStart = (indices.firstOrNull { !isUpperCaseCharAt(it, asciiOnly) } ?: return toLowerCase(this, asciiOnly)) - 1
+
+ return toLowerCase(substring(0, secondWordStart), asciiOnly) + substring(secondWordStart)
+}
+
+private fun String.isUpperCaseCharAt(index: Int, asciiOnly: Boolean): Boolean {
+ val c = this[index]
+ return if (asciiOnly) c in 'A'..'Z' else c.isUpperCase()
+}
+
+private fun toLowerCase(string: String, asciiOnly: Boolean): String {
+ return if (asciiOnly) string.toLowerCaseAsciiOnly() else string.lowercase()
+}
+
+private fun toUpperCase(string: String, asciiOnly: Boolean): String {
+ return if (asciiOnly) string.toUpperCaseAsciiOnly() else string.uppercase()
+}
+
+private fun String.decapitalizeAsciiOnly(): String {
+ if (isEmpty()) return this
+ val c = this[0]
+ return if (c in 'A'..'Z')
+ c.lowercaseChar() + substring(1)
+ else
+ this
+}
+
+private fun String.toLowerCaseAsciiOnly(): String {
+ val builder = StringBuilder(length)
+ for (c in this) {
+ builder.append(if (c in 'A'..'Z') c.lowercaseChar() else c)
+ }
+ return builder.toString()
+}
+
+private fun String.toUpperCaseAsciiOnly(): String {
+ val builder = StringBuilder(length)
+ for (c in this) {
+ builder.append(if (c in 'a'..'z') c.uppercaseChar() else c)
+ }
+ return builder.toString()
+}
+
+private fun isValidIdentifier(name: String): Boolean {
+ if (name.isEmpty() || name.startsWith("<")) return false
+ for (element in name) {
+ if (element == '.' || element == '/' || element == '\\') {
+ return false
+ }
+ }
+ return true
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiAccessorConventionUtil.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiAccessorConventionUtil.kt
new file mode 100644
index 00000000..1424244d
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiAccessorConventionUtil.kt
@@ -0,0 +1,98 @@
+package org.jetbrains.dokka.analysis.java.util
+
+import com.intellij.psi.PsiField
+import com.intellij.psi.PsiMethod
+import org.jetbrains.dokka.analysis.java.getVisibility
+import org.jetbrains.dokka.model.JavaVisibility
+import org.jetbrains.dokka.model.KotlinVisibility
+import org.jetbrains.dokka.model.Visibility
+
+
+internal data class PsiFunctionsHolder(
+ val regularFunctions: List<PsiMethod>,
+ val accessors: Map<PsiField, List<PsiMethod>>
+)
+
+internal fun splitFunctionsAndAccessors(fields: Array<PsiField>, methods: Array<PsiMethod>): PsiFunctionsHolder {
+ val fieldsByName = fields.associateBy { it.name }
+ val regularFunctions = mutableListOf<PsiMethod>()
+ val accessors = mutableMapOf<PsiField, MutableList<PsiMethod>>()
+ methods.forEach { method ->
+ val possiblePropertyNamesForFunction = method.getPossiblePropertyNamesForFunction()
+ val field = possiblePropertyNamesForFunction.firstNotNullOfOrNull { fieldsByName[it] }
+ if (field != null && method.isAccessorFor(field)) {
+ accessors.getOrPut(field, ::mutableListOf).add(method)
+ } else {
+ regularFunctions.add(method)
+ }
+ }
+
+ val accessorLookalikes = removeNonAccessorsReturning(accessors)
+ regularFunctions.addAll(accessorLookalikes)
+
+ return PsiFunctionsHolder(regularFunctions, accessors)
+}
+
+private fun PsiMethod.getPossiblePropertyNamesForFunction(): List<String> {
+ val jvmName = getAnnotation("kotlin.jvm.JvmName")?.findAttributeValue("name")?.text
+ if (jvmName != null) return listOf(jvmName)
+
+ return when {
+ isGetterName(name) -> listOfNotNull(
+ propertyNameByGetMethodName(name)
+ )
+ isSetterName(name) -> {
+ propertyNamesBySetMethodName(name)
+ }
+ else -> listOf()
+ }
+}
+
+private fun isGetterName(name: String): Boolean {
+ return name.startsWith("get") || name.startsWith("is")
+}
+
+private fun isSetterName(name: String): Boolean {
+ return name.startsWith("set")
+}
+
+/**
+ * If a field has no getter, it's not accessible as a property from Kotlin's perspective,
+ * but it still might have a setter. In this case, this "setter" should be just a regular function
+ */
+private fun removeNonAccessorsReturning(
+ fieldAccessors: MutableMap<PsiField, MutableList<PsiMethod>>
+): List<PsiMethod> {
+ val nonAccessors = mutableListOf<PsiMethod>()
+ fieldAccessors.entries.removeIf { (field, methods) ->
+ if (methods.size == 1 && methods[0].isSetterFor(field)) {
+ nonAccessors.add(methods[0])
+ true
+ } else {
+ false
+ }
+ }
+ return nonAccessors
+}
+
+internal fun PsiMethod.isAccessorFor(field: PsiField): Boolean {
+ return (this.isGetterFor(field) || this.isSetterFor(field))
+ && !field.getVisibility().isPublicAPI()
+ && this.getVisibility().isPublicAPI()
+}
+
+internal fun PsiMethod.isGetterFor(field: PsiField): Boolean {
+ return this.returnType == field.type && !this.hasParameters()
+}
+
+internal fun PsiMethod.isSetterFor(field: PsiField): Boolean {
+ return parameterList.getParameter(0)?.type == field.type && parameterList.getParametersCount() == 1
+}
+
+private fun Visibility.isPublicAPI() = when(this) {
+ KotlinVisibility.Public,
+ KotlinVisibility.Protected,
+ JavaVisibility.Public,
+ JavaVisibility.Protected -> true
+ else -> false
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiCommentsUtils.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiCommentsUtils.kt
new file mode 100644
index 00000000..10bb79c7
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiCommentsUtils.kt
@@ -0,0 +1,49 @@
+package org.jetbrains.dokka.analysis.java.util
+
+import com.intellij.psi.JavaDocTokenType
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiJavaCodeReferenceElement
+import com.intellij.psi.PsiWhiteSpace
+import com.intellij.psi.impl.source.tree.JavaDocElementType
+import com.intellij.psi.javadoc.PsiDocComment
+import com.intellij.psi.javadoc.PsiDocTag
+import com.intellij.psi.javadoc.PsiDocToken
+import com.intellij.psi.util.PsiTreeUtil
+import org.jetbrains.dokka.analysis.java.DescriptionJavadocTag
+import org.jetbrains.dokka.analysis.java.JavadocTag
+
+internal fun PsiDocComment.hasTag(tag: JavadocTag): Boolean =
+ when (tag) {
+ DescriptionJavadocTag -> descriptionElements.isNotEmpty()
+ else -> findTagByName(tag.name) != null
+ }
+
+internal fun PsiDocTag.contentElementsWithSiblingIfNeeded(): List<PsiElement> = if (dataElements.isNotEmpty()) {
+ listOfNotNull(
+ dataElements[0],
+ dataElements[0].nextSibling?.takeIf { it.text != dataElements.drop(1).firstOrNull()?.text },
+ *dataElements.drop(1).toTypedArray()
+ )
+} else {
+ emptyList()
+}
+
+internal fun PsiDocTag.resolveToElement(): PsiElement? =
+ dataElements.firstOrNull()?.firstChild?.referenceElementOrSelf()?.resolveToGetDri()
+
+internal fun PsiDocTag.referenceElement(): PsiElement? =
+ linkElement()?.referenceElementOrSelf()
+
+internal fun PsiElement.referenceElementOrSelf(): PsiElement? =
+ if (node.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER) {
+ PsiTreeUtil.findChildOfType(this, PsiJavaCodeReferenceElement::class.java)
+ } else this
+
+internal fun PsiDocTag.linkElement(): PsiElement? =
+ valueElement ?: dataElements.firstOrNull { it !is PsiWhiteSpace }
+
+internal fun PsiElement.defaultLabel() = children.firstOrNull {
+ it is PsiDocToken && it.text.isNotBlank() && !it.isSharpToken()
+} ?: this
+
+internal fun PsiDocToken.isSharpToken() = tokenType == JavaDocTokenType.DOC_TAG_VALUE_SHARP_TOKEN
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiUtil.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiUtil.kt
new file mode 100644
index 00000000..ed58eb56
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiUtil.kt
@@ -0,0 +1,119 @@
+package org.jetbrains.dokka.analysis.java.util
+
+import com.intellij.psi.*
+import com.intellij.psi.util.PsiTreeUtil
+import org.jetbrains.dokka.InternalDokkaApi
+import org.jetbrains.dokka.links.*
+import org.jetbrains.dokka.model.DocumentableSource
+import org.jetbrains.dokka.utilities.firstIsInstanceOrNull
+
+// TODO [beresnev] copy-paste
+
+internal val PsiElement.parentsWithSelf: Sequence<PsiElement>
+ get() = generateSequence(this) { if (it is PsiFile) null else it.parent }
+
+internal fun DRI.Companion.from(psi: PsiElement) = psi.parentsWithSelf.run {
+ val psiMethod = firstIsInstanceOrNull<PsiMethod>()
+ val psiField = firstIsInstanceOrNull<PsiField>()
+ val classes = filterIsInstance<PsiClass>().filterNot { it is PsiTypeParameter }
+ .toList() // We only want exact PsiClass types, not PsiTypeParameter subtype
+ val additionalClasses = if (psi is PsiEnumConstant) listOfNotNull(psiField?.name) else emptyList()
+ DRI(
+ packageName = classes.lastOrNull()?.qualifiedName?.substringBeforeLast('.', "") ?: "",
+ classNames = (additionalClasses + classes.mapNotNull { it.name }).takeIf { it.isNotEmpty() }
+ ?.asReversed()?.joinToString("."),
+ // The fallback strategy test whether psi is not `PsiEnumConstant`. The reason behind this is that
+ // we need unified DRI for both Java and Kotlin enums, so we can link them properly and treat them alike.
+ // To achieve that, we append enum name to classNames list and leave the callable part set to null. For Kotlin enums
+ // it is by default, while for Java enums we have to explicitly test for that in this `takeUnless` condition.
+ callable = psiMethod?.let { Callable.from(it) } ?: psiField?.takeUnless { psi is PsiEnumConstant }?.let { Callable.from(it) },
+ target = DriTarget.from(psi),
+ extra = if (psi is PsiEnumConstant)
+ DRIExtraContainer().also { it[EnumEntryDRIExtra] = EnumEntryDRIExtra }.encode()
+ else null
+ )
+}
+
+internal fun Callable.Companion.from(psi: PsiMethod) = with(psi) {
+ Callable(
+ name,
+ null,
+ parameterList.parameters.map { param -> JavaClassReference(param.type.canonicalText) })
+}
+
+internal fun Callable.Companion.from(psi: PsiField): Callable {
+ return Callable(
+ name = psi.name,
+ receiver = null,
+ params = emptyList()
+ )
+}
+
+internal fun DriTarget.Companion.from(psi: PsiElement): DriTarget = psi.parentsWithSelf.run {
+ return when (psi) {
+ is PsiTypeParameter -> PointingToGenericParameters(psi.index)
+ else -> firstIsInstanceOrNull<PsiParameter>()?.let {
+ val callable = firstIsInstanceOrNull<PsiMethod>()
+ val params = (callable?.parameterList?.parameters).orEmpty()
+ PointingToCallableParameters(params.indexOf(it))
+ } ?: PointingToDeclaration
+ }
+}
+
+// TODO [beresnev] copy-paste
+internal fun PsiElement.siblings(forward: Boolean = true, withItself: Boolean = true): Sequence<PsiElement> {
+ return object : Sequence<PsiElement> {
+ override fun iterator(): Iterator<PsiElement> {
+ var next: PsiElement? = this@siblings
+ return object : Iterator<PsiElement> {
+ init {
+ if (!withItself) next()
+ }
+
+ override fun hasNext(): Boolean = next != null
+ override fun next(): PsiElement {
+ val result = next ?: throw NoSuchElementException()
+ next = if (forward) result.nextSibling else result.prevSibling
+ return result
+ }
+ }
+ }
+ }
+}
+
+// TODO [beresnev] copy-paste
+internal fun PsiElement.getNextSiblingIgnoringWhitespace(withItself: Boolean = false): PsiElement? {
+ return siblings(withItself = withItself).filter { it !is PsiWhiteSpace }.firstOrNull()
+}
+
+@InternalDokkaApi
+class PsiDocumentableSource(val psi: PsiNamedElement) : DocumentableSource {
+ override val path = psi.containingFile.virtualFile.path
+
+ override fun computeLineNumber(): Int? {
+ val range = psi.getChildOfType<PsiIdentifier>()?.textRange ?: psi.textRange
+ val doc = PsiDocumentManager.getInstance(psi.project).getDocument(psi.containingFile)
+ // IJ uses 0-based line-numbers; external source browsers use 1-based
+ return doc?.getLineNumber(range.startOffset)?.plus(1)
+ }
+}
+
+inline fun <reified T : PsiElement> PsiElement.getChildOfType(): T? {
+ return PsiTreeUtil.getChildOfType(this, T::class.java)
+}
+
+internal fun PsiElement.getKotlinFqName(): String? = this.kotlinFqNameProp
+
+//// from import org.jetbrains.kotlin.idea.base.psi.kotlinFqName
+internal val PsiElement.kotlinFqNameProp: String?
+ get() = when (val element = this) {
+ is PsiPackage -> element.qualifiedName
+ is PsiClass -> element.qualifiedName
+ is PsiMember -> element.name?.let { name ->
+ val prefix = element.containingClass?.qualifiedName
+ if (prefix != null) "$prefix.$name" else name
+ }
+// is KtNamedDeclaration -> element.fqName TODO [beresnev] decide what to do with it
+ is PsiQualifiedNamedElement -> element.qualifiedName
+ else -> null
+ }
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/StdlibUtil.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/StdlibUtil.kt
new file mode 100644
index 00000000..cce76ce6
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/StdlibUtil.kt
@@ -0,0 +1,33 @@
+package org.jetbrains.dokka.analysis.java.util
+
+import java.util.*
+
+// TODO [beresnev] copy-paste
+
+// TODO [beresnev] remove this copy-paste and use the same method from stdlib instead after updating to 1.5
+internal fun <T, R : Any> Iterable<T>.firstNotNullOfOrNull(transform: (T) -> R?): R? {
+ for (element in this) {
+ val result = transform(element)
+ if (result != null) {
+ return result
+ }
+ }
+ return null
+}
+
+// TODO [beresnev] remove this copy-paste and use the same method from stdlib instead after updating to 1.5
+internal fun Char.uppercaseChar(): Char = Character.toUpperCase(this)
+
+// TODO [beresnev] remove this copy-paste and use the same method from stdlib instead after updating to 1.5
+internal fun Char.lowercaseChar(): Char = Character.toLowerCase(this)
+
+// TODO [beresnev] remove this copy-paste and use the same method from stdlib instead after updating to 1.5
+internal fun String.lowercase(): String = this.toLowerCase(Locale.ROOT)
+
+// TODO [beresnev] remove this copy-paste and use the same method from stdlib instead after updating to 1.5
+internal fun String.uppercase(): String = this.toUpperCase(Locale.ROOT)
+
+// TODO [beresnev] remove this copy-paste and use the same method from stdlib instead after updating to 1.5
+internal fun String.replaceFirstChar(transform: (Char) -> Char): String {
+ return if (isNotEmpty()) transform(this[0]) + substring(1) else this
+}
diff --git a/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/resolveToGetDri.kt b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/resolveToGetDri.kt
new file mode 100644
index 00000000..2972c009
--- /dev/null
+++ b/subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/resolveToGetDri.kt
@@ -0,0 +1,7 @@
+package org.jetbrains.dokka.analysis.java.util
+
+import com.intellij.psi.PsiElement
+
+// TODO [beresnev] get rid of
+internal fun PsiElement.resolveToGetDri(): PsiElement? =
+ reference?.resolve()