diff options
Diffstat (limited to 'plugins/base/src/main/kotlin/translators')
6 files changed, 2494 insertions, 0 deletions
diff --git a/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt new file mode 100644 index 00000000..ffceaaa7 --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt @@ -0,0 +1,782 @@ +package org.jetbrains.dokka.base.translators.descriptors + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.analysis.DescriptorDocumentableSource +import org.jetbrains.dokka.analysis.DokkaResolutionFacade +import org.jetbrains.dokka.analysis.KotlinAnalysis +import org.jetbrains.dokka.analysis.from +import org.jetbrains.dokka.base.parsers.MarkdownParser +import org.jetbrains.dokka.links.* +import org.jetbrains.dokka.links.Callable +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.Nullable +import org.jetbrains.dokka.model.TypeConstructor +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.sources.SourceToDocumentableTranslator +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.builtins.isExtensionFunctionType +import org.jetbrains.kotlin.builtins.isFunctionType +import org.jetbrains.kotlin.codegen.isJvmStaticInObjectOrClassOrInterface +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.descriptors.Visibility +import org.jetbrains.kotlin.descriptors.annotations.Annotated +import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor +import org.jetbrains.kotlin.descriptors.impl.DeclarationDescriptorVisitorEmptyBodies +import org.jetbrains.kotlin.idea.kdoc.findKDoc +import org.jetbrains.kotlin.load.kotlin.toSourceElement +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.calls.callUtil.getValueArgumentsInParentheses +import org.jetbrains.kotlin.resolve.calls.components.isVararg +import org.jetbrains.kotlin.resolve.constants.ConstantValue +import org.jetbrains.kotlin.resolve.constants.KClassValue.Value.LocalClass +import org.jetbrains.kotlin.resolve.constants.KClassValue.Value.NormalClass +import org.jetbrains.kotlin.resolve.descriptorUtil.annotationClass +import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassNotAny +import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperInterfaces +import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter +import org.jetbrains.kotlin.resolve.scopes.MemberScope +import org.jetbrains.kotlin.resolve.source.KotlinSourceElement +import org.jetbrains.kotlin.resolve.source.PsiSourceElement +import org.jetbrains.kotlin.types.DynamicType +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.TypeProjection +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull +import org.jetbrains.kotlin.utils.addToStdlib.safeAs +import java.nio.file.Paths +import org.jetbrains.kotlin.resolve.constants.AnnotationValue as ConstantsAnnotationValue +import org.jetbrains.kotlin.resolve.constants.ArrayValue as ConstantsArrayValue +import org.jetbrains.kotlin.resolve.constants.EnumValue as ConstantsEnumValue +import org.jetbrains.kotlin.resolve.constants.KClassValue as ConstantsKtClassValue + +class DefaultDescriptorToDocumentableTranslator( + private val kotlinAnalysis: KotlinAnalysis +) : SourceToDocumentableTranslator { + + override fun invoke(sourceSet: DokkaSourceSet, context: DokkaContext): DModule { + val (environment, facade) = kotlinAnalysis[sourceSet] + val packageFragments = environment.getSourceFiles().asSequence() + .map { it.packageFqName } + .distinct() + .mapNotNull { facade.resolveSession.getPackageFragment(it) } + .toList() + + return DokkaDescriptorVisitor(sourceSet, kotlinAnalysis[sourceSet].facade, context.logger).run { + packageFragments.mapNotNull { it.safeAs<PackageFragmentDescriptor>() }.map { + visitPackageFragmentDescriptor( + it, + DRIWithPlatformInfo(DRI.topLevel, emptyMap()) + ) + } + }.let { DModule(sourceSet.moduleDisplayName, it, emptyMap(), null, setOf(sourceSet)) } + } +} + +data class DRIWithPlatformInfo( + val dri: DRI, + val actual: SourceSetDependent<DocumentableSource> +) + +fun DRI.withEmptyInfo() = DRIWithPlatformInfo(this, emptyMap()) + +private class DokkaDescriptorVisitor( + private val sourceSet: DokkaSourceSet, + private val resolutionFacade: DokkaResolutionFacade, + private val logger: DokkaLogger +) : DeclarationDescriptorVisitorEmptyBodies<Documentable, DRIWithPlatformInfo>() { + override fun visitDeclarationDescriptor(descriptor: DeclarationDescriptor, parent: DRIWithPlatformInfo): Nothing { + throw IllegalStateException("${javaClass.simpleName} should never enter ${descriptor.javaClass.simpleName}") + } + + private fun Collection<DeclarationDescriptor>.filterDescriptorsInSourceSet() = filter { + it.toSourceElement.containingFile.toString().let { path -> + path.isNotBlank() && sourceSet.sourceRoots.any { root -> + Paths.get(path).startsWith(Paths.get(root.path)) + } + } + } + + private fun <T> T.toSourceSetDependent() = mapOf(sourceSet to this) + + override fun visitPackageFragmentDescriptor( + descriptor: PackageFragmentDescriptor, + parent: DRIWithPlatformInfo + ): DPackage { + val name = descriptor.fqName.asString().takeUnless { it.isBlank() } ?: fallbackPackageName() + val driWithPlatform = DRI(packageName = name).withEmptyInfo() + val scope = descriptor.getMemberScope() + + return DPackage( + dri = driWithPlatform.dri, + functions = scope.functions(driWithPlatform, true), + properties = scope.properties(driWithPlatform, true), + classlikes = scope.classlikes(driWithPlatform, true), + typealiases = scope.typealiases(driWithPlatform, true), + documentation = descriptor.resolveDescriptorData(), + sourceSets = setOf(sourceSet) + ) + } + + override fun visitClassDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DClasslike = + when (descriptor.kind) { + ClassKind.ENUM_CLASS -> enumDescriptor(descriptor, parent) + ClassKind.OBJECT -> objectDescriptor(descriptor, parent) + ClassKind.INTERFACE -> interfaceDescriptor(descriptor, parent) + ClassKind.ANNOTATION_CLASS -> annotationDescriptor(descriptor, parent) + else -> classDescriptor(descriptor, parent) + } + + private fun interfaceDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DInterface { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val scope = descriptor.unsubstitutedMemberScope + val isExpect = descriptor.isExpect + val info = descriptor.resolveClassDescriptionData() + + return DInterface( + dri = driWithPlatform.dri, + name = descriptor.name.asString(), + functions = scope.functions(driWithPlatform), + properties = scope.properties(driWithPlatform), + classlikes = scope.classlikes(driWithPlatform), + sources = descriptor.createSources(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + supertypes = info.supertypes.toSourceSetDependent(), + documentation = info.docs, + generics = descriptor.declaredTypeParameters.map { it.toTypeParameter() }, + companion = descriptor.companion(driWithPlatform), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + ImplementedInterfaces(info.allImplementedInterfaces.toSourceSetDependent()) + ) + ) + } + + private fun objectDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DObject { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val scope = descriptor.unsubstitutedMemberScope + val isExpect = descriptor.isExpect + val info = descriptor.resolveClassDescriptionData() + + + return DObject( + dri = driWithPlatform.dri, + name = descriptor.name.asString(), + functions = scope.functions(driWithPlatform), + properties = scope.properties(driWithPlatform), + classlikes = scope.classlikes(driWithPlatform), + sources = descriptor.createSources(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + supertypes = info.supertypes.toSourceSetDependent(), + documentation = info.docs, + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + ImplementedInterfaces(info.allImplementedInterfaces.toSourceSetDependent()) + ) + ) + } + + private fun enumDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DEnum { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val scope = descriptor.unsubstitutedMemberScope + val isExpect = descriptor.isExpect + val info = descriptor.resolveClassDescriptionData() + + return DEnum( + dri = driWithPlatform.dri, + name = descriptor.name.asString(), + entries = scope.enumEntries(driWithPlatform), + constructors = descriptor.constructors.map { visitConstructorDescriptor(it, driWithPlatform) }, + functions = scope.functions(driWithPlatform), + properties = scope.properties(driWithPlatform), + classlikes = scope.classlikes(driWithPlatform), + sources = descriptor.createSources(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + supertypes = info.supertypes.toSourceSetDependent(), + documentation = info.docs, + companion = descriptor.companion(driWithPlatform), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + ImplementedInterfaces(info.allImplementedInterfaces.toSourceSetDependent()) + ) + ) + } + + private fun enumEntryDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DEnumEntry { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val scope = descriptor.unsubstitutedMemberScope + val isExpect = descriptor.isExpect + + return DEnumEntry( + dri = driWithPlatform.dri, + name = descriptor.name.asString(), + documentation = descriptor.resolveDescriptorData(), + classlikes = scope.classlikes(driWithPlatform), + functions = scope.functions(driWithPlatform), + properties = scope.properties(driWithPlatform), + sourceSets = setOf(sourceSet), + expectPresentInSet = sourceSet.takeIf { isExpect }, + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + ConstructorValues(descriptor.getAppliedConstructorParameters().toSourceSetDependent()) + ) + ) + } + + fun annotationDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DAnnotation { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val scope = descriptor.unsubstitutedMemberScope + + return DAnnotation( + dri = driWithPlatform.dri, + name = descriptor.name.asString(), + documentation = descriptor.resolveDescriptorData(), + classlikes = scope.classlikes(driWithPlatform), + functions = scope.functions(driWithPlatform), + properties = scope.properties(driWithPlatform), + expectPresentInSet = null, + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations() + ), + companion = descriptor.companionObjectDescriptor?.let { objectDescriptor(it, driWithPlatform) }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + generics = descriptor.declaredTypeParameters.map { it.toTypeParameter() }, + constructors = descriptor.constructors.map { visitConstructorDescriptor(it, driWithPlatform) }, + sources = descriptor.createSources() + ) + } + + private fun classDescriptor(descriptor: ClassDescriptor, parent: DRIWithPlatformInfo): DClass { + val driWithPlatform = parent.dri.withClass(descriptor.name.asString()).withEmptyInfo() + val scope = descriptor.unsubstitutedMemberScope + val isExpect = descriptor.isExpect + val info = descriptor.resolveClassDescriptionData() + val actual = descriptor.createSources() + + return DClass( + dri = driWithPlatform.dri, + name = descriptor.name.asString(), + constructors = descriptor.constructors.map { + visitConstructorDescriptor( + it, + if (it.isPrimary) DRIWithPlatformInfo(driWithPlatform.dri, actual) + else DRIWithPlatformInfo(driWithPlatform.dri, emptyMap()) + ) + }, + functions = scope.functions(driWithPlatform), + properties = scope.properties(driWithPlatform), + classlikes = scope.classlikes(driWithPlatform), + sources = actual, + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + supertypes = info.supertypes.toSourceSetDependent(), + generics = descriptor.declaredTypeParameters.map { it.toTypeParameter() }, + documentation = info.docs, + modifier = descriptor.modifier().toSourceSetDependent(), + companion = descriptor.companion(driWithPlatform), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + ImplementedInterfaces(info.allImplementedInterfaces.toSourceSetDependent()) + ) + ) + } + + override fun visitPropertyDescriptor(descriptor: PropertyDescriptor, parent: DRIWithPlatformInfo): DProperty { + val dri = parent.dri.copy(callable = Callable.from(descriptor)) + val isExpect = descriptor.isExpect + + val actual = descriptor.createSources() + return DProperty( + dri = dri, + name = descriptor.name.asString(), + receiver = descriptor.extensionReceiverParameter?.let { + visitReceiverParameterDescriptor(it, DRIWithPlatformInfo(dri, actual)) + }, + sources = actual, + getter = descriptor.accessors.filterIsInstance<PropertyGetterDescriptor>().singleOrNull()?.let { + visitPropertyAccessorDescriptor(it, descriptor, dri) + }, + setter = descriptor.accessors.filterIsInstance<PropertySetterDescriptor>().singleOrNull()?.let { + visitPropertyAccessorDescriptor(it, descriptor, dri) + }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + documentation = descriptor.resolveDescriptorData(), + modifier = descriptor.modifier().toSourceSetDependent(), + type = descriptor.returnType!!.toBound(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + sourceSets = setOf(sourceSet), + generics = descriptor.typeParameters.map { it.toTypeParameter() }, + extra = PropertyContainer.withAll( + (descriptor.additionalExtras() + descriptor.getAnnotationsWithBackingField() + .toAdditionalExtras()).toSet().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotationsWithBackingField().toSourceSetDependent().toAnnotations() + ) + ) + } + + fun CallableMemberDescriptor.createDRI(wasOverridenBy: DRI? = null): Pair<DRI, DRI?> = + if (kind == CallableMemberDescriptor.Kind.DECLARATION || overriddenDescriptors.isEmpty()) + Pair(DRI.from(this), wasOverridenBy) + else + overriddenDescriptors.first().createDRI(DRI.from(this)) + + override fun visitFunctionDescriptor(descriptor: FunctionDescriptor, parent: DRIWithPlatformInfo): DFunction { + val (dri, inheritedFrom) = descriptor.createDRI() + val isExpect = descriptor.isExpect + + val actual = descriptor.createSources() + return DFunction( + dri = dri, + name = descriptor.name.asString(), + isConstructor = false, + receiver = descriptor.extensionReceiverParameter?.let { + visitReceiverParameterDescriptor(it, DRIWithPlatformInfo(dri, actual)) + }, + parameters = descriptor.valueParameters.mapIndexed { index, desc -> + parameter(index, desc, DRIWithPlatformInfo(dri, actual)) + }, + expectPresentInSet = sourceSet.takeIf { isExpect }, + sources = actual, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + generics = descriptor.typeParameters.map { it.toTypeParameter() }, + documentation = descriptor.takeIf { it.kind != CallableMemberDescriptor.Kind.SYNTHESIZED }?.resolveDescriptorData() ?: emptyMap(), + modifier = descriptor.modifier().toSourceSetDependent(), + type = descriptor.returnType!!.toBound(), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + InheritedFunction(inheritedFrom.toSourceSetDependent()), + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations() + ) + ) + } + + override fun visitConstructorDescriptor(descriptor: ConstructorDescriptor, parent: DRIWithPlatformInfo): DFunction { + val dri = parent.dri.copy(callable = Callable.from(descriptor)) + val actual = descriptor.createSources() + val isExpect = descriptor.isExpect + + return DFunction( + dri = dri, + name = "<init>", + isConstructor = true, + receiver = descriptor.extensionReceiverParameter?.let { + visitReceiverParameterDescriptor(it, DRIWithPlatformInfo(dri, actual)) + }, + parameters = descriptor.valueParameters.mapIndexed { index, desc -> + parameter(index, desc, DRIWithPlatformInfo(dri, actual)) + }, + sources = actual, + expectPresentInSet = sourceSet.takeIf { isExpect }, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + documentation = descriptor.resolveDescriptorData().let { sourceSetDependent -> + if (descriptor.isPrimary) { + sourceSetDependent.map { entry -> + Pair( + entry.key, + entry.value.copy(children = (entry.value.children.find { it is Constructor }?.root?.let { constructor -> + listOf(Description(constructor)) + } ?: emptyList<TagWrapper>()) + entry.value.children.filterIsInstance<Param>())) + }.toMap() + } else { + sourceSetDependent + } + }, + type = descriptor.returnType.toBound(), + modifier = descriptor.modifier().toSourceSetDependent(), + generics = descriptor.typeParameters.map { it.toTypeParameter() }, + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll<DFunction>( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations() + ).let { + if (descriptor.isPrimary) { + it + PrimaryConstructorExtra + } else it + } + ) + } + + override fun visitReceiverParameterDescriptor( + descriptor: ReceiverParameterDescriptor, + parent: DRIWithPlatformInfo + ) = DParameter( + dri = parent.dri.copy(target = PointingToDeclaration), + name = null, + type = descriptor.type.toBound(), + expectPresentInSet = null, + documentation = descriptor.resolveDescriptorData(), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll(descriptor.getAnnotations().toSourceSetDependent().toAnnotations()) + ) + + private fun visitPropertyAccessorDescriptor( + descriptor: PropertyAccessorDescriptor, + propertyDescriptor: PropertyDescriptor, + parent: DRI + ): DFunction { + val dri = parent.copy(callable = Callable.from(descriptor)) + val isGetter = descriptor is PropertyGetterDescriptor + val isExpect = descriptor.isExpect + + fun PropertyDescriptor.asParameter(parent: DRI) = + DParameter( + parent.copy(target = PointingToCallableParameters(parameterIndex = 1)), + this.name.asString(), + type = this.type.toBound(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + documentation = descriptor.resolveDescriptorData(), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + getAnnotationsWithBackingField().toSourceSetDependent().toAnnotations() + ) + ) + + val name = run { + val modifier = if (isGetter) "get" else "set" + val rawName = propertyDescriptor.name.asString() + "$modifier${rawName[0].toUpperCase()}${rawName.drop(1)}" + } + + val parameters = + if (isGetter) { + emptyList() + } else { + listOf(propertyDescriptor.asParameter(dri)) + } + + return DFunction( + dri, + name, + isConstructor = false, + parameters = parameters, + visibility = descriptor.visibility.toDokkaVisibility().toSourceSetDependent(), + documentation = descriptor.resolveDescriptorData(), + type = descriptor.returnType!!.toBound(), + generics = descriptor.typeParameters.map { it.toTypeParameter() }, + modifier = descriptor.modifier().toSourceSetDependent(), + expectPresentInSet = sourceSet.takeIf { isExpect }, + receiver = descriptor.extensionReceiverParameter?.let { + visitReceiverParameterDescriptor( + it, + DRIWithPlatformInfo(dri, descriptor.createSources()) + ) + }, + sources = descriptor.createSources(), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations() + ) + ) + } + + override fun visitTypeAliasDescriptor(descriptor: TypeAliasDescriptor, parent: DRIWithPlatformInfo?) = + with(descriptor) { + DTypeAlias( + dri = DRI.from(this), + name = name.asString(), + type = defaultType.toBound(), + expectPresentInSet = null, + underlyingType = underlyingType.toBound().toSourceSetDependent(), + visibility = visibility.toDokkaVisibility().toSourceSetDependent(), + documentation = resolveDescriptorData(), + sourceSets = setOf(sourceSet) + ) + } + + private fun parameter(index: Int, descriptor: ValueParameterDescriptor, parent: DRIWithPlatformInfo) = + DParameter( + dri = parent.dri.copy(target = PointingToCallableParameters(index)), + name = descriptor.name.asString(), + type = descriptor.type.toBound(), + expectPresentInSet = null, + documentation = descriptor.resolveDescriptorData(), + sourceSets = setOf(sourceSet), + extra = PropertyContainer.withAll(listOfNotNull( + descriptor.additionalExtras().toSourceSetDependent().toAdditionalModifiers(), + descriptor.getAnnotations().toSourceSetDependent().toAnnotations(), + descriptor.getDefaultValue()?.let { DefaultValue(it) } + )) + ) + + private fun MemberScope.getContributedDescriptors(kindFilter: DescriptorKindFilter, shouldFilter: Boolean) = + getContributedDescriptors(kindFilter) { true }.let { + if (shouldFilter) it.filterDescriptorsInSourceSet() else it + } + + private fun MemberScope.functions(parent: DRIWithPlatformInfo, packageLevel: Boolean = false): List<DFunction> = + getContributedDescriptors(DescriptorKindFilter.FUNCTIONS, packageLevel) + .filterIsInstance<FunctionDescriptor>() + .map { visitFunctionDescriptor(it, parent) } + + private fun MemberScope.properties(parent: DRIWithPlatformInfo, packageLevel: Boolean = false): List<DProperty> = + getContributedDescriptors(DescriptorKindFilter.VALUES, packageLevel) + .filterIsInstance<PropertyDescriptor>() + .map { visitPropertyDescriptor(it, parent) } + + private fun MemberScope.classlikes(parent: DRIWithPlatformInfo, packageLevel: Boolean = false): List<DClasslike> = + getContributedDescriptors(DescriptorKindFilter.CLASSIFIERS, packageLevel) + .filter { it is ClassDescriptor && it.kind != ClassKind.ENUM_ENTRY } + .map { visitClassDescriptor(it as ClassDescriptor, parent) } + + private fun MemberScope.typealiases(parent: DRIWithPlatformInfo, packageLevel: Boolean = false): List<DTypeAlias> = + getContributedDescriptors(DescriptorKindFilter.TYPE_ALIASES, packageLevel) + .filterIsInstance<TypeAliasDescriptor>() + .map { visitTypeAliasDescriptor(it, parent) } + + private fun MemberScope.enumEntries(parent: DRIWithPlatformInfo): List<DEnumEntry> = + this.getContributedDescriptors(DescriptorKindFilter.CLASSIFIERS) { true } + .filterIsInstance<ClassDescriptor>() + .filter { it.kind == ClassKind.ENUM_ENTRY } + .map { enumEntryDescriptor(it, parent) } + + + private fun DeclarationDescriptor.resolveDescriptorData(): SourceSetDependent<DocumentationNode> = + getDocumentation()?.toSourceSetDependent() ?: emptyMap() + + private fun ClassDescriptor.resolveClassDescriptionData(): ClassInfo { + tailrec fun buildInheritanceInformation( + inheritorClass: ClassDescriptor?, + interfaces: List<ClassDescriptor>, + level: Int = 0, + inheritanceInformation: Set<InheritanceLevel> = emptySet() + ): Set<InheritanceLevel> { + if (inheritorClass == null && interfaces.isEmpty()) return inheritanceInformation + + val updated = inheritanceInformation + InheritanceLevel( + level, + inheritorClass?.let { DRI.from(it) }, + interfaces.map { DRI.from(it) }) + val superInterfacesFromClass = inheritorClass?.getSuperInterfaces().orEmpty() + return buildInheritanceInformation( + inheritorClass = inheritorClass?.getSuperClassNotAny(), + interfaces = interfaces.flatMap { it.getSuperInterfaces() } + superInterfacesFromClass, + level = level + 1, + inheritanceInformation = updated + ) + } + return ClassInfo( + buildInheritanceInformation(getSuperClassNotAny(), getSuperInterfaces()).sortedBy { it.level }, + resolveDescriptorData() + ) + } + + private fun TypeParameterDescriptor.toTypeParameter() = + DTypeParameter( + DRI.from(this), + name.identifier, + resolveDescriptorData(), + null, + upperBounds.map { it.toBound() }, + setOf(sourceSet), + extra = PropertyContainer.withAll(additionalExtras().toSourceSetDependent().toAdditionalModifiers()) + ) + + private fun KotlinType.toBound(): Bound = when (this) { + is DynamicType -> Dynamic + else -> when (val ctor = constructor.declarationDescriptor) { + is TypeParameterDescriptor -> OtherParameter( + declarationDRI = DRI.from(ctor.containingDeclaration).withPackageFallbackTo(fallbackPackageName()), + name = ctor.name.asString() + ) + else -> TypeConstructor( + DRI.from(constructor.declarationDescriptor!!), // TODO: remove '!!' + arguments.map { it.toProjection() }, + if (isExtensionFunctionType) FunctionModifiers.EXTENSION + else if (isFunctionType) FunctionModifiers.FUNCTION + else FunctionModifiers.NONE + ) + }.let { + if (isMarkedNullable) Nullable(it) else it + } + } + + private fun TypeProjection.toProjection(): Projection = + if (isStarProjection) Star else formPossiblyVariant() + + private fun TypeProjection.formPossiblyVariant(): Projection = type.fromPossiblyNullable().let { + when (projectionKind) { + org.jetbrains.kotlin.types.Variance.INVARIANT -> it + org.jetbrains.kotlin.types.Variance.IN_VARIANCE -> Variance(Variance.Kind.In, it) + org.jetbrains.kotlin.types.Variance.OUT_VARIANCE -> Variance(Variance.Kind.Out, it) + } + } + + private fun KotlinType.fromPossiblyNullable(): Bound = + toBound().let { if (isMarkedNullable) Nullable(it) else it } + + private fun DeclarationDescriptor.getDocumentation() = findKDoc().let { + MarkdownParser(resolutionFacade, this, logger).parseFromKDocTag(it) + }.takeIf { it.children.isNotEmpty() } + + private fun ClassDescriptor.companion(dri: DRIWithPlatformInfo): DObject? = companionObjectDescriptor?.let { + objectDescriptor(it, dri) + } + + private fun MemberDescriptor.modifier() = when (modality) { + Modality.FINAL -> KotlinModifier.Final + Modality.SEALED -> KotlinModifier.Sealed + Modality.OPEN -> KotlinModifier.Open + Modality.ABSTRACT -> KotlinModifier.Abstract + else -> KotlinModifier.Empty + } + + private fun MemberDescriptor.createSources(): SourceSetDependent<DocumentableSource> = + DescriptorDocumentableSource(this).toSourceSetDependent() + + private fun FunctionDescriptor.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Infix.takeIf { isInfix }, + ExtraModifiers.KotlinOnlyModifiers.Inline.takeIf { isInline }, + ExtraModifiers.KotlinOnlyModifiers.Suspend.takeIf { isSuspend }, + ExtraModifiers.KotlinOnlyModifiers.Operator.takeIf { isOperator }, + ExtraModifiers.JavaOnlyModifiers.Static.takeIf { isJvmStaticInObjectOrClassOrInterface() }, + ExtraModifiers.KotlinOnlyModifiers.TailRec.takeIf { isTailrec }, + ExtraModifiers.KotlinOnlyModifiers.External.takeIf { isExternal }, + ExtraModifiers.KotlinOnlyModifiers.Override.takeIf { DescriptorUtils.isOverride(this) } + ).toSet() + + private fun ClassDescriptor.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Inline.takeIf { isInline }, + ExtraModifiers.KotlinOnlyModifiers.External.takeIf { isExternal }, + ExtraModifiers.KotlinOnlyModifiers.Inner.takeIf { isInner }, + ExtraModifiers.KotlinOnlyModifiers.Data.takeIf { isData }, + ExtraModifiers.KotlinOnlyModifiers.Fun.takeIf { isFun } + ).toSet() + + private fun ValueParameterDescriptor.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.NoInline.takeIf { isNoinline }, + ExtraModifiers.KotlinOnlyModifiers.CrossInline.takeIf { isCrossinline }, + ExtraModifiers.KotlinOnlyModifiers.Const.takeIf { isConst }, + ExtraModifiers.KotlinOnlyModifiers.LateInit.takeIf { isLateInit }, + ExtraModifiers.KotlinOnlyModifiers.VarArg.takeIf { isVararg } + ).toSet() + + private fun TypeParameterDescriptor.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Reified.takeIf { isReified } + ).toSet() + + private fun PropertyDescriptor.additionalExtras() = listOfNotNull( + ExtraModifiers.KotlinOnlyModifiers.Const.takeIf { isConst }, + ExtraModifiers.KotlinOnlyModifiers.LateInit.takeIf { isLateInit }, + ExtraModifiers.JavaOnlyModifiers.Static.takeIf { isJvmStaticInObjectOrClassOrInterface() }, + ExtraModifiers.KotlinOnlyModifiers.External.takeIf { isExternal }, + ExtraModifiers.KotlinOnlyModifiers.Override.takeIf { DescriptorUtils.isOverride(this) } + ) + + private fun Annotated.getAnnotations() = annotations.mapNotNull { it.toAnnotation() } + + private fun ConstantValue<*>.toValue(): AnnotationParameterValue? = when (this) { + is ConstantsAnnotationValue -> value.toAnnotation()?.let { AnnotationValue(it) } + is ConstantsArrayValue -> ArrayValue(value.mapNotNull { it.toValue() }) + is ConstantsEnumValue -> EnumValue( + fullEnumEntryName(), + DRI(enumClassId.packageFqName.asString(), fullEnumEntryName()) + ) + is ConstantsKtClassValue -> when (value) { + is NormalClass -> (value as NormalClass).value.classId.let { + ClassValue( + it.relativeClassName.asString(), + DRI(it.packageFqName.asString(), it.relativeClassName.asString()) + ) + } + is LocalClass -> (value as LocalClass).type.let { + ClassValue( + it.toString(), + DRI.from(it.constructor.declarationDescriptor as DeclarationDescriptor) + ) + } + } + else -> StringValue(toString()) + } + + private fun AnnotationDescriptor.toAnnotation(): Annotations.Annotation { + return Annotations.Annotation( + DRI.from(annotationClass as DeclarationDescriptor), + allValueArguments.map { it.key.asString() to it.value.toValue() }.filter { + it.second != null + }.toMap() as Map<String, AnnotationParameterValue>, + annotationClass!!.annotations.hasAnnotation(FqName("kotlin.annotation.MustBeDocumented")) + ) + } + + private fun PropertyDescriptor.getAnnotationsWithBackingField(): List<Annotations.Annotation> = + getAnnotations() + (backingField?.getAnnotations() ?: emptyList()) + + private fun List<Annotations.Annotation>.toAdditionalExtras() = mapNotNull { + try { + ExtraModifiers.valueOf(it.dri.classNames?.toLowerCase() ?: "") + } catch (e: IllegalArgumentException) { + null + } + } + + + private fun ValueParameterDescriptor.getDefaultValue(): String? = + (source as? KotlinSourceElement)?.psi?.children?.find { it is KtExpression }?.text + + private fun ClassDescriptor.getAppliedConstructorParameters() = + (source as PsiSourceElement).psi?.children?.flatMap { + it.safeAs<KtInitializerList>()?.initializersAsText().orEmpty() + }.orEmpty() + + private fun KtInitializerList.initializersAsText() = + initializers.firstIsInstanceOrNull<KtCallElement>() + ?.getValueArgumentsInParentheses() + ?.flatMap { it.childrenAsText() } + .orEmpty() + + private fun ValueArgument.childrenAsText() = this.safeAs<KtValueArgument>()?.children?.map { it.text }.orEmpty() + + private data class InheritanceLevel(val level: Int, val superclass: DRI?, val interfaces: List<DRI>) + + private data class ClassInfo(val inheritance: List<InheritanceLevel>, val docs: SourceSetDependent<DocumentationNode>){ + val supertypes: List<DriWithKind> + get() = inheritance.firstOrNull { it.level == 0 }?.let { + listOfNotNull(it.superclass?.let { DriWithKind(it, KotlinClassKindTypes.CLASS) }) + it.interfaces.map { DriWithKind(it, KotlinClassKindTypes.INTERFACE) } + }.orEmpty() + + val allImplementedInterfaces: List<DRI> + get() = inheritance.flatMap { it.interfaces }.distinct() + } + + private fun Visibility.toDokkaVisibility(): org.jetbrains.dokka.model.Visibility = when (this) { + Visibilities.PUBLIC -> KotlinVisibility.Public + Visibilities.PROTECTED -> KotlinVisibility.Protected + Visibilities.INTERNAL -> KotlinVisibility.Internal + Visibilities.PRIVATE -> KotlinVisibility.Private + else -> KotlinVisibility.Public + } + + private fun ConstantsEnumValue.fullEnumEntryName() = + "${this.enumClassId.relativeClassName.asString()}.${this.enumEntryName.identifier}" + + private fun fallbackPackageName(): String = + "[${sourceSet.displayName} root]"// TODO: error-prone, find a better way to do it +} + +private fun DRI.withPackageFallbackTo(fallbackPackage: String): DRI { + return if (packageName.isNullOrBlank()) { + copy(packageName = fallbackPackage) + } else { + this + } +} diff --git a/plugins/base/src/main/kotlin/translators/documentables/DefaultDocumentableToPageTranslator.kt b/plugins/base/src/main/kotlin/translators/documentables/DefaultDocumentableToPageTranslator.kt new file mode 100644 index 00000000..04251947 --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/documentables/DefaultDocumentableToPageTranslator.kt @@ -0,0 +1,17 @@ +package org.jetbrains.dokka.base.translators.documentables + +import org.jetbrains.dokka.base.signatures.SignatureProvider +import org.jetbrains.dokka.base.transformers.pages.comments.CommentsToContentConverter +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.pages.ModulePageNode +import org.jetbrains.dokka.transformers.documentation.DocumentableToPageTranslator +import org.jetbrains.dokka.utilities.DokkaLogger + +class DefaultDocumentableToPageTranslator( + private val commentsToContentConverter: CommentsToContentConverter, + private val signatureProvider: SignatureProvider, + private val logger: DokkaLogger +) : DocumentableToPageTranslator { + override fun invoke(module: DModule): ModulePageNode = + DefaultPageCreator(commentsToContentConverter, signatureProvider, logger).pageForModule(module) +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt new file mode 100644 index 00000000..02f4b54e --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt @@ -0,0 +1,525 @@ +package org.jetbrains.dokka.base.translators.documentables + +import org.jetbrains.dokka.base.signatures.SignatureProvider +import org.jetbrains.dokka.base.transformers.documentables.CallableExtensions +import org.jetbrains.dokka.base.transformers.documentables.InheritorsInfo +import org.jetbrains.dokka.base.transformers.pages.comments.CommentsToContentConverter +import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder.DocumentableContentBuilder +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.doc.* +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.model.properties.WithExtraProperties +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.utils.addToStdlib.safeAs +import kotlin.reflect.KClass +import kotlin.reflect.full.isSubclassOf +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet + +private typealias GroupedTags = Map<KClass<out TagWrapper>, List<Pair<DokkaSourceSet?, TagWrapper>>> + +private val specialTags: Set<KClass<out TagWrapper>> = + setOf(Property::class, Description::class, Constructor::class, Receiver::class, Param::class, See::class) + +open class DefaultPageCreator( + commentsToContentConverter: CommentsToContentConverter, + signatureProvider: SignatureProvider, + val logger: DokkaLogger +) { + protected open val contentBuilder = PageContentBuilder(commentsToContentConverter, signatureProvider, logger) + + open fun pageForModule(m: DModule) = + ModulePageNode(m.name.ifEmpty { "<root>" }, contentForModule(m), m, m.packages.map(::pageForPackage)) + + open fun pageForPackage(p: DPackage): PackagePageNode = PackagePageNode( + p.name, contentForPackage(p), setOf(p.dri), p, + p.classlikes.map(::pageForClasslike) + + p.functions.map(::pageForFunction) + ) + + open fun pageForEnumEntry(e: DEnumEntry): ClasslikePageNode = + ClasslikePageNode( + e.name, contentForEnumEntry(e), setOf(e.dri), e, + e.classlikes.map(::pageForClasslike) + + e.filteredFunctions.map(::pageForFunction) + ) + + open fun pageForClasslike(c: DClasslike): ClasslikePageNode { + val constructors = if (c is WithConstructors) c.constructors else emptyList() + + return ClasslikePageNode( + c.name.orEmpty(), contentForClasslike(c), setOf(c.dri), c, + constructors.map(::pageForFunction) + + c.classlikes.map(::pageForClasslike) + + c.filteredFunctions.map(::pageForFunction) + + if (c is DEnum) c.entries.map(::pageForEnumEntry) else emptyList() + ) + } + + open fun pageForFunction(f: DFunction) = MemberPageNode(f.name, contentForFunction(f), setOf(f.dri), f) + + open fun pageForTypeAlias(t: DTypeAlias) = MemberPageNode(t.name, contentForTypeAlias(t), setOf(t.dri), t) + + private val WithScope.filteredFunctions: List<DFunction> + get() = functions.mapNotNull { function -> + function.takeIf { + it.sourceSets.any { sourceSet -> it.extra[InheritedFunction]?.isInherited(sourceSet) != true } + } + } + + protected open fun contentForModule(m: DModule) = contentBuilder.contentFor(m) { + group(kind = ContentKind.Cover) { + cover(m.name) + if (contentForDescription(m).isNotEmpty()) { + sourceSetDependentHint( + m.dri, + m.sourceSets.toSet(), + kind = ContentKind.SourceSetDependentHint, + styles = setOf(TextStyle.UnderCoverText) + ) { + +contentForDescription(m) + } + } + } + +contentForComments(m) + block("Packages", 2, ContentKind.Packages, m.packages, m.sourceSets.toSet()) { + link(it.name, it.dri) + } +// text("Index\n") TODO +// text("Link to allpage here") + } + + protected open fun contentForPackage(p: DPackage) = contentBuilder.contentFor(p) { + group(kind = ContentKind.Cover) { + cover("Package ${p.name}") + if (contentForDescription(p).isNotEmpty()) { + sourceSetDependentHint( + p.dri, + p.sourceSets.toSet(), + kind = ContentKind.SourceSetDependentHint, + styles = setOf(TextStyle.UnderCoverText) + ) { + +contentForDescription(p) + } + } + } + group(styles = setOf(ContentStyle.TabbedContent)) { + +contentForComments(p) + +contentForScope(p, p.dri, p.sourceSets) + } + } + + protected open fun contentForScope( + s: WithScope, + dri: DRI, + sourceSets: Set<DokkaSourceSet> + ) = contentBuilder.contentFor(s as Documentable) { + val types = listOf( + s.classlikes, + (s as? DPackage)?.typealiases ?: emptyList() + ).flatten() + divergentBlock("Types", types, ContentKind.Classlikes, extra = mainExtra + SimpleAttr.header("Types")) + divergentBlock( + "Functions", + s.functions, + ContentKind.Functions, + extra = mainExtra + SimpleAttr.header("Functions") + ) + block( + "Properties", + 2, + ContentKind.Properties, + s.properties, + sourceSets.toSet(), + needsAnchors = true, + extra = mainExtra + SimpleAttr.header("Properties") + ) { + link(it.name, it.dri, kind = ContentKind.Main) + sourceSetDependentHint(it.dri, it.sourceSets.toSet(), kind = ContentKind.SourceSetDependentHint) { + contentForBrief(it) + +buildSignature(it) + } + } + s.safeAs<WithExtraProperties<Documentable>>()?.let { it.extra[InheritorsInfo] }?.let { inheritors -> + val map = inheritors.value.filter { it.value.isNotEmpty() } + if (map.values.any()) { + header(2, "Inheritors") { } + +ContentTable( + listOf(contentBuilder.contentFor(mainDRI, mainSourcesetData){ + text("Name") + }), + map.entries.flatMap { entry -> entry.value.map { Pair(entry.key, it) } } + .groupBy({ it.second }, { it.first }).map { (classlike, platforms) -> + buildGroup(setOf(dri), platforms.toSet(), ContentKind.Inheritors) { + link( + classlike.classNames?.substringBeforeLast(".") ?: classlike.toString() + .also { logger.warn("No class name found for DRI $classlike") }, classlike + ) + } + }, + DCI(setOf(dri), ContentKind.Inheritors), + sourceSets.toSet(), + style = emptySet(), + extra = mainExtra + SimpleAttr.header("Inheritors") + ) + } + } + } + + protected open fun contentForEnumEntry(e: DEnumEntry) = contentBuilder.contentFor(e) { + group(kind = ContentKind.Cover) { + cover(e.name) + sourceSetDependentHint(e.dri, e.sourceSets.toSet()) { + +contentForDescription(e) + +buildSignature(e) + } + } + group(styles = setOf(ContentStyle.TabbedContent)) { + +contentForComments(e) + +contentForScope(e, e.dri, e.sourceSets) + } + } + + protected open fun contentForClasslike(c: DClasslike) = contentBuilder.contentFor(c) { + @Suppress("UNCHECKED_CAST") + val extensions = (c as WithExtraProperties<DClasslike>) + .extra[CallableExtensions]?.extensions + ?.filterIsInstance<Documentable>().orEmpty() + // Extensions are added to sourceSets since they can be placed outside the sourceSets from classlike + // Example would be an Interface in common and extension function in jvm + group(kind = ContentKind.Cover, sourceSets = mainSourcesetData + extensions.sourceSets) { + cover(c.name.orEmpty()) + sourceSetDependentHint(c.dri, c.sourceSets) { + +contentForDescription(c) + +buildSignature(c) + } + } + + group(styles = setOf(ContentStyle.TabbedContent), sourceSets = mainSourcesetData + extensions.sourceSets) { + +contentForComments(c) + if (c is WithConstructors) { + block( + "Constructors", + 2, + ContentKind.Constructors, + c.constructors.filter { it.extra[PrimaryConstructorExtra] == null || it.documentation.isNotEmpty() }, + c.sourceSets, + extra = PropertyContainer.empty<ContentNode>() + SimpleAttr.header("Constructors") + ) { + link(it.name, it.dri, kind = ContentKind.Main) + sourceSetDependentHint( + it.dri, + it.sourceSets.toSet(), + kind = ContentKind.SourceSetDependentHint, + styles = emptySet() + ) { + contentForBrief(it) + +buildSignature(it) + } + } + } + if (c is DEnum) { + block( + "Entries", + 2, + ContentKind.Classlikes, + c.entries, + c.sourceSets.toSet(), + needsSorting = false, + extra = mainExtra + SimpleAttr.header("Entries"), + styles = emptySet() + ) { + link(it.name, it.dri) + sourceSetDependentHint(it.dri, it.sourceSets.toSet(), kind = ContentKind.SourceSetDependentHint) { + contentForBrief(it) + +buildSignature(it) + } + } + } + +contentForScope(c, c.dri, c.sourceSets) + + divergentBlock("Extensions", extensions, ContentKind.Extensions, extra = mainExtra + SimpleAttr.header("Extensions")) + } + } + + @Suppress("UNCHECKED_CAST") + private inline fun <reified T : TagWrapper> GroupedTags.withTypeUnnamed(): SourceSetDependent<T> = + (this[T::class] as List<Pair<DokkaSourceSet, T>>?)?.toMap().orEmpty() + + @Suppress("UNCHECKED_CAST") + private inline fun <reified T : NamedTagWrapper> GroupedTags.withTypeNamed(): Map<String, SourceSetDependent<T>> = + (this[T::class] as List<Pair<DokkaSourceSet, T>>?) + ?.groupBy { it.second.name } + ?.mapValues { (_, v) -> v.toMap() } + ?.toSortedMap(String.CASE_INSENSITIVE_ORDER) + .orEmpty() + + private inline fun <reified T : TagWrapper> GroupedTags.isNotEmptyForTag(): Boolean = + this[T::class]?.isNotEmpty() ?: false + + protected open fun contentForDescription( + d: Documentable + ): List<ContentNode> { + val tags: GroupedTags = d.documentation.flatMap { (pd, doc) -> + doc.children.asSequence().map { pd to it }.toList() + }.groupBy { it.second::class } + + val platforms = d.sourceSets.toSet() + + return contentBuilder.contentFor(d, styles = setOf(TextStyle.Block)) { + val description = tags.withTypeUnnamed<Description>() + if (description.any { it.value.root.children.isNotEmpty() }) { + platforms.forEach { platform -> + description[platform]?.also { + group(sourceSets = setOf(platform)) { + comment(it.root) + } + } + } + } + + val unnamedTags: List<SourceSetDependent<TagWrapper>> = + tags.filterNot { (k, _) -> k.isSubclassOf(NamedTagWrapper::class) || k in specialTags } + .map { (_, v) -> v.mapNotNull { (k, v) -> k?.let { it to v } }.toMap() } + if (unnamedTags.isNotEmpty()) { + platforms.forEach { platform -> + unnamedTags.forEach { pdTag -> + pdTag[platform]?.also { tag -> + group(sourceSets = setOf(platform)) { + header(4, tag.toHeaderString()) + comment(tag.root) + } + } + } + } + } + + contentForSinceKotlin(d) + }.children + } + + private fun Documentable.getPossibleFallbackSourcesets(sourceSet: DokkaSourceSet) = + this.sourceSets.filter { it.sourceSetID in sourceSet.dependentSourceSets } + + private fun <V> Map<DokkaSourceSet, V>.fallback(sourceSets: List<DokkaSourceSet>): V? = + sourceSets.firstOrNull { it in this.keys }.let { this[it] } + + protected open fun contentForComments( + d: Documentable + ): List<ContentNode> { + val tags: GroupedTags = d.documentation.flatMap { (pd, doc) -> + doc.children.asSequence().map { pd to it }.toList() + }.groupBy { it.second::class } + + val platforms = d.sourceSets + + fun DocumentableContentBuilder.contentForParams() { + if (tags.isNotEmptyForTag<Param>()) { + header(2, "Parameters", kind = ContentKind.Parameters) + group( + extra = mainExtra + SimpleAttr.header("Parameters"), + styles = setOf(ContentStyle.WithExtraAttributes) + ) { + sourceSetDependentHint(sourceSets = platforms.toSet(), kind = ContentKind.SourceSetDependentHint) { + val receiver = tags.withTypeUnnamed<Receiver>() + val params = tags.withTypeNamed<Param>() + table(kind = ContentKind.Parameters) { + platforms.flatMap { platform -> + val possibleFallbacks = d.getPossibleFallbackSourcesets(platform) + val receiverRow = (receiver[platform] ?: receiver.fallback(possibleFallbacks))?.let { + buildGroup(sourceSets = setOf(platform), kind = ContentKind.Parameters) { + text("<receiver>", styles = mainStyles + ContentStyle.RowTitle) + comment(it.root) + } + } + + val paramRows = params.mapNotNull { (_, param) -> + (param[platform] ?: param.fallback(possibleFallbacks))?.let { + buildGroup(sourceSets = setOf(platform), kind = ContentKind.Parameters) { + text( + it.name, + kind = ContentKind.Parameters, + styles = mainStyles + ContentStyle.RowTitle + ) + comment(it.root) + } + } + } + + listOfNotNull(receiverRow) + paramRows + } + } + } + } + } + } + + fun DocumentableContentBuilder.contentForSeeAlso() { + if (tags.isNotEmptyForTag<See>()) { + header(2, "See also", kind = ContentKind.Comment) + group( + extra = mainExtra + SimpleAttr.header("See also"), + styles = setOf(ContentStyle.WithExtraAttributes) + ) { + sourceSetDependentHint(sourceSets = platforms.toSet(), kind = ContentKind.SourceSetDependentHint) { + val seeAlsoTags = tags.withTypeNamed<See>() + table(kind = ContentKind.Sample) { + platforms.flatMap { platform -> + val possibleFallbacks = d.getPossibleFallbackSourcesets(platform) + seeAlsoTags.mapNotNull { (_, see) -> + (see[platform] ?: see.fallback(possibleFallbacks))?.let { + buildGroup( + sourceSets = setOf(platform), + kind = ContentKind.Comment, + styles = mainStyles + ContentStyle.RowTitle + ) { + if (it.address != null) link( + it.name, + it.address!!, + kind = ContentKind.Comment + ) + else text(it.name, kind = ContentKind.Comment) + comment(it.root) + } + } + } + } + } + } + } + } + } + + fun DocumentableContentBuilder.contentForSamples() { + val samples = tags.withTypeNamed<Sample>() + if (samples.isNotEmpty()) { + header(2, "Samples", kind = ContentKind.Sample) + group( + extra = mainExtra + SimpleAttr.header("Samples"), + styles = emptySet() + ) { + sourceSetDependentHint(sourceSets = platforms.toSet(), kind = ContentKind.SourceSetDependentHint) { + platforms.map { platformData -> + val content = samples.filter { it.value.isEmpty() || platformData in it.value } + group( + sourceSets = setOf(platformData), + kind = ContentKind.Sample, + styles = setOf(TextStyle.Monospace, ContentStyle.RunnableSample) + ) { + content.forEach { + text(it.key) + } + } + } + } + } + } + } + + return contentBuilder.contentFor(d) { + if (tags.isNotEmpty()) { + contentForSamples() + contentForSeeAlso() + contentForParams() + } + }.children + } + + protected open fun DocumentableContentBuilder.contentForBrief(documentable: Documentable) { + documentable.sourceSets.forEach { sourceSet -> + documentable.documentation[sourceSet]?.children?.firstOrNull()?.root?.let { + group(sourceSets = setOf(sourceSet), kind = ContentKind.BriefComment) { + comment(it) + } + } + } + } + + protected open fun DocumentableContentBuilder.contentForSinceKotlin(documentable: Documentable) { + documentable.documentation.mapValues { + it.value.children.find { it is CustomTagWrapper && it.name == "Since Kotlin" } as CustomTagWrapper? + }.run { + documentable.sourceSets.forEach { sourceSet -> + this[sourceSet]?.also { tag -> + group(sourceSets = setOf(sourceSet), kind = ContentKind.Comment, styles = setOf(TextStyle.Block)) { + header(4, tag.name) + comment(tag.root) + } + } + } + } + } + + protected open fun contentForFunction(f: DFunction) = contentForMember(f) + protected open fun contentForTypeAlias(t: DTypeAlias) = contentForMember(t) + protected open fun contentForMember(d: Documentable) = contentBuilder.contentFor(d) { + group(kind = ContentKind.Cover) { + cover(d.name.orEmpty()) + } + divergentGroup(ContentDivergentGroup.GroupID("member")) { + instance(setOf(d.dri), d.sourceSets.toSet()) { + before { + +contentForDescription(d) + +contentForComments(d) + } + divergent(kind = ContentKind.Symbol) { + +buildSignature(d) + } + } + } + } + + protected open fun DocumentableContentBuilder.divergentBlock( + name: String, + collection: Collection<Documentable>, + kind: ContentKind, + extra: PropertyContainer<ContentNode> = mainExtra + ) { + if (collection.any()) { + header(2, name, kind = kind) + table(kind, extra = extra, styles = emptySet()) { + collection + .groupBy { it.name } + // This hacks displaying actual typealias signatures along classlike ones + .mapValues { if (it.value.any { it is DClasslike }) it.value.filter { it !is DTypeAlias } else it.value } + .toSortedMap(compareBy(nullsLast(String.CASE_INSENSITIVE_ORDER)){it}) + .map { (elementName, elements) -> // This groupBy should probably use LocationProvider + buildGroup( + dri = elements.map { it.dri }.toSet(), + sourceSets = elements.flatMap { it.sourceSets }.toSet(), + kind = kind, + styles = emptySet() + ) { + link(elementName.orEmpty(), elements.first().dri, kind = kind) + divergentGroup( + ContentDivergentGroup.GroupID(name), + elements.map { it.dri }.toSet(), + kind = kind + ) { + elements.map { + instance(setOf(it.dri), it.sourceSets.toSet()) { + before { + contentForBrief(it) + contentForSinceKotlin(it) + } + divergent { + group { + +buildSignature(it) + } + } + } + } + } + } + } + } + } + } + + + protected open fun TagWrapper.toHeaderString() = this.javaClass.toGenericString().split('.').last() + + private val List<Documentable>.sourceSets: Set<DokkaSourceSet> + get() = flatMap { it.sourceSets }.toSet() +} diff --git a/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt b/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt new file mode 100644 index 00000000..b7927076 --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/documentables/PageContentBuilder.kt @@ -0,0 +1,474 @@ +package org.jetbrains.dokka.base.translators.documentables + +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint +import org.jetbrains.dokka.base.signatures.SignatureProvider +import org.jetbrains.dokka.base.transformers.pages.comments.CommentsToContentConverter +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.model.Documentable +import org.jetbrains.dokka.model.SourceSetDependent +import org.jetbrains.dokka.model.doc.DocTag +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.pages.* +import org.jetbrains.dokka.utilities.DokkaLogger + +@DslMarker +annotation class ContentBuilderMarker + +open class PageContentBuilder( + val commentsConverter: CommentsToContentConverter, + val signatureProvider: SignatureProvider, + val logger: DokkaLogger +) { + fun contentFor( + dri: DRI, + sourceSets: Set<DokkaSourceSet>, + kind: Kind = ContentKind.Main, + styles: Set<Style> = emptySet(), + extra: PropertyContainer<ContentNode> = PropertyContainer.empty(), + block: DocumentableContentBuilder.() -> Unit + ): ContentGroup = + DocumentableContentBuilder(setOf(dri), sourceSets, styles, extra) + .apply(block) + .build(sourceSets, kind, styles, extra) + + fun contentFor( + dri: Set<DRI>, + sourceSets: Set<DokkaSourceSet>, + kind: Kind = ContentKind.Main, + styles: Set<Style> = emptySet(), + extra: PropertyContainer<ContentNode> = PropertyContainer.empty(), + block: DocumentableContentBuilder.() -> Unit + ): ContentGroup = + DocumentableContentBuilder(dri, sourceSets, styles, extra) + .apply(block) + .build(sourceSets, kind, styles, extra) + + fun contentFor( + d: Documentable, + kind: Kind = ContentKind.Main, + styles: Set<Style> = emptySet(), + extra: PropertyContainer<ContentNode> = PropertyContainer.empty(), + sourceSets: Set<DokkaSourceSet> = d.sourceSets.toSet(), + block: DocumentableContentBuilder.() -> Unit = {} + ): ContentGroup = + DocumentableContentBuilder(setOf(d.dri), sourceSets, styles, extra) + .apply(block) + .build(sourceSets, kind, styles, extra) + + @ContentBuilderMarker + open inner class DocumentableContentBuilder( + val mainDRI: Set<DRI>, + val mainSourcesetData: Set<DokkaSourceSet>, + val mainStyles: Set<Style>, + val mainExtra: PropertyContainer<ContentNode> + ) { + protected val contents = mutableListOf<ContentNode>() + + fun build( + sourceSets: Set<DokkaSourceSet>, + kind: Kind, + styles: Set<Style>, + extra: PropertyContainer<ContentNode> + ) = ContentGroup( + contents.toList(), + DCI(mainDRI, kind), + sourceSets, + styles, + extra + ) + + operator fun ContentNode.unaryPlus() { + contents += this + } + + operator fun Collection<ContentNode>.unaryPlus() { + contents += this + } + + private val defaultHeaders + get() = listOf( + contentFor(mainDRI, mainSourcesetData){ + text("Name") + }, + contentFor(mainDRI, mainSourcesetData){ + text("Summary") + } + ) + + fun header( + level: Int, + text: String, + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit = {} + ) { + contents += ContentHeader( + level, + contentFor( + mainDRI, + sourceSets, + kind, + styles, + extra + SimpleAttr("anchor", text.replace("\\s".toRegex(), "").toLowerCase()) + ) { + text(text, kind = kind) + block() + } + ) + } + + fun cover( + text: String, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles + TextStyle.Cover, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit = {} + ) { + header(1, text, sourceSets = sourceSets, styles = styles, extra = extra, block = block) + } + + fun text( + text: String, + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ) { + contents += createText(text, kind, sourceSets, styles, extra) + } + + fun buildSignature(d: Documentable) = signatureProvider.signature(d) + + fun table( + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + operation: DocumentableContentBuilder.() -> List<ContentGroup> + ) { + contents += ContentTable( + defaultHeaders, + operation(), + DCI(mainDRI, kind), + sourceSets, styles, extra + ) + } + + fun <T : Documentable> block( + name: String, + level: Int, + kind: Kind = ContentKind.Main, + elements: Iterable<T>, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + renderWhenEmpty: Boolean = false, + needsSorting: Boolean = true, + headers: List<ContentGroup>? = null, + needsAnchors: Boolean = false, + operation: DocumentableContentBuilder.(T) -> Unit + ) { + if (renderWhenEmpty || elements.any()) { + header(level, name, kind = kind) { } + contents += ContentTable( + headers ?: defaultHeaders, + elements + .let { + if (needsSorting) + it.sortedWith(compareBy(nullsLast(String.CASE_INSENSITIVE_ORDER)) { it.name }) + else it + } + .map { + val newExtra = if (needsAnchors) extra + SymbolAnchorHint else extra + buildGroup(setOf(it.dri), it.sourceSets.toSet(), kind, styles, newExtra) { + operation(it) + } + }, + DCI(mainDRI, kind), + sourceSets, styles, extra + ) + } + } + + fun <T> list( + elements: List<T>, + prefix: String = "", + suffix: String = "", + separator: String = ", ", + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, // TODO: children should be aware of this platform data + operation: DocumentableContentBuilder.(T) -> Unit + ) { + if (elements.isNotEmpty()) { + if (prefix.isNotEmpty()) text(prefix, sourceSets = sourceSets) + elements.dropLast(1).forEach { + operation(it) + text(separator, sourceSets = sourceSets) + } + operation(elements.last()) + if (suffix.isNotEmpty()) text(suffix, sourceSets = sourceSets) + } + } + + fun link( + text: String, + address: DRI, + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ) { + contents += linkNode(text, address, kind, sourceSets, styles, extra) + } + + fun linkNode( + text: String, + address: DRI, + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ) = ContentDRILink( + listOf(createText(text, kind, sourceSets, styles, extra)), + address, + DCI(mainDRI, kind), + sourceSets + ) + + fun link( + text: String, + address: String, + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ) { + contents += ContentResolvedLink( + children = listOf(createText(text, kind, sourceSets, styles, extra)), + address = address, + extra = PropertyContainer.empty(), + dci = DCI(mainDRI, kind), + sourceSets = sourceSets, + style = emptySet() + ) + } + + fun link( + address: DRI, + kind: Kind = ContentKind.Main, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + contents += ContentDRILink( + contentFor(mainDRI, sourceSets, kind, styles, extra, block).children, + address, + DCI(mainDRI, kind), + sourceSets + ) + } + + fun comment( + docTag: DocTag, + kind: Kind = ContentKind.Comment, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ) { + val content = commentsConverter.buildContent( + docTag, + DCI(mainDRI, kind), + sourceSets + ) + contents += ContentGroup(content, DCI(mainDRI, kind), sourceSets, styles, extra) + } + + fun group( + dri: Set<DRI> = mainDRI, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + kind: Kind = ContentKind.Main, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + contents += buildGroup(dri, sourceSets, kind, styles, extra, block) + } + + fun divergentGroup( + groupID: ContentDivergentGroup.GroupID, + dri: Set<DRI> = mainDRI, + kind: Kind = ContentKind.Main, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + implicitlySourceSetHinted: Boolean = true, + block: DivergentBuilder.() -> Unit + ) { + contents += + DivergentBuilder(dri, kind, styles, extra) + .apply(block) + .build(groupID = groupID, implicitlySourceSetHinted = implicitlySourceSetHinted) + } + + fun buildGroup( + dri: Set<DRI> = mainDRI, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + kind: Kind = ContentKind.Main, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ): ContentGroup = contentFor(dri, sourceSets, kind, styles, extra, block) + + fun sourceSetDependentHint( + dri: Set<DRI> = mainDRI, + sourceSets: Set<DokkaSourceSet> = mainSourcesetData, + kind: Kind = ContentKind.Main, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + contents += PlatformHintedContent( + buildGroup(dri, sourceSets, kind, styles, extra, block), + sourceSets + ) + } + + fun sourceSetDependentHint( + dri: DRI, + sourcesetData: Set<DokkaSourceSet> = mainSourcesetData, + kind: Kind = ContentKind.Main, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + contents += PlatformHintedContent( + buildGroup(setOf(dri), sourcesetData, kind, styles, extra, block), + sourcesetData + ) + } + + protected fun createText( + text: String, + kind: Kind, + sourceSets: Set<DokkaSourceSet>, + styles: Set<Style>, + extra: PropertyContainer<ContentNode> + ) = + ContentText(text, DCI(mainDRI, kind), sourceSets, styles, extra) + + fun <T> sourceSetDependentText( + value: SourceSetDependent<T>, + sourceSets: Set<DokkaSourceSet> = value.keys, + transform: (T) -> String + ) = value.entries.filter { it.key in sourceSets }.mapNotNull { (p, v) -> + transform(v).takeIf { it.isNotBlank() }?.let { it to p } + }.groupBy({ it.first }) { it.second }.forEach { + text(it.key, sourceSets = it.value.toSet()) + } + } + + @ContentBuilderMarker + open inner class DivergentBuilder( + private val mainDRI: Set<DRI>, + private val mainKind: Kind, + private val mainStyles: Set<Style>, + private val mainExtra: PropertyContainer<ContentNode> + ) { + private val instances: MutableList<ContentDivergentInstance> = mutableListOf() + fun instance( + dri: Set<DRI>, + sourceSets: Set<DokkaSourceSet>, // Having correct sourcesetData is crucial here, that's why there's no default + kind: Kind = mainKind, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DivergentInstanceBuilder.() -> Unit + ) { + instances += DivergentInstanceBuilder(dri, sourceSets, styles, extra) + .apply(block) + .build(kind) + } + + fun build( + groupID: ContentDivergentGroup.GroupID, + implicitlySourceSetHinted: Boolean, + kind: Kind = mainKind, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ) = ContentDivergentGroup( + instances.toList(), + DCI(mainDRI, kind), + styles, + extra, + groupID, + implicitlySourceSetHinted + ) + } + + @ContentBuilderMarker + open inner class DivergentInstanceBuilder( + private val mainDRI: Set<DRI>, + private val mainSourceSets: Set<DokkaSourceSet>, + private val mainStyles: Set<Style>, + private val mainExtra: PropertyContainer<ContentNode> + ) { + private var before: ContentNode? = null + private var divergent: ContentNode? = null + private var after: ContentNode? = null + + fun before( + dri: Set<DRI> = mainDRI, + sourceSets: Set<DokkaSourceSet> = mainSourceSets, + kind: Kind = ContentKind.Main, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + contentFor(dri, sourceSets, kind, styles, extra, block) + .takeIf { it.hasAnyContent() } + .also { before = it } + } + + fun divergent( + dri: Set<DRI> = mainDRI, + sourceSets: Set<DokkaSourceSet> = mainSourceSets, + kind: Kind = ContentKind.Main, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + divergent = contentFor(dri, sourceSets, kind, styles, extra, block) + } + + fun after( + dri: Set<DRI> = mainDRI, + sourceSets: Set<DokkaSourceSet> = mainSourceSets, + kind: Kind = ContentKind.Main, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra, + block: DocumentableContentBuilder.() -> Unit + ) { + contentFor(dri, sourceSets, kind, styles, extra, block) + .takeIf { it.hasAnyContent() } + .also { after = it } + } + + + fun build( + kind: Kind, + sourceSets: Set<DokkaSourceSet> = mainSourceSets, + styles: Set<Style> = mainStyles, + extra: PropertyContainer<ContentNode> = mainExtra + ) = + ContentDivergentInstance( + before, + divergent ?: throw IllegalStateException("Divergent block needs divergent part"), + after, + DCI(mainDRI, kind), + sourceSets, + styles, + extra + ) + } +}
\ No newline at end of file diff --git a/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt new file mode 100644 index 00000000..6f980383 --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/psi/DefaultPsiToDocumentableTranslator.kt @@ -0,0 +1,480 @@ +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.types.JvmReferenceType +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.psi.* +import com.intellij.psi.impl.source.PsiClassReferenceType +import com.intellij.psi.impl.source.PsiImmediateClassType +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.analysis.KotlinAnalysis +import org.jetbrains.dokka.analysis.PsiDocumentableSource +import org.jetbrains.dokka.analysis.from +import org.jetbrains.dokka.links.DRI +import org.jetbrains.dokka.links.DriWithKind +import org.jetbrains.dokka.links.nextTarget +import org.jetbrains.dokka.links.withClass +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.doc.DocumentationLink +import org.jetbrains.dokka.model.doc.DocumentationNode +import org.jetbrains.dokka.model.doc.Param +import org.jetbrains.dokka.model.doc.Text +import org.jetbrains.dokka.model.properties.PropertyContainer +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.sources.SourceToDocumentableTranslator +import org.jetbrains.dokka.utilities.DokkaLogger +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.descriptors.Visibilities +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.psi.psiUtil.getChildOfType +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.utils.addToStdlib.safeAs +import java.io.File + +class DefaultPsiToDocumentableTranslator( + private val kotlinAnalysis: KotlinAnalysis +) : SourceToDocumentableTranslator { + + override fun invoke(sourceSet: DokkaSourceSet, context: DokkaContext): DModule { + + fun isFileInSourceRoots(file: File) : Boolean { + return sourceSet.sourceRoots.any { root -> file.path.startsWith(File(root.path).absolutePath) } + } + + val (environment, _) = 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.map { 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, + context.logger + ) + return DModule( + sourceSet.moduleDisplayName, + psiFiles.mapNotNull { it.safeAs<PsiJavaFile>() }.groupBy { it.packageName }.map { (packageName, psiFiles) -> + val dri = DRI(packageName = packageName) + DPackage( + dri, + emptyList(), + emptyList(), + psiFiles.flatMap { psiFile -> + psiFile.classes.map { docParser.parseClasslike(it, dri) } + }, + emptyList(), + emptyMap(), + null, + setOf(sourceSet) + ) + }, + emptyMap(), + null, + setOf(sourceSet) + ) + } + + class DokkaPsiParser( + private val sourceSetData: DokkaSourceSet, + private val logger: DokkaLogger + ) { + + private val javadocParser: JavaDocumentationParser = JavadocParser(logger) + + private val cachedBounds = hashMapOf<String, Bound>() + + private fun PsiModifierListOwner.getVisibility() = modifierList?.children?.toList()?.let { ml -> + when { + ml.any { it.text == PsiKeyword.PUBLIC } -> JavaVisibility.Public + ml.any { it.text == PsiKeyword.PROTECTED } -> JavaVisibility.Protected + ml.any { it.text == PsiKeyword.PRIVATE } -> JavaVisibility.Private + else -> JavaVisibility.Default + } + } ?: JavaVisibility.Default + + private val PsiMethod.hash: Int + get() = "$returnType $name$parameterList".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) + + fun parseClasslike(psi: PsiClass, parent: DRI): DClasslike = with(psi) { + val dri = parent.withClass(name.toString()) + val inheritanceTree = mutableListOf<AncestorLevel>() + val superMethodsKeys = hashSetOf<Int>() + val superMethods = mutableListOf<Pair<PsiMethod, DRI>>() + methods.forEach { superMethodsKeys.add(it.hash) } + fun parseSupertypes(superTypes: Array<PsiClassType>, level: Int = 0) { + if(superTypes.isEmpty()) return + val parsedClasses = superTypes.filter { !it.shouldBeIgnored }.mapNotNull { + it.resolve()?.let { + when { + it.isInterface -> DRI.from(it) to JavaClassKindTypes.INTERFACE + else -> DRI.from(it) to JavaClassKindTypes.CLASS + } + } + } + val (classes, interfaces) = parsedClasses.partition { it.second == JavaClassKindTypes.CLASS } + inheritanceTree.add(AncestorLevel(level, classes.firstOrNull()?.first, interfaces.map { it.first })) + + superTypes.forEach { type -> + (type as? PsiClassType)?.takeUnless { type.shouldBeIgnored }?.resolve()?.let { + val definedAt = DRI.from(it) + it.methods.forEach { method -> + val hash = method.hash + if (!method.isConstructor && !superMethodsKeys.contains(hash) && + method.getVisibility() != Visibilities.PRIVATE + ) { + superMethodsKeys.add(hash) + superMethods.add(Pair(method, definedAt)) + } + } + parseSupertypes(it.superTypes, level + 1) + } + } + } + parseSupertypes(superTypes) + val (regularFunctions, accessors) = splitFunctionsAndAccessors() + val documentation = javadocParser.parseDocumentation(this).toSourceSetDependent() + val allFunctions = regularFunctions.mapNotNull { if (!it.isConstructor) parseFunction(it) else null } + + superMethods.map { parseFunction(it.first, inheritedFrom = it.second) } + val source = PsiDocumentableSource(this).toSourceSetDependent() + val classlikes = innerClasses.map { parseClasslike(it, dri) } + val visibility = getVisibility().toSourceSetDependent() + val ancestors = inheritanceTree.filter { it.level == 0 }.flatMap { + listOfNotNull(it.superclass?.let { + DriWithKind( + dri = it, + kind = JavaClassKindTypes.CLASS + ) + }) + it.interfaces.map { DriWithKind(dri = it, kind = JavaClassKindTypes.INTERFACE) } + }.toSourceSetDependent() + val modifiers = getModifier().toSourceSetDependent() + val implementedInterfacesExtra = ImplementedInterfaces(inheritanceTree.flatMap { it.interfaces }.distinct().toSourceSetDependent()) + return when { + isAnnotationType -> + DAnnotation( + name.orEmpty(), + dri, + documentation, + null, + source, + allFunctions, + fields.mapNotNull { parseField(it, accessors[it].orEmpty()) }, + classlikes, + visibility, + null, + constructors.map { parseFunction(it, true) }, + mapTypeParameters(dri), + setOf(sourceSetData), + PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent() + .toAnnotations()) + ) + isEnum -> DEnum( + dri, + name.orEmpty(), + fields.filterIsInstance<PsiEnumConstant>().map { entry -> + DEnumEntry( + dri.withClass("${entry.name}"), + entry.name, + javadocParser.parseDocumentation(entry).toSourceSetDependent(), + null, + emptyList(), + emptyList(), + emptyList(), + setOf(sourceSetData), + PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent() + .toAnnotations()) + ) + }, + documentation, + null, + source, + allFunctions, + fields.filter { it !is PsiEnumConstant }.map { parseField(it, accessors[it].orEmpty()) }, + classlikes, + visibility, + null, + constructors.map { parseFunction(it, true) }, + ancestors, + setOf(sourceSetData), + PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent() + .toAnnotations()) + ) + isInterface -> DInterface( + dri, + name.orEmpty(), + documentation, + null, + source, + allFunctions, + fields.mapNotNull { parseField(it, accessors[it].orEmpty()) }, + classlikes, + visibility, + null, + mapTypeParameters(dri), + ancestors, + setOf(sourceSetData), + PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent() + .toAnnotations()) + ) + else -> DClass( + dri, + name.orEmpty(), + constructors.map { parseFunction(it, true) }, + allFunctions, + fields.mapNotNull { parseField(it, accessors[it].orEmpty()) }, + classlikes, + source, + visibility, + null, + mapTypeParameters(dri), + ancestors, + documentation, + null, + modifiers, + setOf(sourceSetData), + PropertyContainer.withAll(implementedInterfacesExtra, annotations.toList().toListOfAnnotations().toSourceSetDependent() + .toAnnotations()) + ) + } + } + + private fun parseFunction( + psi: PsiMethod, + isConstructor: Boolean = false, + inheritedFrom: DRI? = null + ): DFunction { + val dri = DRI.from(psi) + val docs = javadocParser.parseDocumentation(psi) + return DFunction( + dri, + if (isConstructor) "<init>" else psi.name, + isConstructor, + psi.parameterList.parameters.map { psiParameter -> + DParameter( + dri.copy(target = dri.target.nextTarget()), + psiParameter.name, + DocumentationNode( + listOfNotNull(docs.firstChildOfTypeOrNull<Param> { + it.firstChildOfTypeOrNull<DocumentationLink>() + ?.firstChildOfTypeOrNull<Text>()?.body == psiParameter.name + })).toSourceSetDependent(), + null, + getBound(psiParameter.type), + setOf(sourceSetData) + ) + }, + docs.toSourceSetDependent(), + null, + PsiDocumentableSource(psi).toSourceSetDependent(), + psi.getVisibility().toSourceSetDependent(), + psi.returnType?.let { getBound(type = it) } ?: Void, + psi.mapTypeParameters(dri), + null, + psi.getModifier().toSourceSetDependent(), + setOf(sourceSetData), + psi.additionalExtras().let { + PropertyContainer.withAll( + InheritedFunction(inheritedFrom.toSourceSetDependent()), + it.toSourceSetDependent().toAdditionalModifiers(), + (psi.annotations.toList().toListOfAnnotations() + it.toListOfAnnotations()).toSourceSetDependent() + .toAnnotations() + ) + } + ) + } + + 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()) + } + + private fun getBound(type: PsiType): Bound = + cachedBounds.getOrPut(type.canonicalText) { + when (type) { + is PsiClassReferenceType -> { + val resolved: PsiClass = type.resolve() + ?: return UnresolvedBound(type.presentableText) + when { + resolved.qualifiedName == "java.lang.Object" -> JavaObject + resolved is PsiTypeParameter && resolved.owner != null -> + OtherParameter( + declarationDRI = DRI.from(resolved.owner!!), + name = resolved.name.orEmpty() + ) + else -> + TypeConstructor(DRI.from(resolved), type.parameters.map { getProjection(it) }) + } + } + is PsiArrayType -> TypeConstructor( + DRI("kotlin", "Array"), + listOf(getProjection(type.componentType)) + ) + is PsiPrimitiveType -> if (type.name == "void") Void else PrimitiveJavaType(type.name) + is PsiImmediateClassType -> JavaObject + else -> throw IllegalStateException("${type.presentableText} is not supported by PSI parser") + } + } + + private fun getVariance(type: PsiWildcardType): Projection = when { + type.extendsBound != PsiType.NULL -> Variance(Variance.Kind.Out, getBound(type.extendsBound)) + type.superBound != PsiType.NULL -> Variance(Variance.Kind.In, 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.copy(target = dri.target.nextTarget()), + type.name.orEmpty(), + javadocParser.parseDocumentation(type).toSourceSetDependent(), + null, + mapBounds(type.bounds), + setOf(sourceSetData) + ) + } + } + + private fun PsiMethod.getPropertyNameForFunction() = + getAnnotation(DescriptorUtils.JVM_NAME.asString())?.findAttributeValue("name")?.text + ?: when { + JvmAbi.isGetterName(name) -> propertyNameByGetMethodName(Name.identifier(name))?.asString() + JvmAbi.isSetterName(name) -> propertyNamesBySetMethodName(Name.identifier(name)).firstOrNull() + ?.asString() + else -> null + } + + private fun PsiClass.splitFunctionsAndAccessors(): Pair<MutableList<PsiMethod>, MutableMap<PsiField, MutableList<PsiMethod>>> { + val fieldNames = fields.map { it.name to it }.toMap() + val accessors = mutableMapOf<PsiField, MutableList<PsiMethod>>() + val regularMethods = mutableListOf<PsiMethod>() + methods.forEach { method -> + val field = method.getPropertyNameForFunction()?.let { name -> fieldNames[name] } + if (field != null) { + accessors.getOrPut(field, ::mutableListOf).add(method) + } else { + regularMethods.add(method) + } + } + return regularMethods to accessors + } + + private fun parseField(psi: PsiField, accessors: List<PsiMethod>): DProperty { + val dri = DRI.from(psi) + return DProperty( + dri, + psi.name, + javadocParser.parseDocumentation(psi).toSourceSetDependent(), + null, + PsiDocumentableSource(psi).toSourceSetDependent(), + psi.getVisibility().toSourceSetDependent(), + getBound(psi.type), + null, + accessors.firstOrNull { it.hasParameters() }?.let { parseFunction(it) }, + accessors.firstOrNull { it.returnType == psi.type }?.let { parseFunction(it) }, + psi.getModifier().toSourceSetDependent(), + setOf(sourceSetData), + emptyList(), + psi.additionalExtras().let { + PropertyContainer.withAll<DProperty>( + it.toSourceSetDependent().toAdditionalModifiers(), + (psi.annotations.toList().toListOfAnnotations() + it.toListOfAnnotations()).toSourceSetDependent() + .toAnnotations() + ) + } + ) + } + + private fun Collection<PsiAnnotation>.toListOfAnnotations() = + filter { it !is KtLightAbstractAnnotation }.mapNotNull { it.toAnnotation() } + + private fun JvmAnnotationAttribute.toValue(): AnnotationParameterValue = when (this) { + is PsiNameValuePair -> value?.toValue() ?: StringValue("") + else -> StringValue(this.attributeName) + } + + 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 psiClass = ((type as PsiImmediateClassType).parameters.single() as PsiClassReferenceType).resolve() + psiClass?.let { ClassValue(text ?: "", DRI.from(psiClass)) } + } + else -> StringValue(text ?: "") + } + + private fun PsiAnnotation.toAnnotation() = psiReference?.let { psiElement -> + Annotations.Annotation( + DRI.from(psiElement), + attributes.filter { it !is KtLightAbstractAnnotation }.mapNotNull { it.attributeName to it.toValue() } + .toMap(), + (psiElement as PsiClass).annotations.any { + it.hasQualifiedName("java.lang.annotation.Documented") + } + ) + } + + private val PsiElement.psiReference + get() = getChildOfType<PsiJavaCodeReferenceElement>()?.resolve() + } + + private data class AncestorLevel(val level: Int, val superclass: DRI?, val interfaces: List<DRI>) +} diff --git a/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt b/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt new file mode 100644 index 00000000..81955fde --- /dev/null +++ b/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt @@ -0,0 +1,216 @@ +package org.jetbrains.dokka.base.translators.psi + +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.LeafPsiElement +import com.intellij.psi.javadoc.* +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.dokka.analysis.from +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.kotlin.idea.refactoring.fqName.getKotlinFqName +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull +import org.jetbrains.kotlin.utils.addToStdlib.safeAs +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import org.jsoup.nodes.Node +import org.jsoup.nodes.TextNode + +interface JavaDocumentationParser { + fun parseDocumentation(element: PsiNamedElement): DocumentationNode +} + +class JavadocParser( + private val logger: DokkaLogger // TODO: Add logging +) : JavaDocumentationParser { + + override fun parseDocumentation(element: PsiNamedElement): DocumentationNode { + val docComment = findClosestDocComment(element) ?: return DocumentationNode(emptyList()) + val nodes = mutableListOf<TagWrapper>() + docComment.getDescription()?.let { nodes.add(it) } + nodes.addAll(docComment.tags.mapNotNull { tag -> + when (tag.name) { + "param" -> Param(P(convertJavadocElements(tag.dataElements.toList())), tag.text) + "throws" -> Throws(P(convertJavadocElements(tag.dataElements.toList())), tag.text) + "return" -> Return(P(convertJavadocElements(tag.dataElements.toList()))) + "author" -> Author(P(convertJavadocElements(tag.dataElements.toList()))) + "see" -> See(P(getSeeTagElementContent(tag)), tag.referenceElement()?.text.orEmpty(), null) + "deprecated" -> Deprecated(P(convertJavadocElements(tag.dataElements.toList()))) + else -> null + } + }) + return DocumentationNode(nodes) + } + + private fun findClosestDocComment(element: PsiNamedElement): PsiDocComment? { + (element as? PsiDocCommentOwner)?.docComment?.run { return this } + if (element is PsiMethod) { + val superMethods = element.findSuperMethodsOrEmptyArray() + if (superMethods.isEmpty()) return null + + if (superMethods.size == 1) { + return findClosestDocComment(superMethods.single()) + } + + val superMethodDocumentation = superMethods.map(::findClosestDocComment) + if (superMethodDocumentation.size == 1) { + return superMethodDocumentation.single() + } + + logger.warn( + "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 null + } + + /** + * 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) + */ + private fun PsiMethod.findSuperMethodsOrEmptyArray(): 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() + } + } + + private fun PsiClass.implementsInterface(fqName: FqName): Boolean { + return allInterfaces().any { it.getKotlinFqName() == fqName } + } + + private fun PsiClass.allInterfaces(): Sequence<PsiClass> { + return sequence { + this.yieldAll(interfaces.toList()) + interfaces.forEach { yieldAll(it.allInterfaces()) } + } + } + + private fun getSeeTagElementContent(tag: PsiDocTag): List<DocTag> = + listOfNotNull(tag.referenceElement()?.toDocumentationLink()) + + private fun PsiDocComment.getDescription(): Description? { + val nonEmptyDescriptionElements = descriptionElements.filter { it.text.trim().isNotEmpty() } + val convertedDescriptionElements = convertJavadocElements(nonEmptyDescriptionElements) + if (convertedDescriptionElements.isNotEmpty()) { + return Description(P(convertedDescriptionElements)) + } + + return null + } + + private fun convertJavadocElements(elements: Iterable<PsiElement>): List<DocTag> = + elements.mapNotNull { + when (it) { + is PsiReference -> convertJavadocElements(it.children.toList()) + is PsiInlineDocTag -> listOfNotNull(convertInlineDocTag(it)) + is PsiDocParamRef -> listOfNotNull(it.toDocumentationLink()) + is PsiDocTagValue, + is LeafPsiElement -> Jsoup.parse(it.text.trim()).body().childNodes().mapNotNull(::convertHtmlNode) + else -> null + } + }.flatten() + + private fun convertHtmlNode(node: Node, insidePre: Boolean = false): DocTag? = when (node) { + is TextNode -> Text(body = if (insidePre) node.wholeText else node.text()) + is Element -> createBlock(node) + else -> null + } + + private fun createBlock(element: Element): DocTag { + val children = element.childNodes().mapNotNull { convertHtmlNode(it) } + return when (element.tagName()) { + "p" -> P(listOf(Br, Br) + children) + "b" -> B(children) + "strong" -> Strong(children) + "i" -> I(children) + "em" -> Em(children) + "code" -> CodeBlock(children) + "pre" -> Pre(children) + "ul" -> Ul(children) + "ol" -> Ol(children) + "li" -> Li(children) + "a" -> createLink(element, children) + else -> Text(body = element.ownText()) + } + } + + private fun createLink(element: Element, children: List<DocTag>): DocTag { + return when { + element.hasAttr("docref") -> { + A(children, params = mapOf("docref" to element.attr("docref"))) + } + element.hasAttr("href") -> { + A(children, params = mapOf("href" to element.attr("href"))) + } + else -> Text(children = children) + } + } + + private fun PsiDocToken.isSharpToken() = tokenType.toString() == "DOC_TAG_VALUE_SHARP_TOKEN" + + private fun PsiElement.toDocumentationLink(labelElement: PsiElement? = null) = + reference?.resolve()?.let { + val dri = DRI.from(it) + val label = labelElement ?: children.firstOrNull { + it is PsiDocToken && it.text.isNotBlank() && !it.isSharpToken() + } ?: this + DocumentationLink(dri, convertJavadocElements(listOfNotNull(label))) + } + + private fun convertInlineDocTag(tag: PsiInlineDocTag) = when (tag.name) { + "link", "linkplain" -> { + tag.referenceElement()?.toDocumentationLink(tag.dataElements.firstIsInstanceOrNull<PsiDocToken>()) + } + "code", "literal" -> { + CodeInline(listOf(Text(tag.text))) + } + "index" -> Index(tag.children.filterIsInstance<PsiDocTagValue>().map { Text(it.text) }) + else -> Text(tag.text) + } + + private fun PsiDocTag.referenceElement(): PsiElement? = + linkElement()?.let { + if (it.node.elementType == JavaDocElementType.DOC_REFERENCE_HOLDER) { + PsiTreeUtil.findChildOfType(it, PsiJavaCodeReferenceElement::class.java) + } else { + it + } + } + + private fun PsiDocTag.linkElement(): PsiElement? = + valueElement ?: dataElements.firstOrNull { it !is PsiWhiteSpace } +} |