From ac6a56761e01fc267e5daf74f74ea29222dbf57e Mon Sep 17 00:00:00 2001 From: Dmitry Jemerov Date: Tue, 3 Mar 2015 18:29:42 +0100 Subject: [PATCH] J2K: convert javadoc comments to kdoc --- .../kotlin/j2k/AnnotationConverter.kt | 31 ++- .../org/jetbrains/kotlin/j2k/CodeBuilder.kt | 18 +- .../kotlin/j2k/DocCommentConverter.kt | 185 ++++++++++++++++++ .../docComments/deprecatedDocTag.java | 6 + .../docComments/deprecatedDocTag.kt | 5 + .../docComments/docCommentWithParamTag.java | 4 + .../docComments/docCommentWithParamTag.kt | 4 + .../docComments/htmlInDocComment.java | 16 ++ .../docComments/htmlInDocComment.kt | 16 ++ .../docComments/inlineTagsInDocComment.java | 5 + .../docComments/inlineTagsInDocComment.kt | 4 + .../fileOrElement/docComments/linkTag.java | 7 + .../fileOrElement/docComments/linkTag.kt | 7 + .../docComments/linkTagWithLabel.java | 7 + .../docComments/linkTagWithLabel.kt | 7 + .../docComments/onlyDeprecatedDocTag.java | 5 + .../docComments/onlyDeprecatedDocTag.kt | 2 + .../fileOrElement/docComments/seeTag.java | 7 + .../fileOrElement/docComments/seeTag.kt | 7 + ...otlinConverterForWebDemoTestGenerated.java | 58 ++++++ ...otlinConverterSingleFileTestGenerated.java | 58 ++++++ 21 files changed, 448 insertions(+), 11 deletions(-) create mode 100644 j2k/src/org/jetbrains/kotlin/j2k/DocCommentConverter.kt create mode 100644 j2k/testData/fileOrElement/docComments/deprecatedDocTag.java create mode 100644 j2k/testData/fileOrElement/docComments/deprecatedDocTag.kt create mode 100644 j2k/testData/fileOrElement/docComments/docCommentWithParamTag.java create mode 100644 j2k/testData/fileOrElement/docComments/docCommentWithParamTag.kt create mode 100644 j2k/testData/fileOrElement/docComments/htmlInDocComment.java create mode 100644 j2k/testData/fileOrElement/docComments/htmlInDocComment.kt create mode 100644 j2k/testData/fileOrElement/docComments/inlineTagsInDocComment.java create mode 100644 j2k/testData/fileOrElement/docComments/inlineTagsInDocComment.kt create mode 100644 j2k/testData/fileOrElement/docComments/linkTag.java create mode 100644 j2k/testData/fileOrElement/docComments/linkTag.kt create mode 100644 j2k/testData/fileOrElement/docComments/linkTagWithLabel.java create mode 100644 j2k/testData/fileOrElement/docComments/linkTagWithLabel.kt create mode 100644 j2k/testData/fileOrElement/docComments/onlyDeprecatedDocTag.java create mode 100644 j2k/testData/fileOrElement/docComments/onlyDeprecatedDocTag.kt create mode 100644 j2k/testData/fileOrElement/docComments/seeTag.java create mode 100644 j2k/testData/fileOrElement/docComments/seeTag.kt diff --git a/j2k/src/org/jetbrains/kotlin/j2k/AnnotationConverter.kt b/j2k/src/org/jetbrains/kotlin/j2k/AnnotationConverter.kt index 34ed6c08557..d70cf4adb87 100644 --- a/j2k/src/org/jetbrains/kotlin/j2k/AnnotationConverter.kt +++ b/j2k/src/org/jetbrains/kotlin/j2k/AnnotationConverter.kt @@ -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 = (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 { + 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 { diff --git a/j2k/src/org/jetbrains/kotlin/j2k/CodeBuilder.kt b/j2k/src/org/jetbrains/kotlin/j2k/CodeBuilder.kt index 47ab2bcdd7b..5be0dd53c2d 100644 --- a/j2k/src/org/jetbrains/kotlin/j2k/CodeBuilder.kt +++ b/j2k/src/org/jetbrains/kotlin/j2k/CodeBuilder.kt @@ -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 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()) { diff --git a/j2k/src/org/jetbrains/kotlin/j2k/DocCommentConverter.kt b/j2k/src/org/jetbrains/kotlin/j2k/DocCommentConverter.kt new file mode 100644 index 00000000000..348cef23cbc --- /dev/null +++ b/j2k/src/org/jetbrains/kotlin/j2k/DocCommentConverter.kt @@ -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): 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") "$escaped" 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) + "$linkText" + } + 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 = 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("") diff --git a/j2k/testData/fileOrElement/docComments/deprecatedDocTag.java b/j2k/testData/fileOrElement/docComments/deprecatedDocTag.java new file mode 100644 index 00000000000..1fbb5613a87 --- /dev/null +++ b/j2k/testData/fileOrElement/docComments/deprecatedDocTag.java @@ -0,0 +1,6 @@ +/** + * This is a deprecated class. + * @deprecated do not use + */ +class C { +} diff --git a/j2k/testData/fileOrElement/docComments/deprecatedDocTag.kt b/j2k/testData/fileOrElement/docComments/deprecatedDocTag.kt new file mode 100644 index 00000000000..102b6dbb6cc --- /dev/null +++ b/j2k/testData/fileOrElement/docComments/deprecatedDocTag.kt @@ -0,0 +1,5 @@ +/** + * This is a deprecated class. + */ +deprecated("do not use") +class C diff --git a/j2k/testData/fileOrElement/docComments/docCommentWithParamTag.java b/j2k/testData/fileOrElement/docComments/docCommentWithParamTag.java new file mode 100644 index 00000000000..8070c4f7df9 --- /dev/null +++ b/j2k/testData/fileOrElement/docComments/docCommentWithParamTag.java @@ -0,0 +1,4 @@ +/** + * @param T This is the parameter of class {@code C} + */ +class C {} diff --git a/j2k/testData/fileOrElement/docComments/docCommentWithParamTag.kt b/j2k/testData/fileOrElement/docComments/docCommentWithParamTag.kt new file mode 100644 index 00000000000..64d12bfd689 --- /dev/null +++ b/j2k/testData/fileOrElement/docComments/docCommentWithParamTag.kt @@ -0,0 +1,4 @@ +/** + * @param T This is the **parameter** of class `C` + */ +class C diff --git a/j2k/testData/fileOrElement/docComments/htmlInDocComment.java b/j2k/testData/fileOrElement/docComments/htmlInDocComment.java new file mode 100644 index 00000000000..8b5111dee35 --- /dev/null +++ b/j2k/testData/fileOrElement/docComments/htmlInDocComment.java @@ -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): & < > " + * Made by JetBrains + *
    + *
  • Kotlin
  • + *
  • Java
  • + *
+ *
    + *
  1. First
  2. + *
  3. Second
  4. + *
+ */ +public class C { +} \ No newline at end of file diff --git a/j2k/testData/fileOrElement/docComments/htmlInDocComment.kt b/j2k/testData/fileOrElement/docComments/htmlInDocComment.kt new file mode 100644 index 00000000000..66b4a23ab53 --- /dev/null +++ b/j2k/testData/fileOrElement/docComments/htmlInDocComment.kt @@ -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): & < > " + * Made by [JetBrains](http://www.jetbrains.com) + * + * * Kotlin + * * Java + * + * + * 1. First + * 1. Second + * + */ +public class C \ No newline at end of file diff --git a/j2k/testData/fileOrElement/docComments/inlineTagsInDocComment.java b/j2k/testData/fileOrElement/docComments/inlineTagsInDocComment.java new file mode 100644 index 00000000000..1b20d2753db --- /dev/null +++ b/j2k/testData/fileOrElement/docComments/inlineTagsInDocComment.java @@ -0,0 +1,5 @@ +/** + * {@code A