Skip to content

Commit f29bdba

Browse files
committed
Render UtBot custom JavaDoc tags correctly #565
1 parent d2706a3 commit f29bdba

File tree

4 files changed

+248
-9
lines changed

4 files changed

+248
-9
lines changed

utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/javadoc/UtCustomJavaDocTagProvider.kt

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ class UtCustomJavaDocTagProvider : CustomJavadocTagProvider {
2323
UtCustomTag.ThrowsException,
2424
)
2525

26-
sealed class UtCustomTag(private val name: String) : JavadocTagInfo {
26+
sealed class UtCustomTag(private val name: String, private val message: String) : JavadocTagInfo {
2727
override fun getName(): String = name
2828

29+
fun getMessage(): String = message
30+
2931
override fun isInline() = false
3032

3133
override fun checkTagValue(value: PsiDocTagValue?): String? = null
@@ -36,13 +38,13 @@ class UtCustomJavaDocTagProvider : CustomJavadocTagProvider {
3638
return element is PsiMethod
3739
}
3840

39-
object ClassUnderTest : UtCustomTag("utbot.classUnderTest")
40-
object MethodUnderTest : UtCustomTag("utbot.methodUnderTest")
41-
object ExpectedResult : UtCustomTag("utbot.expectedResult")
42-
object ActualResult : UtCustomTag("utbot.actualResult")
43-
object Executes : UtCustomTag("utbot.executes")
44-
object Invokes : UtCustomTag("utbot.invokes")
45-
object ReturnsFrom : UtCustomTag("utbot.returnsFrom")
46-
object ThrowsException : UtCustomTag("utbot.throwsException")
41+
object ClassUnderTest : UtCustomTag("utbot.classUnderTest", "Class under test")
42+
object MethodUnderTest : UtCustomTag("utbot.methodUnderTest", "Method under test")
43+
object ExpectedResult : UtCustomTag("utbot.expectedResult", "Expected result")
44+
object ActualResult : UtCustomTag("utbot.actualResult", "Actual result")
45+
object Executes : UtCustomTag("utbot.executes", "Executes")
46+
object Invokes : UtCustomTag("utbot.invokes", "Invokes")
47+
object ReturnsFrom : UtCustomTag("utbot.returnsFrom", "Returns from")
48+
object ThrowsException : UtCustomTag("utbot.throwsException", "Throws exception")
4749
}
4850
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.utbot.intellij.plugin.javadoc
2+
3+
import com.intellij.codeInsight.javadoc.JavaDocExternalFilter
4+
import com.intellij.codeInsight.javadoc.JavaDocInfoGenerator
5+
import com.intellij.lang.java.JavaDocumentationProvider
6+
import com.intellij.psi.PsiDocCommentBase
7+
import com.intellij.psi.PsiJavaDocumentedElement
8+
import com.intellij.psi.javadoc.PsiDocComment
9+
10+
/**
11+
* To render UtBot custom JavaDoc tags correctly, we need to override the way it generates HTML tags for comments.
12+
* We get JavaDoc info generated by IJ platform and include sections related to UTBot,
13+
* each section relates to the specific JavaDoc tag.
14+
* It renders text, code, and links.
15+
*/
16+
class UtDocumentationProvider : JavaDocumentationProvider() {
17+
override fun generateRenderedDoc(comment: PsiDocCommentBase): String {
18+
var target = comment.owner
19+
if (target == null) target = comment
20+
val docComment: PsiDocComment?
21+
var finalJavaDoc = ""
22+
if (target is PsiJavaDocumentedElement) {
23+
docComment = target.docComment
24+
if (docComment != null) {
25+
val baseJavaDocInfoGenerator = JavaDocInfoGenerator(target.project, target)
26+
val baseJavaDocInfo = baseJavaDocInfoGenerator.generateRenderedDocInfo()
27+
val utJavaDocInfoGenerator = UtJavaDocInfoGenerator()
28+
val javaDocInfoWithUtSections =
29+
utJavaDocInfoGenerator.addUtBotSpecificSectionsToJavaDoc(baseJavaDocInfo, docComment)
30+
finalJavaDoc = JavaDocExternalFilter.filterInternalDocInfo(javaDocInfoWithUtSections)!!
31+
}
32+
}
33+
return finalJavaDoc
34+
}
35+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package org.utbot.intellij.plugin.javadoc
2+
3+
import com.intellij.codeInsight.documentation.DocumentationManagerUtil
4+
import com.intellij.codeInsight.javadoc.JavaDocUtil
5+
import com.intellij.lang.documentation.DocumentationMarkup
6+
import com.intellij.openapi.project.DumbService
7+
import com.intellij.openapi.project.IndexNotReadyException
8+
import com.intellij.openapi.util.text.StringUtil
9+
import com.intellij.psi.*
10+
import com.intellij.psi.javadoc.PsiDocComment
11+
import com.intellij.psi.javadoc.PsiDocTag
12+
import com.intellij.psi.javadoc.PsiDocToken
13+
import com.intellij.psi.javadoc.PsiInlineDocTag
14+
import mu.KotlinLogging
15+
16+
private const val LINK_TAG = "link"
17+
private const val LINKPLAIN_TAG = "linkplain"
18+
private const val LITERAL_TAG = "literal"
19+
private const val CODE_TAG = "code"
20+
private const val SYSTEM_PROPERTY_TAG = "systemProperty"
21+
private const val MESSAGE_SEPARATOR = ":"
22+
23+
private val logger = KotlinLogging.logger {}
24+
25+
class UtJavaDocInfoGenerator {
26+
/**
27+
* Generates UtBot specific sections to include them to rendered JavaDoc comment.
28+
*/
29+
fun addUtBotSpecificSectionsToJavaDoc(javadoc: String?, comment: PsiDocComment): String {
30+
val builder: StringBuilder = StringBuilder(javadoc)
31+
generateUtTagSection(builder, comment, UtCustomJavaDocTagProvider.UtCustomTag.ClassUnderTest)
32+
generateUtTagSection(builder, comment, UtCustomJavaDocTagProvider.UtCustomTag.MethodUnderTest)
33+
generateUtTagSection(builder, comment, UtCustomJavaDocTagProvider.UtCustomTag.Invokes)
34+
generateUtTagSection(builder, comment, UtCustomJavaDocTagProvider.UtCustomTag.Executes)
35+
generateUtTagSection(builder, comment, UtCustomJavaDocTagProvider.UtCustomTag.ExpectedResult)
36+
generateUtTagSection(builder, comment, UtCustomJavaDocTagProvider.UtCustomTag.ActualResult)
37+
generateUtTagSection(builder, comment, UtCustomJavaDocTagProvider.UtCustomTag.ReturnsFrom)
38+
generateUtTagSection(builder, comment, UtCustomJavaDocTagProvider.UtCustomTag.ThrowsException)
39+
return builder.toString()
40+
}
41+
42+
/**
43+
* Searches for UtBot tag in the comment and generates a related section for it.
44+
*/
45+
private fun generateUtTagSection(
46+
builder: StringBuilder,
47+
comment: PsiDocComment?,
48+
utTag: UtCustomJavaDocTagProvider.UtCustomTag
49+
) {
50+
if (comment != null) {
51+
val tag = comment.findTagByName(utTag.name) ?: return
52+
startHeaderSection(builder, utTag.getMessage())?.append("<p>")
53+
val tmp = StringBuilder()
54+
generateValue(tmp, tag.dataElements)
55+
builder.append(tmp.toString().trim { it <= ' ' })
56+
builder.append(DocumentationMarkup.SECTION_END)
57+
}
58+
}
59+
60+
private fun startHeaderSection(builder: StringBuilder, message: String): StringBuilder? {
61+
return builder.append(DocumentationMarkup.SECTION_HEADER_START)
62+
.append(message)
63+
.append(MESSAGE_SEPARATOR)
64+
.append(DocumentationMarkup.SECTION_SEPARATOR)
65+
}
66+
67+
/**
68+
* Generates info depending on tag's value type.
69+
*/
70+
private fun generateValue(builder: StringBuilder, elements: Array<PsiElement>) {
71+
var offset = if (elements.isNotEmpty()) {
72+
elements[0].textOffset + elements[0].text.length
73+
} else 0
74+
75+
for (i in elements.indices) {
76+
if (elements[i].textOffset > offset) builder.append(' ')
77+
offset = elements[i].textOffset + elements[i].text.length
78+
val element = elements[i]
79+
if (element is PsiInlineDocTag) {
80+
when (element.name) {
81+
LITERAL_TAG -> generateLiteralValue(builder, element)
82+
CODE_TAG, SYSTEM_PROPERTY_TAG -> generateCodeValue(element, builder)
83+
LINK_TAG -> generateLinkValue(element, builder, false)
84+
LINKPLAIN_TAG -> generateLinkValue(element, builder, true)
85+
}
86+
} else {
87+
appendPlainText(builder, element.text)
88+
}
89+
}
90+
}
91+
92+
private fun appendPlainText(builder: StringBuilder, text: String) {
93+
builder.append(StringUtil.replaceUnicodeEscapeSequences(text))
94+
}
95+
96+
private fun collectElementText(builder: StringBuilder, element: PsiElement) {
97+
element.accept(object : PsiRecursiveElementWalkingVisitor() {
98+
override fun visitElement(element: PsiElement) {
99+
super.visitElement(element)
100+
if (element is PsiWhiteSpace ||
101+
element is PsiJavaToken ||
102+
element is PsiDocToken && element.tokenType !== JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS
103+
) {
104+
builder.append(element.text)
105+
}
106+
}
107+
})
108+
}
109+
110+
private fun generateCodeValue(tag: PsiInlineDocTag, builder: StringBuilder) {
111+
builder.append("<code>")
112+
val pos = builder.length
113+
generateLiteralValue(builder, tag)
114+
builder.append("</code>")
115+
if (builder[pos] == '\n') builder.insert(
116+
pos,
117+
' '
118+
) // line break immediately after opening tag is ignored by JEditorPane
119+
}
120+
121+
private fun generateLiteralValue(builder: StringBuilder, tag: PsiDocTag) {
122+
val tmpBuilder = StringBuilder()
123+
val children = tag.children
124+
for (i in 2 until children.size - 1) { // process all children except tag opening/closing elements
125+
val child = children[i]
126+
if (child is PsiDocToken && child.tokenType === JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS) continue
127+
var elementText = child.text
128+
if (child is PsiWhiteSpace) {
129+
val pos = elementText.lastIndexOf('\n')
130+
if (pos >= 0) elementText = elementText.substring(0, pos + 1) // skip whitespace before leading asterisk
131+
}
132+
appendPlainText(tmpBuilder, StringUtil.escapeXmlEntities(elementText))
133+
}
134+
builder.append(StringUtil.trimLeading(tmpBuilder))
135+
}
136+
137+
private fun generateLinkValue(tag: PsiInlineDocTag, builder: StringBuilder, plainLink: Boolean) {
138+
val tagElements = tag.dataElements
139+
val linkText: String = createLinkText(tagElements)
140+
if (linkText.isNotEmpty()) {
141+
val index = JavaDocUtil.extractReference(linkText)
142+
val referenceText = linkText.substring(0, index).trim { it <= ' ' }
143+
val label = StringUtil.nullize(linkText.substring(index).trim { it <= ' ' })
144+
generateLink(builder, referenceText, label, tagElements[0], plainLink)
145+
}
146+
}
147+
148+
private fun createLinkText(tagElements: Array<PsiElement>): String {
149+
var offset = if (tagElements.isNotEmpty()) {
150+
tagElements[0].textOffset + tagElements[0].text.length
151+
} else {
152+
0
153+
}
154+
155+
val builder = StringBuilder()
156+
for (i in tagElements.indices) {
157+
val tagElement = tagElements[i]
158+
if (tagElement.textOffset > offset) builder.append(' ')
159+
offset = tagElement.textOffset + tagElement.text.length
160+
collectElementText(builder, tagElement)
161+
if (i < tagElements.size - 1) {
162+
builder.append(' ')
163+
}
164+
}
165+
return builder.toString().trim { it <= ' ' }
166+
}
167+
168+
private fun generateLink(
169+
builder: StringBuilder,
170+
refText: String?,
171+
label: String?,
172+
context: PsiElement,
173+
plainLink: Boolean
174+
) {
175+
var linkLabel = label
176+
if (label == null) {
177+
val manager = context.manager
178+
linkLabel = JavaDocUtil.getLabelText(manager.project, manager, refText, context)
179+
}
180+
181+
var target: PsiElement? = null
182+
try {
183+
if (refText != null) {
184+
target = JavaDocUtil.findReferenceTarget(context.manager, refText, context)
185+
}
186+
} catch (e: IndexNotReadyException) {
187+
logger.info(e) { "Failed to find a reference while generating JavaDoc comment. Details: ${e.message}" }
188+
}
189+
190+
if (target == null && DumbService.isDumb(context.project)) {
191+
builder.append(linkLabel)
192+
} else if (target == null) {
193+
builder.append("<font color=red>").append(linkLabel).append("</font>")
194+
} else {
195+
val referenceText = JavaDocUtil.getReferenceText(target.project, target)
196+
if (referenceText != null) {
197+
DocumentationManagerUtil.createHyperlink(builder, target, referenceText, linkLabel, plainLink)
198+
}
199+
}
200+
}
201+
}

utbot-intellij/src/main/resources/META-INF/plugin.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
<projectModelModifier implementation="org.utbot.intellij.plugin.util.UtProjectModelModifier"/>
3737
<!--Documentation-->
3838
<customJavadocTagProvider implementation="org.utbot.intellij.plugin.javadoc.UtCustomJavaDocTagProvider"/>
39+
<lang.documentationProvider language="JAVA" order="first" implementationClass="org.utbot.intellij.plugin.javadoc.UtDocumentationProvider"/>
3940
</extensions>
4041

4142
<!-- Minimum and maximum build of IDE compatible with the plugin -->

0 commit comments

Comments
 (0)