diff --git a/core/src/main/kotlin/utilities/safeEnumValueOf.kt b/core/src/main/kotlin/utilities/safeEnumValueOf.kt deleted file mode 100644 index 9f4c23c940..0000000000 --- a/core/src/main/kotlin/utilities/safeEnumValueOf.kt +++ /dev/null @@ -1,4 +0,0 @@ -package org.jetbrains.dokka.utilities - -inline fun > enumValueOrNull(name: String): T? = - T::class.java.enumConstants.firstOrNull { it.name.equals(name, ignoreCase = true) } \ No newline at end of file diff --git a/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt b/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt index 6a39652ae9..4af8c7908a 100644 --- a/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt +++ b/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt @@ -16,7 +16,6 @@ import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.model.doc.* import org.jetbrains.dokka.model.doc.Deprecated import org.jetbrains.dokka.utilities.DokkaLogger -import org.jetbrains.dokka.utilities.enumValueOrNull import org.jetbrains.dokka.utilities.htmlEscape import org.jetbrains.kotlin.idea.kdoc.resolveKDocLink import org.jetbrains.kotlin.idea.base.utils.fqname.getKotlinFqName @@ -84,93 +83,106 @@ class JavadocParser( parseWithChildren = parseWithChildren ) - private fun parseDocTag(tag: PsiDocTag, docComment: PsiDocComment, analysedElement: PsiNamedElement): TagWrapper = - enumValueOrNull(tag.name)?.let { javadocTag -> - val resolutionContext = CommentResolutionContext(comment = docComment, tag = javadocTag) - when (resolutionContext.tag) { - JavadocTag.PARAM -> { - val name = tag.dataElements.firstOrNull()?.text.orEmpty() - val index = - (analysedElement as? PsiMethod)?.parameterList?.parameters?.map { it.name }?.indexOf(name) - Param( - wrapTagIfNecessary( - convertJavadocElements( - tag.contentElementsWithSiblingIfNeeded().drop(1), - context = resolutionContext.copy(name = name, parameterIndex = index) - ) - ), - name - ) - } - JavadocTag.THROWS, JavadocTag.EXCEPTION -> { - val resolved = tag.resolveToElement() - val dri = resolved?.let { DRI.from(it) } - val name = resolved?.getKotlinFqName()?.asString() - ?: tag.dataElements.firstOrNull()?.text.orEmpty() - Throws( - root = wrapTagIfNecessary( - convertJavadocElements( - tag.dataElements.drop(1), - context = resolutionContext.copy(name = name) - ) - ), - /* we always would like to have a fully qualified name as name, + private fun parseDocTag(tag: PsiDocTag, docComment: PsiDocComment, analysedElement: PsiNamedElement): TagWrapper { + val javadocTag = JavadocTag.lowercaseValueOfOrNull(tag.name) + if (javadocTag == null) { + return emptyTagWrapper(tag, docComment) + } + // Javadoc tag found + val resolutionContext = CommentResolutionContext(comment = docComment, tag = javadocTag) + return when (resolutionContext.tag) { + JavadocTag.PARAM -> { + val name = tag.dataElements.firstOrNull()?.text.orEmpty() + val index = + (analysedElement as? PsiMethod)?.parameterList?.parameters?.map { it.name }?.indexOf(name) + Param( + wrapTagIfNecessary( + convertJavadocElements( + tag.contentElementsWithSiblingIfNeeded().drop(1), + context = resolutionContext.copy(name = name, parameterIndex = index) + ) + ), + name + ) + } + + JavadocTag.THROWS, JavadocTag.EXCEPTION -> { + val resolved = tag.resolveToElement() + val dri = resolved?.let { DRI.from(it) } + val name = resolved?.getKotlinFqName()?.asString() + ?: tag.dataElements.firstOrNull()?.text.orEmpty() + Throws( + root = wrapTagIfNecessary( + convertJavadocElements( + tag.dataElements.drop(1), + context = resolutionContext.copy(name = name) + ) + ), + /* we always would like to have a fully qualified name as name, * because it will be used as a display name later and we would like to have those unified * even if documentation states shortened version * * Only if dri search fails we should use the provided phrase (since then we are not able to get a fq name) * */ - name = name, - exceptionAddress = dri + name = name, + exceptionAddress = dri + ) + } + + JavadocTag.RETURN -> Return( + wrapTagIfNecessary( + convertJavadocElements( + tag.contentElementsWithSiblingIfNeeded(), + context = resolutionContext ) - } - JavadocTag.RETURN -> Return( - wrapTagIfNecessary( - convertJavadocElements( - tag.contentElementsWithSiblingIfNeeded(), - context = resolutionContext - ) + ) + ) + + JavadocTag.AUTHOR -> Author( + wrapTagIfNecessary( + convertJavadocElements( + tag.contentElementsWithSiblingIfNeeded(), + context = resolutionContext ) ) - JavadocTag.AUTHOR -> Author( - wrapTagIfNecessary( - convertJavadocElements( - tag.contentElementsWithSiblingIfNeeded(), - context = resolutionContext - ) + ) // Workaround: PSI returns first word after @author tag as a `DOC_TAG_VALUE_ELEMENT`, then the rest as a `DOC_COMMENT_DATA`, so for `Name Surname` we get them parted + JavadocTag.SEE -> { + val name = + tag.resolveToElement()?.getKotlinFqName()?.asString() ?: tag.referenceElement()?.text.orEmpty() + .removePrefix("#") + getSeeTagElementContent(tag, resolutionContext.copy(name = name)).let { + See( + wrapTagIfNecessary(it.first), + name, + it.second ) - ) // Workaround: PSI returns first word after @author tag as a `DOC_TAG_VALUE_ELEMENT`, then the rest as a `DOC_COMMENT_DATA`, so for `Name Surname` we get them parted - JavadocTag.SEE -> { - val name = - tag.resolveToElement()?.getKotlinFqName()?.asString() ?: tag.referenceElement()?.text.orEmpty().removePrefix("#") - getSeeTagElementContent(tag, resolutionContext.copy(name = name)).let { - See( - wrapTagIfNecessary(it.first), - name, - it.second - ) - } } - JavadocTag.DEPRECATED -> Deprecated( - wrapTagIfNecessary( - convertJavadocElements( - tag.contentElementsWithSiblingIfNeeded(), - context = resolutionContext - ) - ) - ) - else -> null - //TODO https://github.com/Kotlin/dokka/issues/1618 } - } ?: CustomTagWrapper( - wrapTagIfNecessary( - convertJavadocElements( - tag.contentElementsWithSiblingIfNeeded(), - context = CommentResolutionContext(docComment, null) + + JavadocTag.DEPRECATED -> Deprecated( + wrapTagIfNecessary( + convertJavadocElements( + tag.contentElementsWithSiblingIfNeeded(), + context = resolutionContext + ) ) - ), - tag.name - ) + ) + + else -> emptyTagWrapper(tag, docComment) + } + } + + // Wrapper for unsupported tags https://github.com/Kotlin/dokka/issues/1618 + private fun emptyTagWrapper( + tag: PsiDocTag, + docComment: PsiDocComment + ) = CustomTagWrapper( + wrapTagIfNecessary( + convertJavadocElements( + tag.contentElementsWithSiblingIfNeeded(), + context = CommentResolutionContext(docComment, null) + )), tag.name + ) private fun wrapTagIfNecessary(list: List): CustomDocTag = if (list.size == 1 && (list.first() as? CustomDocTag)?.name == MarkdownElementTypes.MARKDOWN_FILE.name) diff --git a/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocTag.kt b/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocTag.kt index 869ced3026..747e2efee7 100644 --- a/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocTag.kt +++ b/plugins/base/src/main/kotlin/translators/psi/parsers/JavadocTag.kt @@ -17,4 +17,16 @@ internal enum class JavadocTag { SINCE, VERSION */ -} \ No newline at end of file + + companion object { + private val name2Value = values().associateBy { it.name.toLowerCase() } + + /** + * Lowercase-based `Enum.valueOf` variation for [JavadocTag]. + * + * Note: tags are [case-sensitive](https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html) in Java, + * thus we are not allowed to use case-insensitive or uppercase-based lookup. + */ + fun lowercaseValueOfOrNull(name: String): JavadocTag? = name2Value[name] + } +} diff --git a/plugins/base/src/test/kotlin/parsers/JavadocParserTest.kt b/plugins/base/src/test/kotlin/parsers/JavadocParserTest.kt index b2397b9529..cf1332db55 100644 --- a/plugins/base/src/test/kotlin/parsers/JavadocParserTest.kt +++ b/plugins/base/src/test/kotlin/parsers/JavadocParserTest.kt @@ -2,6 +2,7 @@ package parsers import com.jetbrains.rd.util.first import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.base.translators.psi.parsers.* import org.jetbrains.dokka.links.Callable import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.links.JavaClassReference @@ -13,7 +14,8 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import utils.docs import utils.text -import kotlin.test.assertNotNull +import kotlin.random.* +import kotlin.test.* class JavadocParserTest : BaseAbstractTest() { @@ -60,8 +62,12 @@ class JavadocParserTest : BaseAbstractTest() { @Test fun `correctly parsed list`() { performJavadocTest { module -> - val docs = (module.packages.single().classlikes.single() as DEnum).entries.single().documentation.values.single().children.single().root.text() - assertEquals("content being refreshed, which can be a result of invalidation, refresh that may contain content updates, or the initial load.", docs.trimEnd()) + val docs = + (module.packages.single().classlikes.single() as DEnum).entries.single().documentation.values.single().children.single().root.text() + assertEquals( + "content being refreshed, which can be a result of invalidation, refresh that may contain content updates, or the initial load.", + docs.trimEnd() + ) } } @@ -171,7 +177,8 @@ class JavadocParserTest : BaseAbstractTest() { kotlin.test.assertEquals( listOf( P(children = listOf(Text(body = "An example of using the literal tag "))), - Pre(children = + Pre( + children = listOf( Text(body = "@"), Text(body = "Entity\npublic class User {}\n") @@ -206,10 +213,12 @@ class JavadocParserTest : BaseAbstractTest() { kotlin.test.assertEquals( listOf( - P(children = listOf( - Text(body = "An example of using the literal tag "), - Text(body = "ac") - )), + P( + children = listOf( + Text(body = "An example of using the literal tag "), + Text(body = "ac") + ) + ), ), root.children ) @@ -355,7 +364,7 @@ class JavadocParserTest : BaseAbstractTest() { } } } - + @Test fun `header tags are handled properly`() { val source = """ @@ -424,10 +433,12 @@ class JavadocParserTest : BaseAbstractTest() { kotlin.test.assertEquals( listOf( - P(children = listOf( - Text("An example of using var tag: "), - Var(children = listOf(Text("variable"))), - )), + P( + children = listOf( + Text("An example of using var tag: "), + Var(children = listOf(Text("variable"))), + ) + ), ), root.children ) @@ -456,10 +467,12 @@ class JavadocParserTest : BaseAbstractTest() { assertEquals( listOf( - P(children = listOf( - Text("An example of using u tag: "), - U(children = listOf(Text("underlined"))), - )), + P( + children = listOf( + Text("An example of using u tag: "), + U(children = listOf(Text("underlined"))), + ) + ), ), root.children ) @@ -468,7 +481,7 @@ class JavadocParserTest : BaseAbstractTest() { } @Test - fun `undocumented see also from java`(){ + fun `undocumented see also from java`() { testInline( """ |/src/main/java/example/Source.java @@ -485,7 +498,8 @@ class JavadocParserTest : BaseAbstractTest() { """.trimIndent(), configuration ) { documentablesTransformationStage = { module -> - val functionWithSeeTag = module.packages.flatMap { it.classlikes }.flatMap { it.functions }.find { it.name == "getProperty" && it.parameters.count() == 1 } + val functionWithSeeTag = module.packages.flatMap { it.classlikes }.flatMap { it.functions } + .find { it.name == "getProperty" && it.parameters.count() == 1 } val seeTag = functionWithSeeTag?.docs()?.firstIsInstanceOrNull() val expectedLinkDestinationDRI = DRI( packageName = "example", @@ -505,7 +519,7 @@ class JavadocParserTest : BaseAbstractTest() { } @Test - fun `documented see also from java`(){ + fun `documented see also from java`() { testInline( """ |/src/main/java/example/Source.java @@ -522,7 +536,8 @@ class JavadocParserTest : BaseAbstractTest() { """.trimIndent(), configuration ) { documentablesTransformationStage = { module -> - val functionWithSeeTag = module.packages.flatMap { it.classlikes }.flatMap { it.functions }.find { it.name == "getProperty" && it.parameters.size == 1 } + val functionWithSeeTag = module.packages.flatMap { it.classlikes }.flatMap { it.functions } + .find { it.name == "getProperty" && it.parameters.size == 1 } val seeTag = functionWithSeeTag?.docs()?.firstIsInstanceOrNull() val expectedLinkDestinationDRI = DRI( packageName = "example", @@ -544,4 +559,58 @@ class JavadocParserTest : BaseAbstractTest() { } } } + + @Test + fun `tags are case-sensitive`() { + val source = """ + |/src/main/kotlin/test/Test.java + |package example + | + | /** + | * Java's tag with wrong case + | * {@liTeRal @}Entity + | * public class User {} + | */ + | public class Test {} + """.trimIndent() + testInline( + source, + configuration, + ) { + documentablesCreationStage = { modules -> + val docs = modules.first().packages.first().classlikes.single().documentation.first().value + val root = docs.children.first().root + + kotlin.test.assertEquals( + listOf( + Text(body = "Java's tag with wrong case {@liTeRal @}Entity public class User {}"), + ), + root.children.first().children + ) + } + } + } + + @Test + fun `test isolated parsing is case sensitive`() { + // Ensure that it won't accidentally break + val values = JavadocTag.values().map { it.toString().toLowerCase() } + val withRandomizedCapitalization = values.map { + val result = buildString { + for (char in it) { + if (Random.nextBoolean()) { + append(char) + } else { + append(char.toLowerCase()) + } + } + } + if (result == it) result.toUpperCase() else result + } + + for ((index, value) in JavadocTag.values().withIndex()) { + assertEquals(value, JavadocTag.lowercaseValueOfOrNull(values[index])) + assertNull(JavadocTag.lowercaseValueOfOrNull(withRandomizedCapitalization[index])) + } + } }