J2K: convert javadoc comments to kdoc

This commit is contained in:
Dmitry Jemerov
2015-03-03 18:29:42 +01:00
parent cbb8df954c
commit ac6a56761e
21 changed files with 448 additions and 11 deletions
@@ -16,10 +16,12 @@
package org.jetbrains.kotlin.j2k
import com.intellij.codeInsight.NullableNotNullManager
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.*
import com.intellij.psi.javadoc.PsiDocTag
import org.jetbrains.kotlin.j2k.ast.*
import org.jetbrains.kotlin.j2k.ast.Annotation
import com.intellij.codeInsight.NullableNotNullManager
class AnnotationConverter(private val converter: Converter) {
public val annotationsToRemove: Set<String> = (NullableNotNullManager.getInstance(converter.project).getNotNulls()
@@ -31,8 +33,7 @@ class AnnotationConverter(private val converter: Converter) {
private fun convertAnnotationsOnly(owner: PsiModifierListOwner): Annotations {
val modifierList = owner.getModifierList()
val annotations = modifierList?.getAnnotations()?.filter { it.getQualifiedName() !in annotationsToRemove }
if (annotations == null || annotations.isEmpty()) return Annotations.Empty
val annotations = modifierList?.getAnnotations()?.filter { it.getQualifiedName() !in annotationsToRemove }.orEmpty()
val newLines = run {
if (!modifierList!!.isInSingleLine()) {
@@ -48,8 +49,28 @@ class AnnotationConverter(private val converter: Converter) {
}
}
val list = annotations.map { convertAnnotation(it, owner is PsiLocalVariable, newLines) }.filterNotNull() //TODO: brackets are also needed for local classes
return Annotations(list).assignNoPrototype()
var list = annotations.map { convertAnnotation(it, owner is PsiLocalVariable, newLines) }.filterNotNull() //TODO: brackets are also needed for local classes
if (owner is PsiDocCommentOwner) {
val deprecatedAnnotation = convertDeprecatedJavadocTag(owner)
if (deprecatedAnnotation != null) {
list += deprecatedAnnotation
}
}
return if (list.isEmpty()) Annotations.Empty else Annotations(list).assignNoPrototype()
}
private fun convertDeprecatedJavadocTag(element: PsiDocCommentOwner): Annotation? {
val deprecatedTag = element.getDocComment()?.findTagByName("deprecated")
if (deprecatedTag != null) {
val deferredExpression = converter.deferredElement<Expression> {
LiteralExpression("\"${StringUtil.escapeStringCharacters(deprecatedTag.content())}\"").assignNoPrototype()
}
return Annotation(Identifier("deprecated").assignPrototype(deprecatedTag.getNameElement()),
listOf(null to deferredExpression), false, true)
.assignPrototype(deprecatedTag)
}
return null
}
private fun convertModifiersToAnnotations(owner: PsiModifierListOwner): Annotations {
@@ -16,14 +16,15 @@
package org.jetbrains.kotlin.j2k
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.*
import java.util.HashSet
import com.intellij.psi.javadoc.PsiDocComment
import org.jetbrains.kotlin.j2k.ast.CommentsAndSpacesInheritance
import org.jetbrains.kotlin.j2k.ast.Element
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
import java.util.ArrayList
import org.jetbrains.kotlin.j2k.ast.Element
import java.util.HashSet
import kotlin.platform.platformName
import org.jetbrains.kotlin.j2k.ast.CommentsAndSpacesInheritance
import com.intellij.openapi.util.text.StringUtil
fun<T> CodeBuilder.append(generators: Collection<() -> T>, separator: String, prefix: String = "", suffix: String = ""): CodeBuilder {
if (generators.isNotEmpty()) {
@@ -57,8 +58,13 @@ class CodeBuilder(private val topElement: PsiElement?) {
public fun append(text: String): CodeBuilder
= append(text, false)
private fun appendCommentOrWhiteSpace(element: PsiElement)
= append(element.getText()!!, element.isEndOfLineComment())
private fun appendCommentOrWhiteSpace(element: PsiElement) {
if (element is PsiDocComment) {
append(DocCommentConverter.convertDocComment(element), false)
} else {
append(element.getText()!!, element.isEndOfLineComment())
}
}
private fun append(text: String, endOfLineComment: Boolean = false): CodeBuilder {
if (text.isEmpty()) {
@@ -0,0 +1,185 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.j2k
import com.intellij.ide.highlighter.HtmlFileType
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.*
import com.intellij.psi.javadoc.PsiDocComment
import com.intellij.psi.javadoc.PsiDocTag
import com.intellij.psi.javadoc.PsiDocToken
import com.intellij.psi.javadoc.PsiInlineDocTag
import com.intellij.psi.xml.XmlFile
import com.intellij.psi.xml.XmlTag
import com.intellij.psi.xml.XmlText
import com.intellij.psi.xml.XmlTokenType
import java.util.Stack
object DocCommentConverter {
fun convertDocComment(docComment: PsiDocComment): String {
val htmlTextBuilder = StringBuilder()
htmlTextBuilder.appendJavadocElements(docComment.getDescriptionElements())
docComment.getTags().filter { it.getName() != "deprecated" }.forEach {
if (it.getName() == "see") {
htmlTextBuilder.append("@see ${convertJavadocLink(it.content())}\n")
} else {
htmlTextBuilder.appendJavadocElements(it.getChildren()).append("\n")
}
}
val html = htmlTextBuilder.toString()
if (html.trim().isEmpty() && docComment.findTagByName("deprecated") != null) {
// @deprecated was the only content of the doc comment; we can drop the comment
return ""
}
val htmlFile = PsiFileFactory.getInstance(docComment.getProject()).createFileFromText(
"javadoc.html", HtmlFileType.INSTANCE, html)
val htmlToMarkdownConverter = HtmlToMarkdownConverter()
htmlFile.accept(htmlToMarkdownConverter)
return htmlToMarkdownConverter.markdownBuilder.toString()
}
fun StringBuilder.appendJavadocElements(elements: Array<PsiElement>): StringBuilder {
elements.forEach {
if (it is PsiInlineDocTag) {
append(convertInlineDocTag(it))
} else {
append(it.getText())
}
}
return this
}
fun convertInlineDocTag(tag: PsiInlineDocTag) = when(tag.getName()) {
"code", "literal" -> {
val text = StringBuilder()
tag.getDataElements().forEach { text.append(it.getText()) }
val escaped = StringUtil.escapeXml(text.toString().trimLeading())
if (tag.getName() == "code") "<code>$escaped</code>" else escaped
}
"link", "linkplain" -> {
val valueElement = tag.linkElement()
val labelText = tag.getDataElements().firstOrNull { it is PsiDocToken }?.getText() ?: ""
val kdocLink = convertJavadocLink(valueElement?.getText())
val linkText = if (labelText.isEmpty()) kdocLink else StringUtil.escapeXml(labelText)
"<a docref=\"$kdocLink\">$linkText</a>"
}
else -> tag.getText()
}
fun convertJavadocLink(link: String?): String =
if (link != null) link.substringBefore('(').replace('#', '.') else ""
private fun PsiDocTag.linkElement(): PsiElement? =
getValueElement() ?: getDataElements().firstOrNull { it !is PsiWhiteSpace }
class HtmlToMarkdownConverter() : XmlRecursiveElementVisitor() {
enum class ListType { Ordered; Unordered }
val markdownBuilder = StringBuilder("/**")
var afterLineBreak: Boolean = false
var whitespaceIsPartOfText: Boolean = true
var currentListType = ListType.Unordered
override fun visitWhiteSpace(space: PsiWhiteSpace) {
super.visitWhiteSpace(space)
if (whitespaceIsPartOfText) {
appendPendingText()
markdownBuilder.append(space.getText())
if (space.textContains('\n')) {
afterLineBreak = true
}
}
}
override fun visitElement(element: PsiElement) {
super.visitElement(element)
val tokenType = element.getNode().getElementType()
if (tokenType == XmlTokenType.XML_DATA_CHARACTERS || tokenType == XmlTokenType.XML_CHAR_ENTITY_REF) {
appendPendingText()
markdownBuilder.append(element.getText())
}
}
override fun visitXmlTag(tag: XmlTag) {
withWhitespaceAsPartOfText(false) {
val oldListType = currentListType
val atLineStart = afterLineBreak
appendPendingText()
val (openingMarkdown, closingMarkdown) = getMarkdownForTag(tag, atLineStart)
markdownBuilder.append(openingMarkdown)
super.visitXmlTag(tag)
markdownBuilder.append(closingMarkdown)
currentListType = oldListType
}
}
override fun visitXmlText(text: XmlText?) {
withWhitespaceAsPartOfText(true) {
super.visitXmlText(text)
}
}
private inline fun withWhitespaceAsPartOfText(newValue: Boolean, block: () -> Unit) {
val oldValue = whitespaceIsPartOfText
whitespaceIsPartOfText = newValue
try {
block()
} finally {
whitespaceIsPartOfText = oldValue
}
}
private fun getMarkdownForTag(tag: XmlTag, atLineStart: Boolean): Pair<String, String> = when(tag.getName()) {
"b", "strong" -> "**" to "**"
"p" -> if (atLineStart) "\n * " to "" else "\n *\n *" to ""
"i", "em" -> "*" to "*"
"s", "del" -> "~~" to "~~"
"code" -> "`" to "`"
"a" -> if (tag.getAttributeValue("docref") != null) {
val docRef = tag.getAttributeValue("docref")
val innerText = tag.getValue().getText()
if (docRef == innerText) "[" to "]" else "[" to "][$docRef]"
} else {
"[" to "](${tag.getAttributeValue("href")})"
}
"ul" -> { currentListType = ListType.Unordered; "" to "" }
"ol" -> { currentListType = ListType.Ordered; "" to "" }
"li" -> if (currentListType == ListType.Unordered) " * " to "" else " 1. " to ""
else -> "" to ""
}
private fun appendPendingText() {
if (afterLineBreak ) {
markdownBuilder.append(" * ")
afterLineBreak = false
}
}
override fun visitXmlFile(file: XmlFile?) {
super.visitXmlFile(file)
markdownBuilder.append(" */")
}
}
}
fun PsiDocTag.content(): String =
getChildren()
.dropWhile { it.getNode().getElementType() == JavaDocTokenType.DOC_TAG_NAME }
.dropWhile { it is PsiWhiteSpace }
.filterNot { it.getNode().getElementType() == JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS }
.map { it.getText() }
.join("")
@@ -0,0 +1,6 @@
/**
* This is a deprecated class.
* @deprecated do not use
*/
class C {
}
@@ -0,0 +1,5 @@
/**
* This is a deprecated class.
*/
deprecated("do not use")
class C
@@ -0,0 +1,4 @@
/**
* @param T This is the <b>parameter</b> of class {@code C}
*/
class C<T> {}
@@ -0,0 +1,4 @@
/**
* @param T This is the **parameter** of class `C`
*/
class C<T>
@@ -0,0 +1,16 @@
/**
* We support the following HTML styles: <b>bold</b>, <i>italic</i>, <s>strikethrough</s>, <code>code</code>
* <p>Paragraph tags also work.</p>
* HTML entities (need to remain as is in Markdown): &amp; &lt; &gt; &quot;
* Made by <a href="http://www.jetbrains.com">JetBrains</a>
* <ul>
* <li>Kotlin</li>
* <li>Java</li>
* </ul>
* <ol>
* <li>First</li>
* <li>Second</li>
* </ol>
*/
public class C {
}
@@ -0,0 +1,16 @@
/**
* We support the following HTML styles: **bold**, *italic*, ~~strikethrough~~, `code`
*
* Paragraph tags also work.
* HTML entities (need to remain as is in Markdown): &amp; &lt; &gt; &quot;
* Made by [JetBrains](http://www.jetbrains.com)
*
* * Kotlin
* * Java
*
*
* 1. First
* 1. Second
*
*/
public class C
@@ -0,0 +1,5 @@
/**
* {@code A<B}
*/
public class C {
}
@@ -0,0 +1,4 @@
/**
* `A&lt;B`
*/
public class C
@@ -0,0 +1,7 @@
/**
* {@link C#foo(int)}
*/
class C {
void foo(int i) {
}
}
@@ -0,0 +1,7 @@
/**
* [C.foo]
*/
class C {
fun foo(i: Int) {
}
}
@@ -0,0 +1,7 @@
/**
* {@link C#foo(int) the best foo method ever}
*/
class C {
void foo(int i) {
}
}
@@ -0,0 +1,7 @@
/**
* [the best foo method ever][C.foo]
*/
class C {
fun foo(i: Int) {
}
}
@@ -0,0 +1,5 @@
/**
* @deprecated do not use
*/
class C {
}
@@ -0,0 +1,2 @@
deprecated("do not use")
class C
@@ -0,0 +1,7 @@
/**
* @see C#foo(int)
*/
class C {
void foo(int i) {
}
}
@@ -0,0 +1,7 @@
/**
* @see C.foo
*/
class C {
fun foo(i: Int) {
}
}
@@ -51,6 +51,7 @@ import java.util.regex.Pattern;
JavaToKotlinConverterForWebDemoTestGenerated.ContinueStatement.class,
JavaToKotlinConverterForWebDemoTestGenerated.DeclarationStatement.class,
JavaToKotlinConverterForWebDemoTestGenerated.DoWhileStatement.class,
JavaToKotlinConverterForWebDemoTestGenerated.DocComments.class,
JavaToKotlinConverterForWebDemoTestGenerated.DropAccessors.class,
JavaToKotlinConverterForWebDemoTestGenerated.Enum.class,
JavaToKotlinConverterForWebDemoTestGenerated.Equals.class,
@@ -1368,6 +1369,63 @@ public class JavaToKotlinConverterForWebDemoTestGenerated extends AbstractJavaTo
}
}
@TestMetadata("j2k/testData/fileOrElement/docComments")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class DocComments extends AbstractJavaToKotlinConverterForWebDemoTest {
public void testAllFilesPresentInDocComments() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("j2k/testData/fileOrElement/docComments"), Pattern.compile("^(.+)\\.java$"), true);
}
@TestMetadata("deprecatedDocTag.java")
public void testDeprecatedDocTag() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/docComments/deprecatedDocTag.java");
doTest(fileName);
}
@TestMetadata("docCommentWithParamTag.java")
public void testDocCommentWithParamTag() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/docComments/docCommentWithParamTag.java");
doTest(fileName);
}
@TestMetadata("htmlInDocComment.java")
public void testHtmlInDocComment() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/docComments/htmlInDocComment.java");
doTest(fileName);
}
@TestMetadata("inlineTagsInDocComment.java")
public void testInlineTagsInDocComment() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/docComments/inlineTagsInDocComment.java");
doTest(fileName);
}
@TestMetadata("linkTag.java")
public void testLinkTag() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/docComments/linkTag.java");
doTest(fileName);
}
@TestMetadata("linkTagWithLabel.java")
public void testLinkTagWithLabel() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/docComments/linkTagWithLabel.java");
doTest(fileName);
}
@TestMetadata("onlyDeprecatedDocTag.java")
public void testOnlyDeprecatedDocTag() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/docComments/onlyDeprecatedDocTag.java");
doTest(fileName);
}
@TestMetadata("seeTag.java")
public void testSeeTag() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/docComments/seeTag.java");
doTest(fileName);
}
}
@TestMetadata("j2k/testData/fileOrElement/dropAccessors")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@@ -51,6 +51,7 @@ import java.util.regex.Pattern;
JavaToKotlinConverterSingleFileTestGenerated.ContinueStatement.class,
JavaToKotlinConverterSingleFileTestGenerated.DeclarationStatement.class,
JavaToKotlinConverterSingleFileTestGenerated.DoWhileStatement.class,
JavaToKotlinConverterSingleFileTestGenerated.DocComments.class,
JavaToKotlinConverterSingleFileTestGenerated.DropAccessors.class,
JavaToKotlinConverterSingleFileTestGenerated.Enum.class,
JavaToKotlinConverterSingleFileTestGenerated.Equals.class,
@@ -1368,6 +1369,63 @@ public class JavaToKotlinConverterSingleFileTestGenerated extends AbstractJavaTo
}
}
@TestMetadata("j2k/testData/fileOrElement/docComments")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class DocComments extends AbstractJavaToKotlinConverterSingleFileTest {
public void testAllFilesPresentInDocComments() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("j2k/testData/fileOrElement/docComments"), Pattern.compile("^(.+)\\.java$"), true);
}
@TestMetadata("deprecatedDocTag.java")
public void testDeprecatedDocTag() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/docComments/deprecatedDocTag.java");
doTest(fileName);
}
@TestMetadata("docCommentWithParamTag.java")
public void testDocCommentWithParamTag() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/docComments/docCommentWithParamTag.java");
doTest(fileName);
}
@TestMetadata("htmlInDocComment.java")
public void testHtmlInDocComment() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/docComments/htmlInDocComment.java");
doTest(fileName);
}
@TestMetadata("inlineTagsInDocComment.java")
public void testInlineTagsInDocComment() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/docComments/inlineTagsInDocComment.java");
doTest(fileName);
}
@TestMetadata("linkTag.java")
public void testLinkTag() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/docComments/linkTag.java");
doTest(fileName);
}
@TestMetadata("linkTagWithLabel.java")
public void testLinkTagWithLabel() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/docComments/linkTagWithLabel.java");
doTest(fileName);
}
@TestMetadata("onlyDeprecatedDocTag.java")
public void testOnlyDeprecatedDocTag() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/docComments/onlyDeprecatedDocTag.java");
doTest(fileName);
}
@TestMetadata("seeTag.java")
public void testSeeTag() throws Exception {
String fileName = JetTestUtils.navigationMetadata("j2k/testData/fileOrElement/docComments/seeTag.java");
doTest(fileName);
}
}
@TestMetadata("j2k/testData/fileOrElement/dropAccessors")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)