001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.javadoc; 021 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.List; 025 026import com.puppycrawl.tools.checkstyle.StatelessCheck; 027import com.puppycrawl.tools.checkstyle.api.DetailNode; 028import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes; 029import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 030import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 031 032/** 033 * <div> 034 * Checks that one blank line before the block tag if it is present in Javadoc. 035 * </div> 036 * 037 * @since 8.36 038 */ 039@StatelessCheck 040public class RequireEmptyLineBeforeBlockTagGroupCheck extends AbstractJavadocCheck { 041 042 /** 043 * The key in "messages.properties" for the message that describes a tag in javadoc 044 * requiring an empty line before it. 045 */ 046 public static final String MSG_JAVADOC_TAG_LINE_BEFORE = "javadoc.tag.line.before"; 047 048 /** 049 * Case when space separates the tag and the asterisk like in the below example. 050 * <pre> 051 * /** 052 * * @param noSpace there is no space here 053 * </pre> 054 */ 055 private static final List<Integer> ONLY_TAG_VARIATION_1 = Arrays.asList( 056 JavadocCommentsTokenTypes.TEXT, 057 JavadocCommentsTokenTypes.LEADING_ASTERISK, 058 JavadocCommentsTokenTypes.NEWLINE); 059 060 /** 061 * Case when no space separates the tag and the asterisk like in the below example. 062 * <pre> 063 * /** 064 * *@param noSpace there is no space here 065 * </pre> 066 */ 067 private static final List<Integer> ONLY_TAG_VARIATION_2 = Arrays.asList( 068 JavadocCommentsTokenTypes.LEADING_ASTERISK, 069 JavadocCommentsTokenTypes.NEWLINE); 070 071 /** 072 * Returns only javadoc tags so visitJavadocToken only receives javadoc tags. 073 * 074 * @return only javadoc tags. 075 */ 076 @Override 077 public int[] getDefaultJavadocTokens() { 078 return new int[] { 079 JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG, 080 }; 081 } 082 083 @Override 084 public int[] getRequiredJavadocTokens() { 085 return getAcceptableJavadocTokens(); 086 } 087 088 /** 089 * Logs when there is no empty line before the tag. 090 * 091 * @param tagNode the at tag node to check for an empty space before it. 092 */ 093 @Override 094 public void visitJavadocToken(DetailNode tagNode) { 095 // No need to filter token because overridden getDefaultJavadocTokens ensures that we only 096 // receive JAVADOC_BLOCK_TAG DetailNode. 097 if (!isAnotherTagBefore(tagNode) 098 && !isOnlyTagInWholeJavadoc(tagNode) 099 && hasInsufficientConsecutiveNewlines(tagNode)) { 100 final String tagName = JavadocUtil.getTagName(tagNode); 101 log(tagNode.getLineNumber(), 102 MSG_JAVADOC_TAG_LINE_BEFORE, 103 "@" + tagName); 104 } 105 } 106 107 /** 108 * Returns true when there is a javadoc tag before the provided tagNode. 109 * 110 * @param tagNode the javadoc tag node, to look for more tags before it. 111 * @return true when there is a javadoc tag before the provided tagNode. 112 */ 113 private static boolean isAnotherTagBefore(DetailNode tagNode) { 114 boolean found = false; 115 DetailNode currentNode = tagNode.getPreviousSibling(); 116 while (currentNode != null) { 117 if (currentNode.getType() == JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG) { 118 found = true; 119 break; 120 } 121 currentNode = currentNode.getPreviousSibling(); 122 } 123 return found; 124 } 125 126 /** 127 * Returns true when there are is only whitespace and asterisks before the provided tagNode. 128 * When javadoc has only a javadoc tag like {@literal @} in it, the JAVADOC_TAG in a JAVADOC 129 * detail node will always have 2 or 3 siblings before it. The parse tree looks like: 130 * <pre> 131 * JAVADOC_CONTENT[3x0] 132 * |--NEWLINE[3x0] : [\n] 133 * |--LEADING_ASTERISK[4x0] : [ *] 134 * |--TEXT[4x2] : [ ] 135 * |--JAVADOC_BLOCK_TAG[4x3] : [@param T The bar.\n ] 136 * </pre> 137 * Or it can also look like: 138 * <pre> 139 * JAVADOC_CONTENT[3x0] 140 * |--NEWLINE[3x0] : [\n] 141 * |--LEADING_ASTERISK[4x0] : [ *] 142 * |--JAVADOC_BLOCK_TAG[4x3] : [@param T The bar.\n ] 143 * </pre> 144 * We do not include the variation 145 * <pre> 146 * /**@param noSpace there is no space here 147 * </pre> 148 * which results in the tree 149 * <pre> 150 * JAVADOC_CONTENT[3x0] 151 * |--JAVADOC_BLOCK_TAG[4x3] : [@param noSpace there is no space here\n ] 152 * </pre> 153 * because this one is invalid. We must recommend placing a blank line to separate @param 154 * from the first javadoc asterisks. 155 * 156 * @param tagNode the at tag node to check if there is nothing before it 157 * @return true if there is no text before the tagNode 158 */ 159 @SuppressWarnings("InvalidInlineTag") 160 private static boolean isOnlyTagInWholeJavadoc(DetailNode tagNode) { 161 final List<Integer> previousNodeTypes = new ArrayList<>(); 162 DetailNode currentNode = tagNode.getPreviousSibling(); 163 164 while (currentNode != null) { 165 previousNodeTypes.add(currentNode.getType()); 166 currentNode = currentNode.getPreviousSibling(); 167 } 168 return ONLY_TAG_VARIATION_1.equals(previousNodeTypes) 169 || ONLY_TAG_VARIATION_2.equals(previousNodeTypes); 170 } 171 172 /** 173 * Returns true when there are not enough empty lines before the provided tagNode. 174 * 175 * <p>Iterates through the previous siblings of the tagNode looking for empty lines until 176 * there are no more siblings or it hits something other than asterisk, whitespace or newline. 177 * If it finds at least one empty line, return true. Return false otherwise.</p> 178 * 179 * @param tagNode the tagNode to check if there are sufficient empty lines before it. 180 * @return true if there are not enough empty lines before the tagNode. 181 */ 182 private static boolean hasInsufficientConsecutiveNewlines(DetailNode tagNode) { 183 int count = 0; 184 DetailNode currentNode = tagNode.getPreviousSibling(); 185 while (currentNode != null 186 && (CommonUtil.isBlank(currentNode.getText()) 187 || currentNode.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK)) { 188 if (currentNode.getType() == JavadocCommentsTokenTypes.NEWLINE) { 189 count++; 190 } 191 currentNode = currentNode.getPreviousSibling(); 192 } 193 194 return count <= 1; 195 } 196}