diff options
author | Ignat Beresnev <ignat.beresnev@jetbrains.com> | 2023-07-05 10:04:55 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-07-05 10:04:55 +0200 |
commit | 9559158bfeeb274e9ccf1b4563f1b23b42afc493 (patch) | |
tree | 3ece0887623cfe2b7148af23001867a1dd5e6597 /plugins/base/src/main/kotlin/translators/psi | |
parent | cbd9733d3dd2f52992e98e7cebd072091a572529 (diff) | |
download | dokka-9559158bfeeb274e9ccf1b4563f1b23b42afc493.tar.gz dokka-9559158bfeeb274e9ccf1b4563f1b23b42afc493.tar.bz2 dokka-9559158bfeeb274e9ccf1b4563f1b23b42afc493.zip |
Decompose Kotlin/Java analysis (#3034)
* Extract analysis into separate modules
Diffstat (limited to 'plugins/base/src/main/kotlin/translators/psi')
9 files changed, 0 insertions, 1875 deletions
diff --git a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt deleted file mode 100644 index c410104c..00000000 --- a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt +++ /dev/null @@ -1,863 +0,0 @@ -package org.jetbrains.dokka.base.translators.psi - -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.vfs.VirtualFileManager -import com.intellij.psi.* -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet -import org.jetbrains.dokka.analysis.DokkaResolutionFacade -import org.jetbrains.dokka.analysis.KotlinAnalysis -import org.jetbrains.dokka.analysis.PsiDocumentableSource -import org.jetbrains.dokka.analysis.from -import org.jetbrains.dokka.base.DokkaBase -import org.jetbrains.dokka.base.translators.psi.parsers.JavadocParser -import org.jetbrains.dokka.base.translators.typeConstructorsBeingExceptions -import org.jetbrains.dokka.base.translators.unquotedValue -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.plugability.DokkaContext -import org.jetbrains.dokka.plugability.plugin -import org.jetbrains.dokka.plugability.querySingle -import org.jetbrains.dokka.transformers.sources.AsyncSourceToDocumentableTranslator -import org.jetbrains.dokka.utilities.DokkaLogger -import org.jetbrains.dokka.utilities.parallelForEach -import org.jetbrains.dokka.utilities.parallelMap -import org.jetbrains.dokka.utilities.parallelMapNotNull -import org.jetbrains.kotlin.asJava.elements.KtLightAbstractAnnotation -import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys -import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot -import org.jetbrains.kotlin.idea.base.utils.fqname.getKotlinFqName -import org.jetbrains.kotlin.psi.psiUtil.getChildOfType -import org.jetbrains.kotlin.utils.KotlinExceptionWithAttachments -import org.jetbrains.kotlin.utils.addToStdlib.safeAs -import java.io.File - -class DefaultPsiToDocumentableTranslator( - context: DokkaContext, -) : AsyncSourceToDocumentableTranslator { - - private val kotlinAnalysis: KotlinAnalysis = context.plugin<DokkaBase>().querySingle { kotlinAnalysis } - - override suspend fun invokeSuspending(sourceSet: DokkaSourceSet, context: DokkaContext): DModule { - return coroutineScope { - fun isFileInSourceRoots(file: File): Boolean = - sourceSet.sourceRoots.any { root -> file.startsWith(root) } - - - val (environment, facade) = kotlinAnalysis[sourceSet] - - val sourceRoots = environment.configuration.get(CLIConfigurationKeys.CONTENT_ROOTS) - ?.filterIsInstance<JavaSourceRoot>() - ?.mapNotNull { it.file.takeIf(::isFileInSourceRoots) } - ?: listOf() - val localFileSystem = VirtualFileManager.getInstance().getFileSystem("file") - - val psiFiles = sourceRoots.parallelMap { sourceRoot -> - sourceRoot.absoluteFile.walkTopDown().mapNotNull { - localFileSystem.findFileByPath(it.path)?.let { vFile -> - PsiManager.getInstance(environment.project).findFile(vFile) as? PsiJavaFile - } - }.toList() - }.flatten() - - val docParser = - DokkaPsiParser( - sourceSet, - facade, - context.logger - ) - - DModule( - name = context.configuration.moduleName, - packages = psiFiles.parallelMapNotNull { it.safeAs<PsiJavaFile>() }.groupBy { it.packageName }.toList() - .parallelMap { (packageName: String, psiFiles: List<PsiJavaFile>) -> - docParser.parsePackage(packageName, psiFiles) - }, - documentation = emptyMap(), - expectPresentInSet = null, - sourceSets = setOf(sourceSet) - ) - } - } - - class DokkaPsiParser( - private val sourceSetData: DokkaSourceSet, - private val facade: DokkaResolutionFacade, - private val logger: DokkaLogger, - ) { - private val javadocParser = JavadocParser(logger, facade) - private val syntheticDocProvider = SyntheticElementDocumentationProvider(javadocParser, facade) - - 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 - */ - 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(facade.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 { it !is KtLightAbstractAnnotation }.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(unquotedValue(annotationValue.value)) - 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: KotlinExceptionWithAttachments) { - logger.warn("Failed to get annotations from ${this.getKotlinFqName()}") - null - } - } - - return psiReference?.let { psiElement -> - Annotations.Annotation( - dri = DRI.from(psiElement), - params = attributes - .filter { it !is KtLightAbstractAnnotation } - .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() - } -} - -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/plugins/base/src/main/kotlin/translators/psi/PsiAccessorConventionUtil.kt b/plugins/base/src/main/kotlin/translators/psi/PsiAccessorConventionUtil.kt deleted file mode 100644 index 3c1cb2cf..00000000 --- a/plugins/base/src/main/kotlin/translators/psi/PsiAccessorConventionUtil.kt +++ /dev/null @@ -1,94 +0,0 @@ -package org.jetbrains.dokka.base.translators.psi - -import com.intellij.psi.PsiField -import com.intellij.psi.PsiMethod -import org.jetbrains.dokka.base.translators.firstNotNullOfOrNull -import org.jetbrains.dokka.model.JavaVisibility -import org.jetbrains.dokka.model.KotlinVisibility -import org.jetbrains.dokka.model.Visibility -import org.jetbrains.kotlin.load.java.JvmAbi -import org.jetbrains.kotlin.load.java.propertyNameByGetMethodName -import org.jetbrains.kotlin.load.java.propertyNamesBySetMethodName -import org.jetbrains.kotlin.name.Name -import org.jetbrains.kotlin.resolve.DescriptorUtils - - -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) -} - -/** - * 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.getPossiblePropertyNamesForFunction(): List<String> { - val jvmName = getAnnotation(DescriptorUtils.JVM_NAME.asString())?.findAttributeValue("name")?.text - return jvmName?.let { listOf(jvmName) } - ?: when { - JvmAbi.isGetterName(name) -> listOfNotNull( - propertyNameByGetMethodName(Name.identifier(name))?.asString() - ) - JvmAbi.isSetterName(name) -> { - propertyNamesBySetMethodName(Name.identifier(name)).map { it.asString() } - } - else -> listOf() - } -} - -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 -} - -internal fun Visibility.isPublicAPI() = when(this) { - KotlinVisibility.Public, - KotlinVisibility.Protected, - JavaVisibility.Public, - JavaVisibility.Protected -> true - else -> false -} diff --git a/plugins/base/src/main/kotlin/translators/psi/PsiInheritance.kt b/plugins/base/src/main/kotlin/translators/psi/PsiInheritance.kt deleted file mode 100644 index 1eca489e..00000000 --- a/plugins/base/src/main/kotlin/translators/psi/PsiInheritance.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.jetbrains.dokka.base.translators.psi - -import com.intellij.psi.PsiClass -import com.intellij.psi.PsiMethod -import org.jetbrains.dokka.utilities.DokkaLogger -import org.jetbrains.kotlin.idea.base.utils.fqname.getKotlinFqName -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.utils.addToStdlib.safeAs - -internal fun PsiClass.implementsInterface(fqName: FqName): Boolean { - return allInterfaces().any { it.getKotlinFqName() == fqName } -} - -internal fun PsiClass.allInterfaces(): Sequence<PsiClass> { - return sequence { - this.yieldAll(interfaces.toList()) - interfaces.forEach { yieldAll(it.allInterfaces()) } - } -} - -/** - * Workaround for failing [PsiMethod.findSuperMethods]. - * This might be resolved once ultra light classes are enabled for dokka - * See [KT-39518](https://youtrack.jetbrains.com/issue/KT-39518) - */ -internal fun PsiMethod.findSuperMethodsOrEmptyArray(logger: DokkaLogger): Array<PsiMethod> { - return try { - /* - We are not even attempting to call "findSuperMethods" on all methods called "getGetter" or "getSetter" - on any object implementing "kotlin.reflect.KProperty", since we know that those methods will fail - (KT-39518). Just catching the exception is not good enough, since "findSuperMethods" will - print the whole exception to stderr internally and then spoil the console. - */ - val kPropertyFqName = FqName("kotlin.reflect.KProperty") - if ( - this.parent?.safeAs<PsiClass>()?.implementsInterface(kPropertyFqName) == true && - (this.name == "getSetter" || this.name == "getGetter") - ) { - logger.warn("Skipped lookup of super methods for ${getKotlinFqName()} (KT-39518)") - return emptyArray() - } - findSuperMethods() - } catch (exception: Throwable) { - logger.warn("Failed to lookup of super methods for ${getKotlinFqName()} (KT-39518)") - emptyArray() - } -}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/translators/psi/SynheticElementDocumentationProvider.kt b/plugins/base/src/main/kotlin/translators/psi/SynheticElementDocumentationProvider.kt deleted file mode 100644 index 376c0940..00000000 --- a/plugins/base/src/main/kotlin/translators/psi/SynheticElementDocumentationProvider.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.jetbrains.dokka.base.translators.psi - -import com.intellij.psi.* -import com.intellij.psi.javadoc.PsiDocComment -import org.jetbrains.dokka.analysis.DokkaResolutionFacade -import org.jetbrains.dokka.base.translators.psi.parsers.JavadocParser -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: JavadocParser, - private val resolutionFacade: DokkaResolutionFacade -) { - 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.parseDocComment(docComment, psiElement) - } - - private fun loadSyntheticDoc(path: String): PsiDocComment? { - val text = javaClass.getResource(path)?.readText() ?: return null - return JavaPsiFacade.getElementFactory(resolutionFacade.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/plugins/base/src/main/kotlin/translators/psi/parsers/InheritDocResolver.kt b/plugins/base/src/main/kotlin/translators/psi/parsers/InheritDocResolver.kt index e7f8c9ec..e69de29b 100644 --- a/plugins/base/src/main/kotlin/translators/psi/parsers/InheritDocResolver.kt +++ b/plugins/base/src/main/kotlin/translators/psi/parsers/InheritDocResolver.kt @@ -1,129 +0,0 @@ -package org.jetbrains.dokka.base.translators.psi.parsers - -import com.intellij.psi.PsiClass -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiMethod -import com.intellij.psi.javadoc.PsiDocComment -import com.intellij.psi.javadoc.PsiDocTag -import org.jetbrains.dokka.utilities.DokkaLogger - -internal data class CommentResolutionContext( - val comment: PsiDocComment, - val tag: JavadocTag?, - val name: String? = null, - val parameterIndex: Int? = null, -) - -internal class InheritDocResolver( - private val logger: DokkaLogger -) { - internal fun resolveFromContext(context: CommentResolutionContext) = - when (context.tag) { - JavadocTag.THROWS, JavadocTag.EXCEPTION -> context.name?.let { name -> - resolveThrowsTag( - context.tag, - context.comment, - name - ) - } - JavadocTag.PARAM -> context.parameterIndex?.let { paramIndex -> - resolveParamTag( - context.comment, - paramIndex - ) - } - JavadocTag.DEPRECATED -> resolveGenericTag(context.comment, JavadocTag.DESCRIPTION) - JavadocTag.SEE -> emptyList() - else -> context.tag?.let { tag -> resolveGenericTag(context.comment, tag) } - } - - private fun resolveGenericTag(currentElement: PsiDocComment, tag: JavadocTag) = - when (val owner = currentElement.owner) { - is PsiClass -> lowestClassWithTag(owner, tag) - is PsiMethod -> lowestMethodWithTag(owner, tag) - else -> null - }?.tagsByName(tag)?.flatMap { - when { - it is PsiDocumentationContent && it.psiElement is PsiDocTag -> - it.psiElement.contentElementsWithSiblingIfNeeded() - .map { content -> PsiDocumentationContent(content, it.tag) } - else -> listOf(it) - } - }.orEmpty() - - /** - * Main resolution point for exception like tags - * - * This should be used only with [JavadocTag.EXCEPTION] or [JavadocTag.THROWS] as their resolution path should be the same - */ - private fun resolveThrowsTag( - tag: JavadocTag, - currentElement: PsiDocComment, - exceptionFqName: String - ): List<DocumentationContent> { - val closestDocs = (currentElement.owner as? PsiMethod)?.let { method -> lowestMethodsWithTag(method, tag) } - .orEmpty().firstOrNull { - findClosestDocComment(it, logger)?.hasTagWithExceptionOfType(tag, exceptionFqName) == true - } - - return when (closestDocs?.language?.id) { - "kotlin" -> closestDocs.toKdocComment()?.tagsByName(tag, exceptionFqName).orEmpty() - else -> closestDocs?.docComment?.tagsByName(tag)?.flatMap { - when (it) { - is PsiDocTag -> it.contentElementsWithSiblingIfNeeded() - else -> listOf(it) - } - }?.withoutReferenceLink().orEmpty().map { PsiDocumentationContent(it, tag) } - } - } - - private fun resolveParamTag( - currentElement: PsiDocComment, - parameterIndex: Int, - ): List<DocumentationContent> = - (currentElement.owner as? PsiMethod)?.let { method -> lowestMethodsWithTag(method, JavadocTag.PARAM) } - .orEmpty().flatMap { - if (parameterIndex >= it.parameterList.parametersCount || parameterIndex < 0) emptyList() - else { - val closestTag = findClosestDocComment(it, logger) - val hasTag = closestTag?.hasTag(JavadocTag.PARAM) - when { - hasTag != true -> emptyList() - closestTag is JavaDocComment -> resolveJavaParamTag(closestTag, parameterIndex, it) - .withoutReferenceLink().map { PsiDocumentationContent(it, JavadocTag.PARAM) } - closestTag is KotlinDocComment -> resolveKdocTag(closestTag, parameterIndex) - else -> emptyList() - } - } - } - - private fun resolveJavaParamTag(comment: JavaDocComment, parameterIndex: Int, method: PsiMethod) = - comment.comment.tagsByName(JavadocTag.PARAM) - .filterIsInstance<PsiDocTag>().map { it.contentElementsWithSiblingIfNeeded() }.firstOrNull { - it.firstOrNull()?.text == method.parameterList.parameters[parameterIndex].name - }.orEmpty() - - private fun resolveKdocTag(comment: KotlinDocComment, parameterIndex: Int): List<DocumentationContent> = - listOf(comment.tagsByName(JavadocTag.PARAM)[parameterIndex]) - - //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 { - findClosestDocComment(it, logger)?.takeIf { tag -> tag.hasTag(javadocTag) } ?: lowestClassWithTag( - it, - javadocTag - ) - } - - private fun lowestMethodWithTag( - baseMethod: PsiMethod, - javadocTag: JavadocTag, - ): DocComment? = - lowestMethodsWithTag(baseMethod, javadocTag).firstOrNull() - ?.let { it.docComment?.let { JavaDocComment(it) } ?: it.toKdocComment() } - - private fun lowestMethodsWithTag(baseMethod: PsiMethod, javadocTag: JavadocTag) = - baseMethod.findSuperMethods().filter { findClosestDocComment(it, logger)?.hasTag(javadocTag) == true } - - private fun List<PsiElement>.withoutReferenceLink(): List<PsiElement> = drop(1) -} diff --git a/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt b/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt index 59c6f702..e69de29b 100644 --- a/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt +++ b/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt @@ -1,511 +0,0 @@ -package org.jetbrains.dokka.base.translators.psi.parsers - -import com.intellij.lexer.JavaDocTokenTypes -import com.intellij.psi.* -import com.intellij.psi.impl.source.javadoc.PsiDocParamRef -import com.intellij.psi.impl.source.tree.JavaDocElementType -import com.intellij.psi.impl.source.tree.LazyParseablePsiElement -import com.intellij.psi.impl.source.tree.LeafPsiElement -import com.intellij.psi.javadoc.* -import org.intellij.markdown.MarkdownElementTypes -import org.jetbrains.dokka.analysis.DokkaResolutionFacade -import org.jetbrains.dokka.analysis.from -import org.jetbrains.dokka.base.parsers.MarkdownParser -import org.jetbrains.dokka.base.translators.parseHtmlEncodedWithNormalisedSpaces -import org.jetbrains.dokka.links.DRI -import org.jetbrains.dokka.model.doc.* -import org.jetbrains.dokka.model.doc.Deprecated -import org.jetbrains.dokka.utilities.DokkaLogger -import org.jetbrains.dokka.utilities.htmlEscape -import org.jetbrains.kotlin.idea.kdoc.resolveKDocLink -import org.jetbrains.kotlin.idea.base.utils.fqname.getKotlinFqName -import org.jetbrains.kotlin.idea.util.CommentSaver.Companion.tokenType -import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespace -import org.jetbrains.kotlin.psi.psiUtil.siblings -import org.jsoup.Jsoup -import org.jsoup.nodes.Comment -import org.jsoup.nodes.Element -import org.jsoup.nodes.Node -import org.jsoup.nodes.TextNode -import java.util.* - -fun interface JavaDocumentationParser { - fun parseDocumentation(element: PsiNamedElement): DocumentationNode -} - -class JavadocParser( - private val logger: DokkaLogger, - private val resolutionFacade: DokkaResolutionFacade, -) : JavaDocumentationParser { - private val inheritDocResolver = InheritDocResolver(logger) - - /** - * 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 var inheritDocSections: MutableMap<UUID, DocumentationNode> = mutableMapOf() - - override fun parseDocumentation(element: PsiNamedElement): DocumentationNode { - return when(val comment = findClosestDocComment(element, logger)){ - is JavaDocComment -> parseDocComment(comment.comment, element) - is KotlinDocComment -> parseDocumentation(comment) - else -> DocumentationNode(emptyList()) - } - } - - internal fun parseDocComment(docComment: PsiDocComment, context: PsiNamedElement): DocumentationNode { - val nodes = listOfNotNull(docComment.getDescription()) + docComment.tags.mapNotNull { tag -> - parseDocTag(tag, docComment, context) - } - return DocumentationNode(nodes) - } - - private fun parseDocumentation(element: KotlinDocComment, parseWithChildren: Boolean = true): DocumentationNode = - MarkdownParser.parseFromKDocTag( - kDocTag = element.comment, - externalDri = { link: String -> - try { - resolveKDocLink( - context = resolutionFacade.resolveSession.bindingContext, - resolutionFacade = resolutionFacade, - fromDescriptor = element.descriptor, - fromSubjectOfTag = null, - qualifiedName = link.split('.') - ).firstOrNull()?.let { DRI.from(it) } - } catch (e1: IllegalArgumentException) { - logger.warn("Couldn't resolve link for $link") - null - } - }, - kdocLocation = null, - parseWithChildren = parseWithChildren - ) - - private fun parseDocTag(tag: PsiDocTag, docComment: PsiDocComment, analysedElement: PsiNamedElement): TagWrapper { - val javadocTag = JavadocTag.lowercaseValueOfOrNull(tag.name) - if (javadocTag == null) { - return emptyTagWrapper(tag, docComment) - } - // Javadoc tag found - val resolutionContext = CommentResolutionContext(comment = docComment, tag = javadocTag) - return when (resolutionContext.tag) { - JavadocTag.PARAM -> { - val name = tag.dataElements.firstOrNull()?.text.orEmpty() - val index = - (analysedElement as? PsiMethod)?.parameterList?.parameters?.map { it.name }?.indexOf(name) - Param( - wrapTagIfNecessary( - convertJavadocElements( - tag.contentElementsWithSiblingIfNeeded().drop(1), - context = resolutionContext.copy(name = name, parameterIndex = index) - ) - ), - name - ) - } - - JavadocTag.THROWS, JavadocTag.EXCEPTION -> { - val resolved = tag.resolveToElement() - val dri = resolved?.let { DRI.from(it) } - val name = resolved?.getKotlinFqName()?.asString() - ?: tag.dataElements.firstOrNull()?.text.orEmpty() - Throws( - root = wrapTagIfNecessary( - convertJavadocElements( - tag.dataElements.drop(1), - context = resolutionContext.copy(name = name) - ) - ), - /* 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) - * */ - name = name, - exceptionAddress = dri - ) - } - - JavadocTag.RETURN -> Return( - wrapTagIfNecessary( - convertJavadocElements( - tag.contentElementsWithSiblingIfNeeded(), - context = resolutionContext - ) - ) - ) - - JavadocTag.AUTHOR -> Author( - wrapTagIfNecessary( - convertJavadocElements( - tag.contentElementsWithSiblingIfNeeded(), - context = resolutionContext - ) - ) - ) // 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 - JavadocTag.SEE -> { - val name = - tag.resolveToElement()?.getKotlinFqName()?.asString() ?: tag.referenceElement()?.text.orEmpty() - .removePrefix("#") - getSeeTagElementContent(tag, resolutionContext.copy(name = name)).let { - See( - wrapTagIfNecessary(it.first), - name, - it.second - ) - } - } - - JavadocTag.DEPRECATED -> Deprecated( - wrapTagIfNecessary( - convertJavadocElements( - tag.contentElementsWithSiblingIfNeeded(), - context = resolutionContext - ) - ) - ) - - JavadocTag.SINCE -> Since( - wrapTagIfNecessary( - convertJavadocElements( - tag.contentElementsWithSiblingIfNeeded(), - context = resolutionContext - ) - ) - ) - - else -> emptyTagWrapper(tag, docComment) - } - } - - // Wrapper for unsupported tags https://github.com/Kotlin/dokka/issues/1618 - private fun emptyTagWrapper( - tag: PsiDocTag, - docComment: PsiDocComment - ) = CustomTagWrapper( - wrapTagIfNecessary( - convertJavadocElements( - tag.contentElementsWithSiblingIfNeeded(), - context = CommentResolutionContext(docComment, null) - )), tag.name - ) - - private fun wrapTagIfNecessary(list: List<DocTag>): CustomDocTag = - if (list.size == 1 && (list.first() as? CustomDocTag)?.name == MarkdownElementTypes.MARKDOWN_FILE.name) - list.first() as CustomDocTag - else - CustomDocTag(list, name = MarkdownElementTypes.MARKDOWN_FILE.name) - - private fun getSeeTagElementContent( - tag: PsiDocTag, - context: CommentResolutionContext - ): Pair<List<DocTag>, DRI?> { - val referenceElement = tag.referenceElement() - val linkElement = referenceElement?.toDocumentationLink(context = context) - val content = convertJavadocElements( - tag.dataElements.dropWhile { it is PsiWhiteSpace || (it as? LazyParseablePsiElement)?.tokenType == JavaDocElementType.DOC_REFERENCE_HOLDER || it == referenceElement }, - context = context - ) - return Pair(content, linkElement?.dri) - } - - private fun PsiDocComment.getDescription(): Description? { - return convertJavadocElements( - descriptionElements.asIterable(), - context = CommentResolutionContext(this, JavadocTag.DESCRIPTION) - ).takeIf { it.isNotEmpty() }?.let { - Description(wrapTagIfNecessary(it)) - } - } - - private data class ParserState( - val currentJavadocTag: JavadocTag?, - val previousElement: PsiElement? = null, - val openPreTags: Int = 0, - val closedPreTags: Int = 0 - ) - - private data class ParsingResult(val newState: ParserState, val parsedLine: String? = null) { - constructor(tag: JavadocTag?) : this(ParserState(tag)) - - operator fun plus(other: ParsingResult): ParsingResult = - ParsingResult( - other.newState, - listOfNotNull(parsedLine, other.parsedLine).joinToString(separator = "") - ) - } - - private inner class Parse : (Iterable<PsiElement>, Boolean, CommentResolutionContext) -> List<DocTag> { - val driMap = mutableMapOf<String, DRI>() - - private fun PsiElement.stringify(state: ParserState, context: CommentResolutionContext): ParsingResult = - when (this) { - is PsiReference -> children.fold(ParsingResult(state)) { acc, e -> - acc + e.stringify(acc.newState, context) - } - else -> stringifySimpleElement(state, context) - } - - private fun DocumentationContent.stringify(state: ParserState, context: CommentResolutionContext): ParsingResult = - when(this){ - is PsiDocumentationContent -> psiElement.stringify(state, context) - is DescriptorDocumentationContent -> { - val id = UUID.randomUUID() - inheritDocSections[id] = parseDocumentation(KotlinDocComment(element, descriptor), parseWithChildren = false) - ParsingResult(state, """<inheritdoc id="$id"/>""") - } - else -> throw IllegalStateException("Unrecognised documentation content: $this") - } - - private fun PsiElement.stringifySimpleElement( - state: ParserState, - context: CommentResolutionContext - ): ParsingResult { - val openPre = state.openPreTags + "<pre(\\s+.*)?>".toRegex().findAll(text).toList().size - val closedPre = state.closedPreTags + "</pre>".toRegex().findAll(text).toList().size - val isInsidePre = openPre > closedPre - val parsed = when (this) { - is PsiInlineDocTag -> convertInlineDocTag(this, state.currentJavadocTag, context) - is PsiDocParamRef -> toDocumentationLinkString() - is PsiDocTagValue, - is LeafPsiElement -> stringifyElementAsText(isInsidePre, state.previousElement) - else -> null - } - val previousElement = if (text.trim() == "") state.previousElement else this - return ParsingResult( - state.copy( - previousElement = previousElement, - closedPreTags = closedPre, - openPreTags = openPre - ), parsed - ) - } - - 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 - } - } - - /** - * 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 - } - - private fun PsiElement.toDocumentationLinkString( - label: String = "" - ): String { - - val dri = reference?.resolve()?.takeIf { it !is PsiParameter }?.let { - val dri = DRI.from(it) - driMap[dri.toString()] = dri - dri.toString() - } ?: UNRESOLVED_PSI_ELEMENT - - return """<a data-dri="${dri.htmlEscape()}">${label.ifBlank{ defaultLabel().text }}</a>""" - } - - private fun convertInlineDocTag( - tag: PsiInlineDocTag, - javadocTag: JavadocTag?, - context: CommentResolutionContext - ) = - when (tag.name) { - "link", "linkplain" -> tag.referenceElement() - ?.toDocumentationLinkString(tag.dataElements.filterIsInstance<PsiDocToken>().joinToString(" ") { - it.stringifyElementAsText(keepFormatting = false).orEmpty() - }) - "code" -> "<code data-inline>${dataElementsAsText(tag)}</code>" - "literal" -> "<literal>${dataElementsAsText(tag)}</literal>" - "index" -> "<index>${tag.children.filterIsInstance<PsiDocTagValue>().joinToString { it.text }}</index>" - "inheritDoc" -> inheritDocResolver.resolveFromContext(context) - ?.fold(ParsingResult(javadocTag)) { result, e -> - result + e.stringify(result.newState, context) - }?.parsedLine.orEmpty() - else -> tag.text - } - - private fun dataElementsAsText(tag: PsiInlineDocTag) = - tag.dataElements.joinToString("") { - it.stringifyElementAsText(keepFormatting = true).orEmpty() - }.htmlEscape() - - 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") && driMap.containsKey(element.attr("data-dri")) -> - DocumentationLink(driMap[element.attr("data-dri")]!!, children) - else -> Text(body = children.filterIsInstance<Text>().joinToString { it.body }) - } - } - - 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" -> { - val id = UUID.fromString(element.attr("id")) - val section = inheritDocSections[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 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() - } - - override fun invoke( - elements: Iterable<PsiElement>, - asParagraph: Boolean, - context: CommentResolutionContext - ): List<DocTag> = - elements.fold(ParsingResult(context.tag)) { acc, e -> - acc + e.stringify(acc.newState, context) - }.parsedLine?.let { - val trimmed = it.trim() - val toParse = if (asParagraph) "<p>$trimmed</p>" else trimmed - Jsoup.parseBodyFragment(toParse).body().childNodes().flatMap { convertHtmlNode(it) } - }.orEmpty() - } - - private fun convertJavadocElements( - elements: Iterable<PsiElement>, - asParagraph: Boolean = true, - context: CommentResolutionContext - ): List<DocTag> = - Parse()(elements, asParagraph, context) - - private fun PsiDocToken.isSharpToken() = tokenType == JavaDocTokenType.DOC_TAG_VALUE_SHARP_TOKEN - - private fun PsiDocToken.isTagName() = tokenType == JavaDocTokenType.DOC_TAG_NAME - - private fun PsiDocToken.isLeadingAsterisk() = tokenType == JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS - - private fun PsiElement.toDocumentationLink(labelElement: PsiElement? = null, context: CommentResolutionContext) = - resolveToGetDri()?.let { - val dri = DRI.from(it) - val label = labelElement ?: defaultLabel() - DocumentationLink(dri, convertJavadocElements(listOfNotNull(label), asParagraph = false, context)) - } - - private fun PsiDocTag.referenceElement(): PsiElement? = - linkElement()?.referenceElementOrSelf() - - private fun PsiElement.defaultLabel() = children.firstOrNull { - it is PsiDocToken && it.text.isNotBlank() && !it.isSharpToken() - } ?: this - - private fun PsiDocTag.linkElement(): PsiElement? = - valueElement ?: dataElements.firstOrNull { it !is PsiWhiteSpace } - - companion object { - private const val UNRESOLVED_PSI_ELEMENT = "UNRESOLVED_PSI_ELEMENT" - } -} diff --git a/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocTag.kt b/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocTag.kt index 5b3be7e3..e69de29b 100644 --- a/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocTag.kt +++ b/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocTag.kt @@ -1,32 +0,0 @@ -package org.jetbrains.dokka.base.translators.psi.parsers - -internal enum class JavadocTag { - PARAM, THROWS, RETURN, AUTHOR, SEE, DEPRECATED, EXCEPTION, HIDE, SINCE, - - /** - * Artificial tag created to handle tag-less section - */ - DESCRIPTION,; - - override fun toString(): String = super.toString().toLowerCase() - - /* Missing tags: - SERIAL, - SERIAL_DATA, - SERIAL_FIELD, - SINCE, - VERSION - */ - - companion object { - private val name2Value = values().associateBy { it.name.toLowerCase() } - - /** - * Lowercase-based `Enum.valueOf` variation for [JavadocTag]. - * - * Note: tags are [case-sensitive](https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html) in Java, - * thus we are not allowed to use case-insensitive or uppercase-based lookup. - */ - fun lowercaseValueOfOrNull(name: String): JavadocTag? = name2Value[name] - } -} diff --git a/plugins/base/src/main/kotlin/translators/psi/parsers/PsiCommentsUtils.kt b/plugins/base/src/main/kotlin/translators/psi/parsers/PsiCommentsUtils.kt deleted file mode 100644 index c4c8cbb2..00000000 --- a/plugins/base/src/main/kotlin/translators/psi/parsers/PsiCommentsUtils.kt +++ /dev/null @@ -1,146 +0,0 @@ -package org.jetbrains.dokka.base.translators.psi.parsers - -import com.intellij.psi.* -import com.intellij.psi.javadoc.PsiDocComment -import com.intellij.psi.javadoc.PsiDocTag -import org.jetbrains.dokka.analysis.from -import org.jetbrains.dokka.base.translators.psi.findSuperMethodsOrEmptyArray -import org.jetbrains.dokka.links.DRI -import org.jetbrains.dokka.utilities.DokkaLogger -import org.jetbrains.kotlin.descriptors.DeclarationDescriptor -import org.jetbrains.kotlin.idea.kdoc.findKDoc -import org.jetbrains.kotlin.idea.base.utils.fqname.getKotlinFqName -import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor -import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag -import org.jetbrains.kotlin.psi.KtDeclaration -import org.jetbrains.kotlin.psi.KtElement -import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils -import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull - -internal interface DocComment { - fun hasTag(tag: JavadocTag): Boolean - fun hasTagWithExceptionOfType(tag: JavadocTag, exceptionFqName: String): Boolean - fun tagsByName(tag: JavadocTag, param: String? = null): List<DocumentationContent> -} - -internal data class JavaDocComment(val comment: PsiDocComment) : DocComment { - override fun hasTag(tag: JavadocTag): Boolean = comment.hasTag(tag) - override fun hasTagWithExceptionOfType(tag: JavadocTag, exceptionFqName: String): Boolean = - comment.hasTag(tag) && comment.tagsByName(tag).firstIsInstanceOrNull<PsiDocTag>() - ?.resolveToElement() - ?.getKotlinFqName()?.asString() == exceptionFqName - - override fun tagsByName(tag: JavadocTag, param: String?): List<DocumentationContent> = - comment.tagsByName(tag).map { PsiDocumentationContent(it, tag) } -} - -internal data class KotlinDocComment(val comment: KDocTag, val descriptor: DeclarationDescriptor) : DocComment { - override fun hasTag(tag: JavadocTag): Boolean = - when (tag) { - JavadocTag.DESCRIPTION -> comment.getContent().isNotEmpty() - else -> tagsWithContent.any { it.text.startsWith("@$tag") } - } - - override fun hasTagWithExceptionOfType(tag: JavadocTag, exceptionFqName: String): Boolean = - tagsWithContent.any { it.hasExceptionWithName(tag, exceptionFqName) } - - override fun tagsByName(tag: JavadocTag, param: String?): List<DocumentationContent> = - when (tag) { - JavadocTag.DESCRIPTION -> listOf(DescriptorDocumentationContent(descriptor, comment, tag)) - else -> comment.children.mapNotNull { (it as? KDocTag) } - .filter { it.name == "$tag" && param?.let { param -> it.hasExceptionWithName(param) } != false } - .map { DescriptorDocumentationContent(descriptor, it, tag) } - } - - private val tagsWithContent: List<KDocTag> = comment.children.mapNotNull { (it as? KDocTag) } - - private fun KDocTag.hasExceptionWithName(tag: JavadocTag, exceptionFqName: String) = - text.startsWith("@$tag") && hasExceptionWithName(exceptionFqName) - - private fun KDocTag.hasExceptionWithName(exceptionFqName: String) = - getSubjectName() == exceptionFqName -} - -internal interface DocumentationContent { - val tag: JavadocTag -} - -internal data class PsiDocumentationContent(val psiElement: PsiElement, override val tag: JavadocTag) : - DocumentationContent - -internal data class DescriptorDocumentationContent( - val descriptor: DeclarationDescriptor, - val element: KDocTag, - override val tag: JavadocTag -) : DocumentationContent - -internal fun PsiDocComment.hasTag(tag: JavadocTag): Boolean = - when (tag) { - JavadocTag.DESCRIPTION -> descriptionElements.isNotEmpty() - else -> findTagByName(tag.toString()) != null - } - -internal fun PsiDocComment.tagsByName(tag: JavadocTag): List<PsiElement> = - when (tag) { - JavadocTag.DESCRIPTION -> descriptionElements.toList() - else -> findTagsByName(tag.toString()).toList() - } - -internal fun findClosestDocComment(element: PsiNamedElement, logger: DokkaLogger): DocComment? { - (element as? PsiDocCommentOwner)?.docComment?.run { return JavaDocComment(this) } - element.toKdocComment()?.run { return this } - - if (element is PsiMethod) { - val superMethods = element.findSuperMethodsOrEmptyArray(logger) - if (superMethods.isEmpty()) return null - - if (superMethods.size == 1) { - return findClosestDocComment(superMethods.single(), logger) - } - - val superMethodDocumentation = superMethods.map { method -> findClosestDocComment(method, logger) }.distinct() - if (superMethodDocumentation.size == 1) { - return superMethodDocumentation.single() - } - - logger.debug( - "Conflicting documentation for ${DRI.from(element)}" + - "${superMethods.map { DRI.from(it) }}" - ) - - /* Prioritize super class over interface */ - val indexOfSuperClass = superMethods.indexOfFirst { method -> - val parent = method.parent - if (parent is PsiClass) !parent.isInterface - else false - } - - return if (indexOfSuperClass >= 0) superMethodDocumentation[indexOfSuperClass] - else superMethodDocumentation.first() - } - return element.children.firstIsInstanceOrNull<PsiDocComment>()?.let { JavaDocComment(it) } -} - -internal fun PsiNamedElement.toKdocComment(): KotlinDocComment? = - (navigationElement as? KtElement)?.findKDoc { DescriptorToSourceUtils.descriptorToDeclaration(it) } - ?.run { - (this@toKdocComment.navigationElement as? KtDeclaration)?.descriptor?.let { - KotlinDocComment( - this, - it - ) - } - } - -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() diff --git a/plugins/base/src/main/kotlin/translators/psi/parsers/exceptionTag.kt b/plugins/base/src/main/kotlin/translators/psi/parsers/exceptionTag.kt deleted file mode 100644 index 3cc16251..00000000 --- a/plugins/base/src/main/kotlin/translators/psi/parsers/exceptionTag.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.jetbrains.dokka.base.translators.psi.parsers - -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiJavaCodeReferenceElement -import com.intellij.psi.impl.source.tree.JavaDocElementType -import com.intellij.psi.util.PsiTreeUtil - -internal fun PsiElement.referenceElementOrSelf(): PsiElement? = - if (node.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER) { - PsiTreeUtil.findChildOfType(this, PsiJavaCodeReferenceElement::class.java) - } else this - -internal fun PsiElement.resolveToGetDri(): PsiElement? = - reference?.resolve()
\ No newline at end of file |