diff --git a/dokka-subprojects/analysis-java-psi/api/analysis-java-psi.api b/dokka-subprojects/analysis-java-psi/api/analysis-java-psi.api index 6b4d444f479..02d66284795 100644 --- a/dokka-subprojects/analysis-java-psi/api/analysis-java-psi.api +++ b/dokka-subprojects/analysis-java-psi/api/analysis-java-psi.api @@ -43,8 +43,7 @@ public abstract class org/jetbrains/dokka/analysis/java/JavadocTag { public final class org/jetbrains/dokka/analysis/java/ParamJavadocTag : org/jetbrains/dokka/analysis/java/JavadocTag { public static final field Companion Lorg/jetbrains/dokka/analysis/java/ParamJavadocTag$Companion; public static final field name Ljava/lang/String; - public fun (Lcom/intellij/psi/PsiMethod;Ljava/lang/String;I)V - public final fun getMethod ()Lcom/intellij/psi/PsiMethod; + public fun (Ljava/lang/String;I)V public final fun getParamIndex ()I public final fun getParamName ()Ljava/lang/String; } diff --git a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavadocTag.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavadocTag.kt index 23aee76413c..8db6e8d3c91 100644 --- a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavadocTag.kt +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/JavadocTag.kt @@ -4,7 +4,6 @@ package org.jetbrains.dokka.analysis.java -import com.intellij.psi.PsiMethod import org.jetbrains.dokka.InternalDokkaApi @InternalDokkaApi @@ -19,7 +18,6 @@ public object ReturnJavadocTag : JavadocTag("return") public object SinceJavadocTag : JavadocTag("since") public class ParamJavadocTag( - public val method: PsiMethod, public val paramName: String, public val paramIndex: Int ) : JavadocTag(name) { diff --git a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocComment.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocComment.kt index 066b516273f..2b32f21d8d6 100644 --- a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocComment.kt +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/doccomment/JavaDocComment.kt @@ -39,9 +39,7 @@ internal class JavaDocComment(val comment: PsiDocComment) : DocComment { val resolvedParamElements = comment.resolveTag(tag) .filterIsInstance() .map { it.contentElementsWithSiblingIfNeeded() } - .firstOrNull { - it.firstOrNull()?.text == tag.method.parameterList.parameters[tag.paramIndex].name - }.orEmpty() + .firstOrNull { it.firstOrNull()?.text == tag.paramName }.orEmpty() return resolvedParamElements .withoutReferenceLink() diff --git a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavaDocCommentParser.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavaDocCommentParser.kt index 1e4c9f2d3c3..4b88863be15 100644 --- a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavaDocCommentParser.kt +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/JavaDocCommentParser.kt @@ -4,6 +4,7 @@ package org.jetbrains.dokka.analysis.java.parsers +import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement import com.intellij.psi.PsiMethod import com.intellij.psi.PsiNamedElement @@ -73,18 +74,38 @@ internal class JavaPsiDocCommentParser( analysedElement: PsiNamedElement ): TagWrapper? { val paramName = tag.dataElements.firstOrNull()?.text.orEmpty() - - // can be a PsiClass if @param is referencing class generics, like here: - // https://github.com/biojava/biojava/blob/2417c230be36e4ba73c62bb3631b60f876265623/biojava-core/src/main/java/org/biojava/nbio/core/alignment/SimpleProfilePair.java#L43 - // not supported at the moment - val method = analysedElement as? PsiMethod ?: return null - val paramIndex = method.parameterList.parameters.map { it.name }.indexOf(paramName) + val paramIndex = when (analysedElement) { + // for functions `@param` can be used with both generics and arguments + // if it's for generics, + // then `paramName` will be in the form of ``, where `T` is a type parameter name + is PsiMethod -> when { + paramName.startsWith('<') -> { + val pName = paramName.removeSurrounding("<", ">") + analysedElement.typeParameters.indexOfFirst { it.name == pName } + } + + else -> analysedElement.parameterList.parameters.indexOfFirst { it.name == paramName } + } + + // for classes `@param` can be used with generics and `record` components + is PsiClass -> when { + paramName.startsWith('<') -> { + val pName = paramName.removeSurrounding("<", ">") + analysedElement.typeParameters.indexOfFirst { it.name == pName } + } + + else -> analysedElement.recordComponents.indexOfFirst { it.name == paramName } + } + + // if `@param` tag is on any other element - ignore it + else -> return null + } val docTags = psiDocTagParser.parseAsParagraph( psiElements = tag.contentElementsWithSiblingIfNeeded().drop(1), commentResolutionContext = CommentResolutionContext( comment = docComment, - tag = ParamJavadocTag(method, paramName, paramIndex) + tag = ParamJavadocTag(paramName, paramIndex) ) ) return Param(root = wrapTagIfNecessary(docTags), name = paramName) diff --git a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagResolver.kt b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagResolver.kt index 344f69b172c..d7856a2ec7d 100644 --- a/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagResolver.kt +++ b/dokka-subprojects/analysis-java-psi/src/main/kotlin/org/jetbrains/dokka/analysis/java/parsers/doctag/InheritDocTagResolver.kt @@ -78,19 +78,26 @@ internal class InheritDocTagResolver( paramTag: ParamJavadocTag, ): List { val parameterIndex = paramTag.paramIndex + if (parameterIndex < 0) return emptyList() - val methods = (currentElement.owner as? PsiMethod) - ?.let { method -> lowestMethodsWithTag(method, paramTag) } - .orEmpty() + val isTypeParameter = paramTag.paramName.startsWith("<") + + // while @param can be used for both classes and functions, + // it can be inherited only for functions + val methods = (currentElement.owner as? PsiMethod)?.let { + lowestMethodsWithTag(it, paramTag) + }.orEmpty() return methods.flatMap { - if (parameterIndex >= it.parameterList.parametersCount || parameterIndex < 0) { - return@flatMap emptyList() - } + val parameterName = when { + isTypeParameter -> it.typeParameters.getOrNull(parameterIndex)?.name + else -> it.parameterList.parameters.getOrNull(parameterIndex)?.name + } ?: return@flatMap emptyList() - val closestTag = docCommentFinder.findClosestToElement(it) - val hasTag = closestTag?.hasTag(paramTag) ?: false - closestTag?.takeIf { hasTag }?.resolveTag(ParamJavadocTag(it, "", parameterIndex)) ?: emptyList() + docCommentFinder.findClosestToElement(it) + ?.takeIf { it.hasTag(paramTag) } + ?.resolveTag(ParamJavadocTag(parameterName, parameterIndex)) + ?: emptyList() } } diff --git a/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/java/JavadocAnalysisTest.kt b/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/java/JavadocAnalysisTest.kt new file mode 100644 index 00000000000..19896a58c2d --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/java/JavadocAnalysisTest.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.jvm.java + +import org.jetbrains.dokka.analysis.test.api.javaTestProject +import org.jetbrains.dokka.analysis.test.api.parse +import org.jetbrains.dokka.model.SourceSetDependent +import org.jetbrains.dokka.model.doc.* +import kotlin.test.Test +import kotlin.test.assertEquals + +class JavadocAnalysisTest { + + @Test + fun `should parse javadoc @param for type parameter on class`() { + val testProject = javaTestProject { + javaFile(pathFromSrc = "Foo.java") { + +""" + /** + * class + * + * @param type parameter, + * long type parameter description + */ + public class Foo {} + """ + } + } + + val module = testProject.parse() + val pkg = module.packages.single() + val cls = pkg.classlikes.single() + + assertDocumentationNodeContains( + cls.documentation, + listOf( + Description(customDocTag("class")), + Param(customDocTag("type parameter, long type parameter description"), ""), + ) + ) + } + + @Test + fun `should parse javadoc @param for type parameter on function`() { + val testProject = javaTestProject { + javaFile(pathFromSrc = "Foo.java") { + +""" + public class Foo { + /** + * function + * + * @param type parameter, + * long type parameter description + */ + public void something(Bar bar) {} + } + """ + } + } + + val module = testProject.parse() + val pkg = module.packages.single() + val cls = pkg.classlikes.single() + val function = cls.functions.single { it.name == "something" } + + assertDocumentationNodeContains( + function.documentation, + listOf( + Description(customDocTag("function")), + Param(customDocTag("type parameter, long type parameter description"), ""), + ) + ) + } + + @Test + fun `should parse javadoc @param for parameter on function`() { + val testProject = javaTestProject { + javaFile(pathFromSrc = "Foo.java") { + +""" + public class Foo { + /** + * function + * + * @param bar parameter, + * long parameter description + */ + public void something(String bar) {} + } + """ + } + } + + val module = testProject.parse() + val pkg = module.packages.single() + val cls = pkg.classlikes.single() + val function = cls.functions.single { it.name == "something" } + + assertDocumentationNodeContains( + function.documentation, + listOf( + Description(customDocTag("function")), + Param(customDocTag("parameter, long parameter description"), "bar"), + ) + ) + } + + private fun customDocTag(text: String): CustomDocTag { + return CustomDocTag(listOf(P(listOf(Text(text)))), name = "MARKDOWN_FILE") + } + + private fun assertDocumentationNodeContains( + node: SourceSetDependent, + expected: List + ) { + assertEquals( + DocumentationNode(expected), + node.values.single() + ) + } +}