From 8e5c63d035ef44a269b8c43430f43f5c8eebfb63 Mon Sep 17 00:00:00 2001 From: Ignat Beresnev Date: Fri, 10 Nov 2023 11:46:54 +0100 Subject: Restructure the project to utilize included builds (#3174) * Refactor and simplify artifact publishing * Update Gradle to 8.4 * Refactor and simplify convention plugins and build scripts Fixes #3132 --------- Co-authored-by: Adam <897017+aSemy@users.noreply.github.com> Co-authored-by: Oleg Yukhnevich --- .../java/DefaultPsiToDocumentableTranslator.kt | 87 +++ .../dokka/analysis/java/JavaAnalysisPlugin.kt | 110 +++ .../jetbrains/dokka/analysis/java/JavadocTag.kt | 54 ++ .../java/SynheticElementDocumentationProvider.kt | 46 ++ .../dokka/analysis/java/doccomment/DocComment.kt | 18 + .../analysis/java/doccomment/DocCommentCreator.kt | 13 + .../analysis/java/doccomment/DocCommentFactory.kt | 24 + .../analysis/java/doccomment/DocCommentFinder.kt | 68 ++ .../java/doccomment/DocumentationContent.kt | 15 + .../analysis/java/doccomment/JavaDocComment.kt | 88 +++ .../java/doccomment/JavaDocCommentCreator.kt | 15 + .../java/doccomment/PsiDocumentationContent.kt | 26 + .../java/parsers/CommentResolutionContext.kt | 13 + .../analysis/java/parsers/DocCommentParser.kt | 16 + .../dokka/analysis/java/parsers/DokkaPsiParser.kt | 803 +++++++++++++++++++++ .../analysis/java/parsers/JavaDocCommentParser.kt | 232 ++++++ .../dokka/analysis/java/parsers/JavadocParser.kt | 28 + .../java/parsers/doctag/DocTagParserContext.kt | 51 ++ .../java/parsers/doctag/HtmlToDocTagConverter.kt | 118 +++ .../parsers/doctag/InheritDocTagContentProvider.kt | 14 + .../java/parsers/doctag/InheritDocTagResolver.kt | 118 +++ .../java/parsers/doctag/PsiDocTagParser.kt | 43 ++ .../parsers/doctag/PsiElementToHtmlConverter.kt | 218 ++++++ .../dokka/analysis/java/util/CoreCopyPaste.kt | 24 + .../dokka/analysis/java/util/NoopIntellijLogger.kt | 47 ++ .../analysis/java/util/PropertiesConventionUtil.kt | 105 +++ .../java/util/PsiAccessorConventionUtil.kt | 102 +++ .../dokka/analysis/java/util/PsiCommentsUtils.kt | 53 ++ .../jetbrains/dokka/analysis/java/util/PsiUtil.kt | 126 ++++ .../dokka/analysis/java/util/StdlibUtil.kt | 37 + .../dokka/analysis/java/util/resolveToGetDri.kt | 11 + .../org.jetbrains.dokka.plugability.DokkaPlugin | 5 + 32 files changed, 2728 insertions(+) create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/DefaultPsiToDocumentableTranslator.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavaAnalysisPlugin.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavadocTag.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/SynheticElementDocumentationProvider.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocComment.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentCreator.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFactory.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFinder.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocumentationContent.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocComment.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocCommentCreator.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/PsiDocumentationContent.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/CommentResolutionContext.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DocCommentParser.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DokkaPsiParser.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavaDocCommentParser.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavadocParser.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/DocTagParserContext.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/HtmlToDocTagConverter.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagContentProvider.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagResolver.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/PsiDocTagParser.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/PsiElementToHtmlConverter.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/CoreCopyPaste.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/NoopIntellijLogger.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PropertiesConventionUtil.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiAccessorConventionUtil.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiCommentsUtils.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/PsiUtil.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/StdlibUtil.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/util/resolveToGetDri.kt create mode 100644 dokka-subprojects/analysis-java-psi/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin (limited to 'dokka-subprojects/analysis-java-psi/src') diff --git a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/DefaultPsiToDocumentableTranslator.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/DefaultPsiToDocumentableTranslator.kt new file mode 100644 index 00000000..3b8ff25b --- /dev/null +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/DefaultPsiToDocumentableTranslator.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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().querySingle { projectProvider } + val project = projectProvider.getProject(sourceSet, context) + + val sourceRootsExtractor = context.plugin().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) -> + docParser.parsePackage(packageName, psiFiles) + }, + documentation = emptyMap(), + expectPresentInSet = null, + sourceSets = setOf(sourceSet) + ) + } + } + + private fun createPsiParser(sourceSet: DokkaSourceSet, context: DokkaContext): DokkaPsiParser { + val projectProvider = context.plugin().querySingle { projectProvider } + val docCommentParsers = context.plugin().query { docCommentParsers } + return DokkaPsiParser( + sourceSetData = sourceSet, + project = projectProvider.getProject(sourceSet, context), + logger = context.logger, + javadocParser = JavadocParser( + docCommentParsers = docCommentParsers, + docCommentFinder = context.plugin().docCommentFinder + ), + javaPsiDocCommentParser = docCommentParsers.single { it is JavaPsiDocCommentParser } as JavaPsiDocCommentParser, + lightMethodChecker = context.plugin().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/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavaAnalysisPlugin.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavaAnalysisPlugin.kt new file mode 100644 index 00000000..9426adf1 --- /dev/null +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavaAnalysisPlugin.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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 +public interface ProjectProvider { + public fun getProject(sourceSet: DokkaSourceSet, context: DokkaContext): Project +} + +@InternalDokkaApi +public interface SourceRootsExtractor { + public fun extract(sourceSet: DokkaSourceSet, context: DokkaContext): List +} + +@InternalDokkaApi +public interface BreakingAbstractionKotlinLightMethodChecker { + // TODO [beresnev] not even sure it's needed, but left for compatibility and to preserve behaviour + public fun isLightAnnotation(annotation: PsiAnnotation): Boolean + public fun isLightAnnotationAttribute(attribute: JvmAnnotationAttribute): Boolean +} + +@InternalDokkaApi +public class JavaAnalysisPlugin : DokkaPlugin() { + + // single + public val projectProvider: ExtensionPoint by extensionPoint() + + // single + public val sourceRootsExtractor: ExtensionPoint by extensionPoint() + + // multiple + public val docCommentCreators: ExtensionPoint by extensionPoint() + + // multiple + public val docCommentParsers: ExtensionPoint by extensionPoint() + + // none or more + public val inheritDocTagContentProviders: ExtensionPoint by extensionPoint() + + // TODO [beresnev] figure out a better way depending on what it's used for + public val kotlinLightMethodChecker: ExtensionPoint by extensionPoint() + + private val docCommentFactory by lazy { + DocCommentFactory(query { docCommentCreators }.reversed()) + } + + public val docCommentFinder: 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/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavadocTag.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavadocTag.kt new file mode 100644 index 00000000..23aee764 --- /dev/null +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavadocTag.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.java + +import com.intellij.psi.PsiMethod +import org.jetbrains.dokka.InternalDokkaApi + +@InternalDokkaApi +public sealed class JavadocTag( + public val name: String +) + +public object AuthorJavadocTag : JavadocTag("author") +public object DeprecatedJavadocTag : JavadocTag("deprecated") +public object DescriptionJavadocTag : JavadocTag("description") +public object ReturnJavadocTag : JavadocTag("return") +public object SinceJavadocTag : JavadocTag("since") + +public class ParamJavadocTag( + public val method: PsiMethod, + public val paramName: String, + public val paramIndex: Int +) : JavadocTag(name) { + public companion object { + public const val name: String = "param" + } +} + +public class SeeJavadocTag( + public val qualifiedReference: String +) : JavadocTag(name) { + public companion object { + public const val name: String = "see" + } +} + +public sealed class ThrowingExceptionJavadocTag( + name: String, + public val exceptionQualifiedName: String? +) : JavadocTag(name) + +public class ThrowsJavadocTag(exceptionQualifiedName: String?) : ThrowingExceptionJavadocTag(name, exceptionQualifiedName) { + public companion object { + public const val name: String = "throws" + } +} + +public class ExceptionJavadocTag(exceptionQualifiedName: String?) : ThrowingExceptionJavadocTag(name, exceptionQualifiedName) { + public companion object { + public const val name: String = "exception" + } +} diff --git a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/SynheticElementDocumentationProvider.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/SynheticElementDocumentationProvider.kt new file mode 100644 index 00000000..5b19a42d --- /dev/null +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/SynheticElementDocumentationProvider.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocComment.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocComment.kt new file mode 100644 index 00000000..be5f7eaa --- /dev/null +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocComment.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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 +public interface DocComment { + public fun hasTag(tag: JavadocTag): Boolean + + public fun resolveTag(tag: JavadocTag): List +} diff --git a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentCreator.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentCreator.kt new file mode 100644 index 00000000..2e22c77c --- /dev/null +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentCreator.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.java.doccomment + +import com.intellij.psi.PsiNamedElement +import org.jetbrains.dokka.InternalDokkaApi + +@InternalDokkaApi +public interface DocCommentCreator { + public fun create(element: PsiNamedElement): DocComment? +} diff --git a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFactory.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFactory.kt new file mode 100644 index 00000000..4647d4d9 --- /dev/null +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFactory.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.java.doccomment + +import com.intellij.psi.PsiNamedElement +import org.jetbrains.dokka.InternalDokkaApi + +@InternalDokkaApi +public class DocCommentFactory( + private val docCommentCreators: List +) { + public fun fromElement(element: PsiNamedElement): DocComment? { + docCommentCreators.forEach { creator -> + val comment = creator.create(element) + if (comment != null) { + return comment + } + } + return null + } +} + diff --git a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFinder.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFinder.kt new file mode 100644 index 00000000..18463e0e --- /dev/null +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocCommentFinder.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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 +public class DocCommentFinder( + private val logger: DokkaLogger, + private val docCommentFactory: DocCommentFactory, +) { + public 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() + .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/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocumentationContent.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocumentationContent.kt new file mode 100644 index 00000000..64e9ceb8 --- /dev/null +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/DocumentationContent.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.java.doccomment + +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.analysis.java.JavadocTag + +@InternalDokkaApi +public interface DocumentationContent { + public val tag: JavadocTag + + public fun resolveSiblings(): List +} diff --git a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocComment.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocComment.kt new file mode 100644 index 00000000..066b5162 --- /dev/null +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocComment.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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() + ?.resolveToElement() + ?.getKotlinFqName() == tag.exceptionQualifiedName + + override fun resolveTag(tag: JavadocTag): List { + 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 { + val resolvedParamElements = comment.resolveTag(tag) + .filterIsInstance() + .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 { + 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 { + return when (tag) { + DescriptionJavadocTag -> this.descriptionElements.toList() + else -> this.findTagsByName(tag.name).toList() + } + } + + private fun List.withoutReferenceLink(): List = 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/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocCommentCreator.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocCommentCreator.kt new file mode 100644 index 00000000..79da0ee2 --- /dev/null +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocCommentCreator.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/PsiDocumentationContent.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/PsiDocumentationContent.kt new file mode 100644 index 00000000..aa1fe120 --- /dev/null +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/PsiDocumentationContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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 { + return if (psiElement is PsiDocTag) { + psiElement.contentElementsWithSiblingIfNeeded() + .map { content -> PsiDocumentationContent(content, tag) } + } else { + listOf(this) + } + } + +} diff --git a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/CommentResolutionContext.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/CommentResolutionContext.kt new file mode 100644 index 00000000..1cd7d604 --- /dev/null +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/CommentResolutionContext.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DocCommentParser.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DocCommentParser.kt new file mode 100644 index 00000000..4a381e80 --- /dev/null +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DocCommentParser.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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 +public interface DocCommentParser { + public fun canParse(docComment: DocComment): Boolean + public fun parse(docComment: DocComment, context: PsiNamedElement): DocumentationNode +} diff --git a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DokkaPsiParser.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DokkaPsiParser.kt new file mode 100644 index 00000000..b8dabe42 --- /dev/null +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/DokkaPsiParser.kt @@ -0,0 +1,803 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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() + + 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.toSourceSetDependent() = mapOf(sourceSetData to this) + + suspend fun parsePackage(packageName: String, psiFiles: List): 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() + val superMethods = mutableListOf>() + val superFieldsKeys = hashSetOf() + val superFields = mutableListOf>() + 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.getSuperTypesPsiClasses(): List> { + 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().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 { + 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 { + 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 { + 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.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.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 PsiTypeParameter.annotations(): PropertyContainer = this.annotations.toList().toListOfAnnotations().annotations() + private fun PsiType.annotations(): PropertyContainer = this.annotations.toList().toListOfAnnotations().annotations() + + private fun List.annotations(): PropertyContainer = + 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) -> 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.isExtends -> Covariance(getBound(type.extendsBound)) + type.isSuper -> Contravariance(getBound(type.superBound)) + // If the type isn't explicitly bounded, it still has an implicit `extends Object` bound + type.extendsBound != PsiType.NULL -> Covariance(getBound(type.extendsBound)) + 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 { + fun mapBounds(bounds: Array): List = + if (bounds.isEmpty()) emptyList() else bounds.mapNotNull { + (it as? PsiClassType)?.let { classType -> Nullable(getBound(classType)) } + } + return typeParame