diff --git a/java/java-impl/src/com/intellij/codeInspection/javaDoc/JavadocBlankLinesInspection.java b/java/java-impl/src/com/intellij/codeInspection/javaDoc/JavadocBlankLinesInspection.java index 12c8e2733b2e..c701c4e146d2 100644 --- a/java/java-impl/src/com/intellij/codeInspection/javaDoc/JavadocBlankLinesInspection.java +++ b/java/java-impl/src/com/intellij/codeInspection/javaDoc/JavadocBlankLinesInspection.java @@ -9,14 +9,18 @@ import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; import com.intellij.psi.javadoc.PsiDocTag; import com.intellij.psi.javadoc.PsiDocToken; import com.intellij.psi.javadoc.PsiInlineDocTag; -import com.intellij.util.ObjectUtils; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.xml.util.HtmlUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import static com.intellij.openapi.util.text.CharFilter.NOT_WHITESPACE_FILTER; + public class JavadocBlankLinesInspection extends LocalInspectionTool { @Override public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { @@ -24,30 +28,79 @@ public class JavadocBlankLinesInspection extends LocalInspectionTool { @Override public void visitDocToken(PsiDocToken token) { super.visitDocToken(token); - PsiElement nextSibling = token.getNextSibling(); + PsiElement nextWhitespace = token.getNextSibling(); + PsiElement prevWhitespace = token.getPrevSibling(); if (token.getTokenType() == JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS && - token.getPrevSibling() instanceof PsiWhiteSpace && - nextSibling instanceof PsiWhiteSpace && !isBeforeParagraphOrBlockTag(nextSibling)) { + prevWhitespace instanceof PsiWhiteSpace && + nextWhitespace instanceof PsiWhiteSpace && + !isAfterParagraphOrBlockTag(prevWhitespace) && + !isBeforeParagraphOrBlockTag(nextWhitespace) && + !isAfterPreTag(token)) { holder.registerProblem(token, JavaBundle.message("inspection.javadoc.blank.lines.message"), new InsertParagraphTagFix(token)); } } }; } - private static boolean isBeforeParagraphOrBlockTag(PsiElement element) { - PsiDocToken maybeLeadingAsterisks = ObjectUtils.tryCast(element.getNextSibling(), PsiDocToken.class); - if (maybeLeadingAsterisks == null || maybeLeadingAsterisks.getTokenType() != JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS) { - return false; + private static boolean isAfterPreTag(PsiDocToken token) { + PsiElement parent = token.getParent(); + if (parent instanceof PsiInlineDocTag) { + return isAfterPreTagInner(PsiTreeUtil.getPrevSiblingOfType(parent, PsiDocToken.class)); } - PsiElement nextSibling = maybeLeadingAsterisks.getNextSibling(); - if (nextSibling == null) return false; - return nextSibling.getText().stripLeading().startsWith("

") || - isBlockTag(nextSibling) || - isBlockTag(nextSibling.getNextSibling()); + return isAfterPreTagInner(token); } - private static boolean isBlockTag(PsiElement element) { - return element instanceof PsiDocTag && !(element instanceof PsiInlineDocTag); + private static boolean isAfterPreTagInner(PsiDocToken token) { + boolean result = false; + while (token != null) { + String text = token.getText(); + int closingPreTagIndex = StringUtil.toLowerCase(text).lastIndexOf(""); + int openingPreTagIndex = StringUtil.toLowerCase(text).lastIndexOf("

");
+      result = openingPreTagIndex != -1 && (closingPreTagIndex == -1 || closingPreTagIndex < openingPreTagIndex);
+      if (closingPreTagIndex != -1 || openingPreTagIndex != -1) break;
+      token = PsiTreeUtil.getPrevSiblingOfType(token, PsiDocToken.class);
+    }
+    return result;
+  }
+
+  private static boolean isAfterParagraphOrBlockTag(PsiElement element) {
+    PsiElement prevSibling = element.getPrevSibling();
+    if (!(prevSibling instanceof PsiDocToken)) return true;
+    if (((PsiDocToken)prevSibling).getTokenType() != JavaDocTokenType.DOC_COMMENT_DATA) return true;
+    String text = prevSibling.getText();
+    return endsWithHtmlBlockTag(text) || isNullOrBlockTag(prevSibling);
+  }
+
+  private static boolean isBeforeParagraphOrBlockTag(PsiElement element) {
+    PsiElement nextSibling = element.getNextSibling();
+    if (!(nextSibling instanceof PsiDocToken)) return true;
+    if (((PsiDocToken)nextSibling).getTokenType() != JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS) return true;
+    nextSibling = nextSibling.getNextSibling();
+    if (nextSibling == null) return true;
+    String text = nextSibling.getText();
+    return isNullOrBlockTag(nextSibling) ||
+           startsWithHtmlBlockTag(text) ||
+           isNullOrBlockTag(nextSibling.getNextSibling());
+  }
+
+  private static boolean startsWithHtmlBlockTag(String text) {
+    text = text.stripLeading();
+    if (text.isEmpty() || text.charAt(0) != '<') return false;
+    String maybeBlockTag = text.substring(1, text.indexOf('>'));
+    String trimmed = StringUtil.trim(maybeBlockTag.strip(), ch -> NOT_WHITESPACE_FILTER.accept(ch) && ch != '/');
+    return HtmlUtil.isHtmlBlockTag(trimmed) || "br".equalsIgnoreCase(trimmed);
+  }
+
+  private static boolean endsWithHtmlBlockTag(String text) {
+    text = text.stripTrailing();
+    if (text.isEmpty() || text.charAt(text.length() - 1) != '>') return false;
+    String maybeBlockTag = text.substring(text.lastIndexOf('<') + 1, text.length() - 1);
+    String trimmed = StringUtil.trim(maybeBlockTag.strip(), ch -> NOT_WHITESPACE_FILTER.accept(ch) && ch != '/');
+    return HtmlUtil.isHtmlBlockTag(trimmed) || "br".equalsIgnoreCase(trimmed);
+  }
+
+  private static boolean isNullOrBlockTag(PsiElement element) {
+    return element == null || (element instanceof PsiDocTag && !(element instanceof PsiInlineDocTag));
   }
 
   private static class InsertParagraphTagFix extends LocalQuickFixAndIntentionActionOnPsiElement {
diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeBetweenJavadocBlockTags.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeBetweenJavadocBlockTags.java
new file mode 100644
index 000000000000..05d6e07bf774
--- /dev/null
+++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeBetweenJavadocBlockTags.java
@@ -0,0 +1,13 @@
+// "Insert 

" "false" +class Test { + /** + * Answer to the ultimate question of life, the universe, and everything + * + * @author Douglas Adams + * + * @return The number 42 + */ + int answer() { + return 42; + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeBr.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeBr.java new file mode 100644 index 000000000000..1492c0cb27f9 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeBr.java @@ -0,0 +1,15 @@ +// "Insert

" "false" +class Test { + /** + *

+   *     Blah
+   * 
+ * + * Blah + * Blah + *
+ */ + int answer() { + return 42; + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeCommentEnd.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeCommentEnd.java new file mode 100644 index 000000000000..1e0f5ec733b1 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeCommentEnd.java @@ -0,0 +1,12 @@ +// "Insert

" "false" +class Test { + /** + * Answer to the ultimate question of life, the universe, and everything + * + * @return The number 42 + * + */ + int answer() { + return 42; + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeCommentStart.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeCommentStart.java new file mode 100644 index 000000000000..db74ae04487a --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeCommentStart.java @@ -0,0 +1,12 @@ +// "Insert

" "false" +class Test { + /** + * + * Answer to the ultimate question of life, the universe, and everything + * + * @return The number 42 + */ + int answer() { + return 42; + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeHtmlBlockTag1.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeHtmlBlockTag1.java new file mode 100644 index 000000000000..047bd2744dc5 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeHtmlBlockTag1.java @@ -0,0 +1,16 @@ +// "Insert

" "false" +class Test { + /** + * Answer to the ultimate question of life, the universe, and everything + *

{@code
+   * 6 * 7 = 42
+   * }
+ * + * That's it. That's all there is. + * + * @return The number 42 + */ + int answer() { + return 42; + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeParagraphTag.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeHtmlBlockTag2.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeParagraphTag.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeHtmlBlockTag2.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeJavadocBlockTagsWithoutSpace.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeJavadocBlockTagsWithoutSpace.java new file mode 100644 index 000000000000..d3423306d298 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforeJavadocBlockTagsWithoutSpace.java @@ -0,0 +1,11 @@ +// "Insert

" "false" +class Test { + /** + * Answer to the ultimate question of life, the universe, and everything + * + *@return The number 42 + */ + int answer() { + return 42; + } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforePreformattedText.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforePreformattedText.java new file mode 100644 index 000000000000..81122160cf73 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforePreformattedText.java @@ -0,0 +1,12 @@ +// "Insert

" "false" +class Test { + /** + *

+   * Blah
+   *
+   * Blah
+   * Blah
+   * 
+ */ + void foo() { } +} diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforePreformattedTextInsideInlineTag.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforePreformattedTextInsideInlineTag.java new file mode 100644 index 000000000000..6ccfa6cbe75e --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/javadocBlankLines/beforePreformattedTextInsideInlineTag.java @@ -0,0 +1,13 @@ +// "Insert

" "false" +class Test { + /** + *

{@code
+   * 1
+   *
+   * 2 3 4
+   * 5 6
+   * }
+   * 
+ */ + void foo() { } +}