diff options
author | sebastian.sellmair <sebastian.sellmair@jetbrains.com> | 2020-06-05 09:07:20 +0200 |
---|---|---|
committer | Paweł Marks <Kordyjan@users.noreply.github.com> | 2020-06-10 10:52:43 +0200 |
commit | e9fd8b7bc00491b50e4822acc82e5615ab0bde3b (patch) | |
tree | 27648e766764df986a154e6c3353d634d219c0bc /plugins/base/src/main/kotlin | |
parent | 77c8777b7f66bddd374d68decd507547d356d602 (diff) | |
download | dokka-e9fd8b7bc00491b50e4822acc82e5615ab0bde3b.tar.gz dokka-e9fd8b7bc00491b50e4822acc82e5615ab0bde3b.tar.bz2 dokka-e9fd8b7bc00491b50e4822acc82e5615ab0bde3b.zip |
Implement `reportUndocumented` option to report undocumented code
Diffstat (limited to 'plugins/base/src/main/kotlin')
4 files changed, 210 insertions, 18 deletions
diff --git a/plugins/base/src/main/kotlin/DokkaBase.kt b/plugins/base/src/main/kotlin/DokkaBase.kt index c99372af..7a969b91 100644 --- a/plugins/base/src/main/kotlin/DokkaBase.kt +++ b/plugins/base/src/main/kotlin/DokkaBase.kt @@ -9,12 +9,12 @@ import org.jetbrains.dokka.base.signatures.SignatureProvider import org.jetbrains.dokka.base.resolvers.external.* import org.jetbrains.dokka.base.resolvers.local.DefaultLocationProviderFactory import org.jetbrains.dokka.base.resolvers.local.LocationProviderFactory -import org.jetbrains.dokka.base.transformers.documentables.ActualTypealiasAdder +import org.jetbrains.dokka.base.transformers.documentables.* import org.jetbrains.dokka.base.transformers.documentables.DefaultDocumentableMerger -import org.jetbrains.dokka.base.transformers.documentables.InheritorsExtractorTransformer -import org.jetbrains.dokka.base.transformers.pages.annotations.DeprecatedStrikethroughTransformer import org.jetbrains.dokka.base.transformers.documentables.DocumentableVisibilityFilter import org.jetbrains.dokka.base.transformers.documentables.ModuleAndPackageDocumentationTransformer +import org.jetbrains.dokka.base.transformers.documentables.ReportUndocumentedTransformer +import org.jetbrains.dokka.base.transformers.pages.annotations.DeprecatedStrikethroughTransformer import org.jetbrains.dokka.base.transformers.pages.comments.CommentsToContentConverter import org.jetbrains.dokka.base.transformers.pages.comments.DocTagToContentConverter import org.jetbrains.dokka.base.transformers.pages.merger.FallbackPageMergerStrategy @@ -73,6 +73,10 @@ class DokkaBase : DokkaPlugin() { CoreExtensions.documentableTransformer with ActualTypealiasAdder() } + val undocumentedCodeReporter by extending { + CoreExtensions.documentableTransformer with ReportUndocumentedTransformer() + } + val documentableToPageTranslator by extending(isFallback = true) { CoreExtensions.documentableToPageTranslator providing { ctx -> DefaultDocumentableToPageTranslator( @@ -175,7 +179,7 @@ class DokkaBase : DokkaPlugin() { } val sourcesetDependencyAppender by extending { - htmlPreprocessors providing ::SourcesetDependencyAppender order { after(rootCreator)} + htmlPreprocessors providing ::SourcesetDependencyAppender order { after(rootCreator) } } val allModulePageCreators by extending { diff --git a/plugins/base/src/main/kotlin/transformers/documentables/ReportUndocumentedTransformer.kt b/plugins/base/src/main/kotlin/transformers/documentables/ReportUndocumentedTransformer.kt new file mode 100644 index 00000000..4aa7632e --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/documentables/ReportUndocumentedTransformer.kt @@ -0,0 +1,157 @@ +package org.jetbrains.dokka.base.transformers.documentables + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfiguration.PassConfiguration +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.transformers.documentation.DocumentableTransformer +import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor +import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor.Kind.FAKE_OVERRIDE +import org.jetbrains.kotlin.utils.addToStdlib.safeAs + +internal class ReportUndocumentedTransformer : DocumentableTransformer { + + override fun invoke(original: DModule, context: DokkaContext): DModule = original.apply { + withDescendants().forEach { documentable -> invoke(documentable, context) } + } + + private fun invoke(documentable: Documentable, context: DokkaContext) { + documentable.sourceSets.forEach { sourceSet -> + if (shouldBeReportedIfNotDocumented(documentable, sourceSet, context)) { + reportIfUndocumented(context, documentable, sourceSet) + } + } + } + + private fun shouldBeReportedIfNotDocumented( + documentable: Documentable, sourceSet: SourceSetData, context: DokkaContext + ): Boolean { + val passConfiguration = passConfiguration(context, sourceSet) + val packageOptionsOrNull = packageOptionsOrNull(passConfiguration, documentable) + + if (!(packageOptionsOrNull?.reportUndocumented ?: passConfiguration.reportUndocumented)) { + return false + } + + if (documentable is DParameter || documentable is DPackage || documentable is DModule) { + return false + } + + if (isConstructor(documentable)) { + return false + } + + if (isFakeOverride(documentable, sourceSet)) { + return false + } + + if (isPrivateOrInternalApi(documentable, sourceSet)) { + return false + } + + return true + } + + private fun reportIfUndocumented( + context: DokkaContext, + documentable: Documentable, + sourceSet: SourceSetData + ) { + if (isUndocumented(documentable, sourceSet)) { + val documentableDescription = with(documentable) { + buildString { + dri.packageName?.run { + append(this) + append("/") + } + + dri.classNames?.run { + append(this) + append("/") + } + + dri.callable?.run { + append(name) + append("/") + append(signature()) + append("/") + } + + val sourceSetName = sourceSet.sourceSetName + if (sourceSetName != null.toString()) { + append(" ($sourceSetName)") + } + } + } + + context.logger.warn("Undocumented: $documentableDescription") + } + } + + private fun isUndocumented(documentable: Documentable, sourceSet: SourceSetData): Boolean { + fun resolveDependentSourceSets(sourceSet: SourceSetData): List<SourceSetData> { + return sourceSet.dependentSourceSets.map { sourceSetName -> + documentable.sourceSets.single { it.sourceSetName == sourceSetName } + } + } + + fun flatDependentSourceSetsTree(sourceSet: SourceSetData): List<SourceSetData> { + return listOf(sourceSet) + resolveDependentSourceSets(sourceSet) + .flatMap { resolveDependentSourceSets -> flatDependentSourceSetsTree(resolveDependentSourceSets) } + } + + return flatDependentSourceSetsTree(sourceSet).all { sourceSetOrDependentSourceSet -> + documentable.documentation[sourceSetOrDependentSourceSet]?.children?.isEmpty() ?: true + } + } + + private fun isConstructor(documentable: Documentable): Boolean { + if (documentable !is DFunction) return false + return documentable.isConstructor + } + + private fun passConfiguration(context: DokkaContext, sourceSet: SourceSetData): PassConfiguration { + return context.configuration.passesConfigurations.single { configuration -> + // TODO: Use sourceSetID after gradle-rewrite + configuration.sourceSetName == sourceSet.sourceSetName && + configuration.analysisPlatform == sourceSet.platform + } + } + + private fun isFakeOverride(documentable: Documentable, sourceSet: SourceSetData): Boolean { + if (documentable is WithExpectActual) { + val callableMemberDescriptor = documentable.sources[sourceSet] + .safeAs<DescriptorDocumentableSource>()?.descriptor + .safeAs<CallableMemberDescriptor>() + + if (callableMemberDescriptor?.kind == FAKE_OVERRIDE) { + return true + } + } + return false + } + + private fun isPrivateOrInternalApi(documentable: Documentable, sourceSet: SourceSetData): Boolean { + return when (documentable.safeAs<WithVisibility>()?.visibility?.get(sourceSet)) { + KotlinVisibility.Public -> false + KotlinVisibility.Private -> true + KotlinVisibility.Protected -> true + KotlinVisibility.Internal -> true + JavaVisibility.Public -> false + JavaVisibility.Private -> true + JavaVisibility.Protected -> true + JavaVisibility.Default -> true + null -> false + } + } + + private fun packageOptionsOrNull( + passConfiguration: PassConfiguration, + documentable: Documentable + ): DokkaConfiguration.PackageOptions? { + val packageName = documentable.dri.packageName ?: return null + return passConfiguration.perPackageOptions + .filter { packageOptions -> packageName.startsWith(packageOptions.prefix) } + .maxBy { packageOptions -> packageOptions.prefix.length } + } +} diff --git a/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt index f3b047bc..9f01267e 100644 --- a/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt +++ b/plugins/base/src/main/kotlin/translators/descriptors/DefaultDescriptorToDocumentableTranslator.kt @@ -1,6 +1,5 @@ package org.jetbrains.dokka.base.translators.descriptors -import org.jetbrains.kotlin.descriptors.annotations.Annotated import org.jetbrains.dokka.analysis.DokkaResolutionFacade import org.jetbrains.dokka.links.* import org.jetbrains.dokka.links.Callable @@ -11,15 +10,16 @@ import org.jetbrains.dokka.model.doc.* import org.jetbrains.dokka.model.properties.PropertyContainer import org.jetbrains.dokka.parsers.MarkdownParser import org.jetbrains.dokka.plugability.DokkaContext -import org.jetbrains.dokka.utilities.DokkaLogger import org.jetbrains.dokka.transformers.sources.SourceToDocumentableTranslator -import org.jetbrains.kotlin.asJava.classes.tryResolveMarkerInterfaceFQName +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.CallableMemberDescriptor.Kind.FAKE_OVERRIDE 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 @@ -29,15 +29,10 @@ 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.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 -import org.jetbrains.kotlin.resolve.constants.KClassValue.Value.NormalClass 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.getAllSuperclassesWithoutAny -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 @@ -46,12 +41,13 @@ 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.firstIsInstance import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull import org.jetbrains.kotlin.utils.addToStdlib.safeAs import java.nio.file.Paths -import kotlin.IllegalArgumentException -import kotlin.reflect.jvm.internal.impl.resolve.constants.KClassValue +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 object DefaultDescriptorToDocumentableTranslator : SourceToDocumentableTranslator { @@ -272,7 +268,7 @@ private class DokkaDescriptorVisitor( supertypes = info.supertypes.toSourceSetDependent(), generics = descriptor.declaredTypeParameters.map { it.toTypeParameter() }, documentation = info.docs, - modifier = descriptor.modifier().toSourceSetDependent(), + modifier = descriptor.modifier().toSourceSetDependent(), companion = descriptor.companion(driWithPlatform), sourceSets = listOf(sourceSet), extra = PropertyContainer.withAll(descriptor.additionalExtras(), descriptor.getAnnotations()) diff --git a/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt b/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt index 2698fd0a..e77c06a4 100644 --- a/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt +++ b/plugins/base/src/main/kotlin/translators/psi/JavadocParser.kt @@ -1,6 +1,7 @@ package org.jetbrains.dokka.base.translators.psi import com.intellij.psi.* +import com.intellij.psi.impl.source.PsiFieldImpl import com.intellij.psi.impl.source.javadoc.PsiDocParamRef import com.intellij.psi.impl.source.tree.JavaDocElementType import com.intellij.psi.impl.source.tree.LeafPsiElement @@ -26,7 +27,7 @@ class JavadocParser( ) : JavaDocumentationParser { override fun parseDocumentation(element: PsiNamedElement): DocumentationNode { - val docComment = (element as? PsiDocCommentOwner)?.docComment ?: return DocumentationNode(emptyList()) + val docComment = findClosestDocComment(element) ?: return DocumentationNode(emptyList()) val nodes = mutableListOf<TagWrapper>() docComment.getDescription()?.let { nodes.add(it) } nodes.addAll(docComment.tags.mapNotNull { tag -> @@ -43,6 +44,40 @@ class JavadocParser( return DocumentationNode(nodes) } + private fun findClosestDocComment(element: PsiNamedElement): PsiDocComment? { + (element as? PsiDocCommentOwner)?.docComment?.run { return this } + if (element is PsiMethod) { + val superMethods = element.findSuperMethods() + 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 + } + private fun getSeeTagElementContent(tag: PsiDocTag): List<DocTag> = listOfNotNull(tag.referenceElement()?.toDocumentationLink()) |