aboutsummaryrefslogtreecommitdiff
path: root/plugins/base
diff options
context:
space:
mode:
authorVsevolod Tolstopyatov <qwwdfsad@gmail.com>2023-03-21 06:48:22 -0700
committerGitHub <noreply@github.com>2023-03-21 16:48:22 +0300
commitff5aa6721063599d8b68a245c482621b0d110fc6 (patch)
tree49b6a876ec0793a86ccd9c0e5284038072cea6b3 /plugins/base
parente66f9d8711b5c1ba5e75fdcde8cfe998042c294a (diff)
downloaddokka-ff5aa6721063599d8b68a245c482621b0d110fc6.tar.gz
dokka-ff5aa6721063599d8b68a245c482621b0d110fc6.tar.bz2
dokka-ff5aa6721063599d8b68a245c482621b0d110fc6.zip
Improve JavadocParser and fix case-sensitivity (#2905)
* Get rid of safeEnumValueOf that was an unnecessary public API burden and constantly allocating * Restructure JavadocParser.parseDocTag, so it has one lever of nesting less * Make tag parsing case-sensitive Fixes #2907
Diffstat (limited to 'plugins/base')
-rw-r--r--plugins/base/src/main/kotlin/translators/psi/parsers/JavadocParser.kt164
-rw-r--r--plugins/base/src/main/kotlin/translators/psi/parsers/JavadocTag.kt14
-rw-r--r--plugins/base/src/test/kotlin/parsers/JavadocParserTest.kt111
3 files changed, 191 insertions, 98 deletions
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 6a39652a..4af8c790 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<JavadocTag>(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<DocTag>): 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 869ced30..747e2efe 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 b2397b95..cf1332db 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 = "a<B>c")
- )),
+ P(
+ children = listOf(
+ Text(body = "An example of using the literal tag "),
+ Text(body = "a<B>c")
+ )
+ ),
),
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<See>()
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<See>()
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]))
+ }
+ }
}