Bug 54627 - [formatter] Blank lines between Javadoc tags

Change-Id: Ieaedb78d6e63e99e564ef116062653251a20b54d
Signed-off-by: Mateusz Matela <mateusz.matela@gmail.com>
diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/formatter/FormatterRegressionTests.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/formatter/FormatterRegressionTests.java
index 567280f..3795c16 100644
--- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/formatter/FormatterRegressionTests.java
+++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/formatter/FormatterRegressionTests.java
@@ -15385,4 +15385,104 @@
 	this.formatterPrefs.continuation_indentation = 2;
 	formatSourceInWorkspace("test549436", "in.java", "K_out.java");
 }
+/**
+ * https://bugs.eclipse.org/54627 - [formatter] Blank lines between Javadoc tags
+ */
+public void testBug54627a() throws JavaModelException {
+	this.formatterPrefs.comment_insert_empty_line_between_different_tags = true;
+	String input =
+		"public class Test {\n" + 
+		"	/**\n" + 
+		"	 * Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.\n" + 
+		"	 * @param a Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" + 
+		"	 * @param b Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n" + 
+		"	 * @param c Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n" + 
+		"	 * @throws IOException Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.\n" + 
+		"	 * @throws SQLException Totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.\n" + 
+		"	 * @return Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.\n" + 
+		"	 */\n" + 
+		"	public String f(int a, int b, int c) throws IOException, SQLException {\n" + 
+		"		return \"\";\n" + 
+		"	}\n" + 
+		"}";
+	formatSource(input,
+		"public class Test {\n" + 
+		"	/**\n" + 
+		"	 * Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod\n" + 
+		"	 * tempor incididunt ut labore et dolore magna aliqua. Neque porro quisquam est,\n" + 
+		"	 * qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia\n" + 
+		"	 * non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam\n" + 
+		"	 * quaerat voluptatem.\n" + 
+		"	 * \n" + 
+		"	 * @param a Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris\n" + 
+		"	 *          nisi ut aliquip ex ea commodo consequat.\n" + 
+		"	 * @param b Duis aute irure dolor in reprehenderit in voluptate velit esse\n" + 
+		"	 *          cillum dolore eu fugiat nulla pariatur.\n" + 
+		"	 * @param c Excepteur sint occaecat cupidatat non proident, sunt in culpa qui\n" + 
+		"	 *          officia deserunt mollit anim id est laborum.\n" + 
+		"	 * \n" + 
+		"	 * @throws IOException  Sed ut perspiciatis unde omnis iste natus error sit\n" + 
+		"	 *                      voluptatem accusantium doloremque laudantium.\n" + 
+		"	 * @throws SQLException Totam rem aperiam, eaque ipsa quae ab illo inventore\n" + 
+		"	 *                      veritatis et quasi architecto beatae vitae dicta sunt\n" + 
+		"	 *                      explicabo.\n" + 
+		"	 * \n" + 
+		"	 * @return Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut\n" + 
+		"	 *         fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem\n" + 
+		"	 *         sequi nesciunt.\n" + 
+		"	 */\n" + 
+		"	public String f(int a, int b, int c) throws IOException, SQLException {\n" + 
+		"		return \"\";\n" + 
+		"	}\n" + 
+		"}");
+}
+/**
+ * https://bugs.eclipse.org/54627 - [formatter] Blank lines between Javadoc tags
+ */
+public void testBug54627b() throws JavaModelException {
+	this.formatterPrefs.comment_insert_empty_line_between_different_tags = true;
+	String input =
+		"public class Test {\n" + 
+		"	\n" + 
+		"	/**\n" + 
+		"	 * Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.\n" + 
+		"	 * @param a Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" + 
+		"	 * @param b Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n" + 
+		"	 * @return Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.\n" + 
+		"	 * @@org.example.transaction.interceptor.RuleBasedTransactionAttribute()\n" + 
+		"	 * @@org.example.transaction.interceptor.RollbackRuleAttribute(Exception.class)\n" + 
+		"	 * @@org.example.transaction.interceptor.NoRollbackRuleAttribute(\"ServletException\")\n" + 
+		"	 */\n" + 
+		"	public String f(int a, int b, int c) {\n" + 
+		"		return \"\";\n" + 
+		"	}\n" + 
+		"}";
+	formatSource(input,
+		"public class Test {\n" + 
+		"\n" + 
+		"	/**\n" + 
+		"	 * Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod\n" + 
+		"	 * tempor incididunt ut labore et dolore magna aliqua. Neque porro quisquam est,\n" + 
+		"	 * qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia\n" + 
+		"	 * non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam\n" + 
+		"	 * quaerat voluptatem.\n" + 
+		"	 * \n" + 
+		"	 * @param a Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris\n" + 
+		"	 *          nisi ut aliquip ex ea commodo consequat.\n" + 
+		"	 * @param b Duis aute irure dolor in reprehenderit in voluptate velit esse\n" + 
+		"	 *          cillum dolore eu fugiat nulla pariatur.\n" + 
+		"	 * \n" + 
+		"	 * @return Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut\n" + 
+		"	 *         fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem\n" + 
+		"	 *         sequi nesciunt.\n" + 
+		"	 * \n" + 
+		"	 * @@org.example.transaction.interceptor.RuleBasedTransactionAttribute()\n" + 
+		"	 * @@org.example.transaction.interceptor.RollbackRuleAttribute(Exception.class)\n" + 
+		"	 * @@org.example.transaction.interceptor.NoRollbackRuleAttribute(\"ServletException\")\n" + 
+		"	 */\n" + 
+		"	public String f(int a, int b, int c) {\n" + 
+		"		return \"\";\n" + 
+		"	}\n" + 
+		"}");
+}
 }
diff --git a/org.eclipse.jdt.core/formatter/org/eclipse/jdt/core/formatter/DefaultCodeFormatterConstants.java b/org.eclipse.jdt.core/formatter/org/eclipse/jdt/core/formatter/DefaultCodeFormatterConstants.java
index 31f10d7..ab143da 100644
--- a/org.eclipse.jdt.core/formatter/org/eclipse/jdt/core/formatter/DefaultCodeFormatterConstants.java
+++ b/org.eclipse.jdt.core/formatter/org/eclipse/jdt/core/formatter/DefaultCodeFormatterConstants.java
@@ -1396,6 +1396,18 @@
 	 * @since 3.1
 	 */
 	public final static String FORMATTER_COMMENT_INSERT_EMPTY_LINE_BEFORE_ROOT_TAGS = "org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags"; //$NON-NLS-1$
+	/**
+	 * <pre>
+	 * FORMATTER / Option to insert an empty line between Javadoc tags of different type
+	 *     - option id:         "org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags"
+	 *     - possible values:   { INSERT, DO_NOT_INSERT }
+	 *     - default:           INSERT
+	 * </pre>
+	 * @see JavaCore#INSERT
+	 * @see JavaCore#DO_NOT_INSERT
+	 * @since 3.20
+	 */
+	public final static String FORMATTER_COMMENT_INSERT_EMPTY_LINE_BETWEEN_DIFFERENT_TAGS = "org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags"; //$NON-NLS-1$
 
 	/**
 	 * <pre>
diff --git a/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/CommentsPreparator.java b/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/CommentsPreparator.java
index 85d047e..c9be5d9 100644
--- a/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/CommentsPreparator.java
+++ b/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/CommentsPreparator.java
@@ -33,7 +33,7 @@
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-
+import java.util.stream.Collectors;
 import org.eclipse.jdt.core.dom.ASTNode;
 import org.eclipse.jdt.core.dom.ASTVisitor;
 import org.eclipse.jdt.core.dom.BlockComment;
@@ -109,7 +109,7 @@
 	private int noFormatTagOpenStart = -1;
 	private int formatCodeTagOpenEnd = -1;
 	private int lastFormatCodeClosingTagIndex = -1;
-	private Token firstTagToken;
+	private ArrayList<Integer> commonAttributeAnnotations = new ArrayList<Integer>();
 	private DefaultCodeFormatter commentCodeFormatter;
 
 	public CommentsPreparator(TokenManager tm, DefaultCodeFormatterOptions options, String sourceLevel) {
@@ -536,7 +536,7 @@
 		this.noFormatTagOpenStart = -1;
 		this.formatCodeTagOpenEnd = -1;
 		this.lastFormatCodeClosingTagIndex = -1;
-		this.firstTagToken = null;
+		this.commonAttributeAnnotations.clear();
 		this.ctm = null;
 
 		int commentIndex = this.tm.firstIndexIn(node, TokenNameCOMMENT_JAVADOC);
@@ -564,6 +564,7 @@
 		this.ctm = new TokenManager(commentToken.getInternalStructure(), this.tm);
 
 		handleJavadocTagAlignment(node);
+		handleJavadocBlankLines(node);
 
 		return true;
 	}
@@ -572,10 +573,6 @@
 	public void endVisit(Javadoc node) {
 		if (this.ctm == null)
 			return;
-		if (this.options.comment_insert_empty_line_before_root_tags && this.firstTagToken != null
-				&& this.ctm.indexOf(this.firstTagToken) > 1) {
-			this.firstTagToken.putLineBreaksBefore(2);
-		}
 		addSubstituteWraps();
 	}
 
@@ -599,10 +596,6 @@
 			Token startTokeen = this.ctm.get(startIndex);
 			if (startIndex > 1)
 				startTokeen.breakBefore();
-			int firstTagIndex;
-			if (this.firstTagToken == null || (firstTagIndex = this.ctm.indexOf(this.firstTagToken)) < 0
-					|| startIndex < firstTagIndex)
-				this.firstTagToken = startTokeen;
 
 			handleHtml(node);
 		}
@@ -697,6 +690,35 @@
 			}
 		}
 	}
+	
+	private void handleJavadocBlankLines(Javadoc node) {
+		List<TagElement> tagElements = node.tags();
+		List<Integer> tagIndexes = tagElements.stream()
+				.filter(t -> !t.isNested() && t.getTagName() != null && t.getTagName().length() > 1)
+				.map(t -> tokenStartingAt(t.getStartPosition()))
+				.collect(Collectors.toList());
+		tagIndexes.addAll(this.commonAttributeAnnotations);
+		Collections.sort(tagIndexes);
+		
+		String previousName = null;
+		if (!tagIndexes.isEmpty()) {
+			int firstIndex = tagIndexes.get(0);
+			previousName = this.ctm.toString(firstIndex);
+			if (this.options.comment_insert_empty_line_before_root_tags && firstIndex > 1)
+				this.ctm.get(firstIndex).putLineBreaksBefore(2);
+		}
+		if (this.options.comment_insert_empty_line_between_different_tags) {
+			for (int i = 1; i < tagIndexes.size(); i++) {
+				Token tagToken = this.ctm.get(tagIndexes.get(i));
+				String thisName = this.tm.toString(tagToken);
+				boolean sameType = previousName.equals(thisName)
+						|| (isCommonsAttributeAnnotation(previousName) && isCommonsAttributeAnnotation(thisName));
+				if (!sameType)
+					tagToken.putLineBreaksBefore(2);
+				previousName = thisName;
+			}
+		}
+	}
 
 	private void alignJavadocTag(List<Token> tagTokens, int paramNameAlign, int descriptionAlign) {
 		Token paramName = tagTokens.get(1);
@@ -1066,11 +1088,12 @@
 						if (this.tm.charAt(tokenStart) == '@') {
 							outputToken.setWrapPolicy(WrapPolicy.DISABLE_WRAP);
 							if (commentToken.tokenType == TokenNameCOMMENT_BLOCK && lineBreaks == 1
-									&& structure.size() > 1)
+									&& structure.size() > 1) {
 								outputToken.putLineBreaksBefore(cleanBlankLines ? 1 : 2);
-							if (this.tm.charAt(tokenStart + 1) == '@' && lineBreaks > 0 && this.firstTagToken == null) {
-								// Commons Attributes annotation, see bug 237051
-								this.firstTagToken = outputToken;
+							}
+							if (lineBreaks > 0 && isCommonsAttributeAnnotation(this.tm.toString(outputToken))) {
+								outputToken.breakBefore();
+								this.commonAttributeAnnotations.add(structure.size());
 							}
 						}
 						structure.add(outputToken);
@@ -1103,6 +1126,10 @@
 		return true;
 	}
 
+	private boolean isCommonsAttributeAnnotation(String tokenContent) {
+		return tokenContent.startsWith("@@"); //$NON-NLS-1$
+	}
+
 	private void noSubstituteWrapping(int from, int to) {
 		int commentStart = this.ctm.get(0).originalStart;
 		assert commentStart <= from && from <= to && to <= this.ctm.get(this.ctm.size() - 1).originalEnd;
diff --git a/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/DefaultCodeFormatterOptions.java b/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/DefaultCodeFormatterOptions.java
index 1828844..753a963 100644
--- a/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/DefaultCodeFormatterOptions.java
+++ b/org.eclipse.jdt.core/formatter/org/eclipse/jdt/internal/formatter/DefaultCodeFormatterOptions.java
@@ -229,6 +229,7 @@
 	public boolean comment_align_tags_names_descriptions;
 	public boolean comment_align_tags_descriptions_grouped;
 	public boolean comment_insert_empty_line_before_root_tags;
+	public boolean comment_insert_empty_line_between_different_tags;
 	public boolean comment_insert_new_line_for_parameter;
 	public boolean comment_preserve_white_space_between_code_and_line_comments;
 	public int comment_line_length;
@@ -600,6 +601,7 @@
 		options.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_ALIGN_TAGS_NAMES_DESCRIPTIONS, this.comment_align_tags_names_descriptions ? DefaultCodeFormatterConstants.TRUE : DefaultCodeFormatterConstants.FALSE);
 		options.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_ALIGN_TAGS_DESCREIPTIONS_GROUPED, this.comment_align_tags_descriptions_grouped ? DefaultCodeFormatterConstants.TRUE : DefaultCodeFormatterConstants.FALSE);
 		options.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_INSERT_EMPTY_LINE_BEFORE_ROOT_TAGS, this.comment_insert_empty_line_before_root_tags ? JavaCore.INSERT : JavaCore.DO_NOT_INSERT);
+		options.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_INSERT_EMPTY_LINE_BETWEEN_DIFFERENT_TAGS, this.comment_insert_empty_line_between_different_tags ? JavaCore.INSERT : JavaCore.DO_NOT_INSERT);
 		options.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_INSERT_NEW_LINE_FOR_PARAMETER, this.comment_insert_new_line_for_parameter ? JavaCore.INSERT : JavaCore.DO_NOT_INSERT);
 		options.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_PRESERVE_WHITE_SPACE_BETWEEN_CODE_AND_LINE_COMMENT, this.comment_preserve_white_space_between_code_and_line_comments ? DefaultCodeFormatterConstants.TRUE : DefaultCodeFormatterConstants.FALSE);
 		options.put(DefaultCodeFormatterConstants.FORMATTER_COMMENT_LINE_LENGTH, Integer.toString(this.comment_line_length));
@@ -1479,6 +1481,8 @@
 		if (commentInsertEmptyLineBeforeRootTagsOption != null) {
 			this.comment_insert_empty_line_before_root_tags = JavaCore.INSERT.equals(commentInsertEmptyLineBeforeRootTagsOption);
 		}
+		setBoolean(settings, DefaultCodeFormatterConstants.FORMATTER_COMMENT_INSERT_EMPTY_LINE_BETWEEN_DIFFERENT_TAGS, JavaCore.INSERT,
+				v -> this.comment_insert_empty_line_between_different_tags = v);
 		final Object commentInsertNewLineForParameterOption = settings.get(DefaultCodeFormatterConstants.FORMATTER_COMMENT_INSERT_NEW_LINE_FOR_PARAMETER);
 		if (commentInsertNewLineForParameterOption != null) {
 			this.comment_insert_new_line_for_parameter = JavaCore.INSERT.equals(commentInsertNewLineForParameterOption);
@@ -2869,6 +2873,7 @@
 		this.comment_align_tags_names_descriptions = false;
 		this.comment_align_tags_descriptions_grouped = false;
 		this.comment_insert_empty_line_before_root_tags = true;
+		this.comment_insert_empty_line_between_different_tags = false;
 		this.comment_insert_new_line_for_parameter = true;
 		this.comment_new_lines_at_block_boundaries = true;
 		this.comment_new_lines_at_javadoc_boundaries = true;
@@ -3234,6 +3239,7 @@
 		this.comment_align_tags_names_descriptions = false;
 		this.comment_align_tags_descriptions_grouped = true;
 		this.comment_insert_empty_line_before_root_tags = true;
+		this.comment_insert_empty_line_between_different_tags = false;
 		this.comment_insert_new_line_for_parameter = false;
 		this.comment_new_lines_at_block_boundaries = true;
 		this.comment_new_lines_at_javadoc_boundaries = true;