Skip to content

Commit

Permalink
Support parsing javadoc @param for classes (#3391)
Browse files Browse the repository at this point in the history
  • Loading branch information
whyoleg authored Dec 12, 2023
1 parent 3d8be6c commit 280a160
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (Lcom/intellij/psi/PsiMethod;Ljava/lang/String;I)V
public final fun getMethod ()Lcom/intellij/psi/PsiMethod;
public fun <init> (Ljava/lang/String;I)V
public final fun getParamIndex ()I
public final fun getParamName ()Ljava/lang/String;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

package org.jetbrains.dokka.analysis.java

import com.intellij.psi.PsiMethod
import org.jetbrains.dokka.InternalDokkaApi

@InternalDokkaApi
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ internal class JavaDocComment(val comment: PsiDocComment) : DocComment {
val resolvedParamElements = comment.resolveTag(tag)
.filterIsInstance<PsiDocTag>()
.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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 `<T>`, 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,26 @@ internal class InheritDocTagResolver(
paramTag: ParamJavadocTag,
): List<DocumentationContent> {
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()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* 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 JavadocParamAnalysisTest {

@Test
fun `should parse javadoc @param for type parameter on class`() {
val testProject = javaTestProject {
javaFile(pathFromSrc = "Foo.java") {
+"""
/**
* class
*
* @param <Bar> type parameter,
* long type parameter description
*/
public class Foo<Bar> {}
"""
}
}

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"), "<Bar>"),
)
)
}

@Test
fun `should parse javadoc @param for type parameter on function`() {
val testProject = javaTestProject {
javaFile(pathFromSrc = "Foo.java") {
+"""
public class Foo {
/**
* function
*
* @param <Bar> type parameter,
* long type parameter description
*/
public <Bar> 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"), "<Bar>"),
)
)
}

@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"),
)
)
}

// this test just freezes current behavior - correct way to annotate type parameter is `<Bar>` not `Bar`
@Test
fun `should parse javadoc @param for type parameter without angle brackets on function`() {
val testProject = javaTestProject {
javaFile(pathFromSrc = "Foo.java") {
+"""
public class Foo {
/**
* function
*
* @param Bar type parameter,
* long type parameter description
*/
public <Bar> 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"), "Bar"),
)
)
}


private fun customDocTag(text: String): CustomDocTag {
return CustomDocTag(listOf(P(listOf(Text(text)))), name = "MARKDOWN_FILE")
}

private fun assertDocumentationNodeContains(
node: SourceSetDependent<DocumentationNode>,
expected: List<TagWrapper>
) {
assertEquals(
DocumentationNode(expected),
node.values.single()
)
}
}

0 comments on commit 280a160

Please sign in to comment.