001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 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.List; 024 025import com.puppycrawl.tools.checkstyle.StatelessCheck; 026import com.puppycrawl.tools.checkstyle.api.DetailNode; 027import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes; 028import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 029import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 030 031/** 032 * <div> 033 * Checks the indentation of the continuation lines in block tags. That is whether the continued 034 * description of at clauses should be indented or not. If the text is not properly indented it 035 * throws a violation. A continuation line is when the description starts/spans past the line with 036 * the tag. Default indentation required is at least 4, but this can be changed with the help of 037 * properties below. 038 * </div> 039 * <ul> 040 * <li> 041 * Notes: 042 * This check does not validate the indentation of lines inside {@code pre} tags. 043 * </li> 044 * </ul> 045 * 046 * @since 6.0 047 */ 048@StatelessCheck 049public class JavadocTagContinuationIndentationCheck extends AbstractJavadocCheck { 050 051 /** 052 * A key is pointing to the warning message text in "messages.properties" 053 * file. 054 */ 055 public static final String MSG_KEY = "tag.continuation.indent"; 056 057 /** Default tag continuation indentation. */ 058 private static final int DEFAULT_INDENTATION = 4; 059 060 /** 061 * Specify how many spaces to use for new indentation level. 062 */ 063 private int offset = DEFAULT_INDENTATION; 064 065 /** 066 * Setter to specify how many spaces to use for new indentation level. 067 * 068 * @param offset custom value. 069 * @since 6.0 070 */ 071 public void setOffset(int offset) { 072 this.offset = offset; 073 } 074 075 @Override 076 public int[] getDefaultJavadocTokens() { 077 return new int[] {JavadocTokenTypes.HTML_TAG, JavadocTokenTypes.DESCRIPTION}; 078 079 } 080 081 @Override 082 public int[] getRequiredJavadocTokens() { 083 return getAcceptableJavadocTokens(); 084 } 085 086 @Override 087 public void visitJavadocToken(DetailNode ast) { 088 if (isBlockDescription(ast) && !isInlineDescription(ast)) { 089 final List<DetailNode> textNodes = getAllNewlineNodes(ast); 090 boolean isTextPreTagChildren = false; 091 for (DetailNode newlineNode : textNodes) { 092 final DetailNode textNode = JavadocUtil.getNextSibling(newlineNode); 093 if (newlineNode.getType() == JavadocTokenTypes.HTML_ELEMENT_START 094 || isHtmlTagChildOfPreTag(ast)) { 095 isTextPreTagChildren = true; 096 } 097 else if (newlineNode.getType() == JavadocTokenTypes.HTML_ELEMENT_END) { 098 isTextPreTagChildren = false; 099 } 100 else if (!isTextPreTagChildren && textNode.getType() != JavadocTokenTypes.NEWLINE 101 && isViolation(textNode)) { 102 log(textNode.getLineNumber(), MSG_KEY, offset); 103 } 104 } 105 } 106 } 107 108 /** 109 * Checks if the given HTML_TAG is contained inside {@code <pre>} tag. 110 * For cases when another HTML tag is placed next to or before {@code <pre>} tag. 111 * For example: 112 * <pre> 113 * {@code 114 * <pre><someOtherTag> 115 * some thing 116 * </someOtherTag></pre> 117 * } 118 * </pre> 119 * 120 * @param htmlTag HTML_TAG 121 * @return {@code true} if {@code pre} tag is parent of the given tag, else {@code false}. 122 */ 123 private static boolean isHtmlTagChildOfPreTag(DetailNode htmlTag) { 124 DetailNode node = htmlTag.getParent().getParent(); 125 node = JavadocUtil.getFirstChild(node); 126 return containsPreTag(node); 127 } 128 129 /** 130 * Checks if a text node meets the criteria for a violation. 131 * If the text is shorter than {@code offset} characters, then a violation is 132 * detected if the text is not blank or the next node is not a newline. 133 * If the text is longer than {@code offset} characters, then a violation is 134 * detected if any of the first {@code offset} characters are not blank. 135 * 136 * @param textNode the node to check. 137 * @return true if the node has a violation. 138 */ 139 private boolean isViolation(DetailNode textNode) { 140 boolean result = false; 141 final String text = textNode.getText(); 142 if (text.length() <= offset) { 143 if (CommonUtil.isBlank(text)) { 144 final DetailNode nextNode = JavadocUtil.getNextSibling(textNode); 145 // text is blank but line hasn't ended yet 146 if (nextNode != null && nextNode.getType() != JavadocTokenTypes.NEWLINE 147 && !containsPreTag(nextNode)) { 148 result = true; 149 } 150 } 151 else { 152 // text is not blank 153 result = true; 154 } 155 } 156 else if (!CommonUtil.isBlank(text.substring(1, offset + 1))) { 157 // first offset number of characters are not blank 158 result = true; 159 } 160 return result; 161 } 162 163 /** 164 * Finds and collects all NEWLINE nodes inside DESCRIPTION node. 165 * 166 * @param descriptionNode DESCRIPTION node. 167 * @return List with NEWLINE nodes. 168 */ 169 private static List<DetailNode> getAllNewlineNodes(DetailNode descriptionNode) { 170 final List<DetailNode> textNodes = new ArrayList<>(); 171 DetailNode node = JavadocUtil.getFirstChild(descriptionNode); 172 while (JavadocUtil.getNextSibling(node) != null) { 173 if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) { 174 final DetailNode descriptionNodeChild = JavadocUtil.getFirstChild(node); 175 textNodes.addAll(getAllNewlineNodes(descriptionNodeChild)); 176 } 177 else if (node.getType() == JavadocTokenTypes.HTML_ELEMENT_START 178 || node.getType() == JavadocTokenTypes.ATTRIBUTE) { 179 if (containsPreTag(node)) { 180 textNodes.add(node); 181 } 182 textNodes.addAll(getAllNewlineNodes(node)); 183 } 184 if (node.getType() == JavadocTokenTypes.LEADING_ASTERISK) { 185 textNodes.add(node); 186 } 187 node = JavadocUtil.getNextSibling(node); 188 } 189 190 // Last node does not get checked in the loop 191 if (containsPreTag(node)) { 192 textNodes.add(node); 193 } 194 return textNodes; 195 } 196 197 /** 198 * Checks if the given HTML related node contains {@code <pre>} tag. 199 * 200 * @param ast the node to check 201 * @return {@code true} if the {@code <pre>} tag is inside the node, {@code false} otherwise 202 */ 203 private static boolean containsPreTag(DetailNode ast) { 204 DetailNode node = ast; 205 if (node.getType() == JavadocTokenTypes.HTML_ELEMENT_START 206 || node.getType() == JavadocTokenTypes.HTML_ELEMENT_END) { 207 node = JavadocUtil.findFirstToken(node, JavadocTokenTypes.HTML_TAG_NAME); 208 } 209 if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) { 210 final DetailNode htmlTag = JavadocUtil.getFirstChild(node); 211 if (htmlTag.getType() == JavadocTokenTypes.HTML_TAG) { 212 final DetailNode htmlElementStart = JavadocUtil.getFirstChild(htmlTag); 213 node = JavadocUtil.findFirstToken(htmlElementStart, 214 JavadocTokenTypes.HTML_TAG_NAME); 215 } 216 } 217 return "pre".equalsIgnoreCase(node.getText()); 218 } 219 220 /** 221 * Checks if the given description node is part of a block Javadoc tag. 222 * 223 * @param description the node to check 224 * @return {@code true} if the node is inside a block tag, {@code false} otherwise 225 */ 226 private static boolean isBlockDescription(DetailNode description) { 227 boolean isBlock = false; 228 DetailNode currentNode = description; 229 while (currentNode != null) { 230 if (currentNode.getType() == JavadocTokenTypes.JAVADOC_TAG) { 231 isBlock = true; 232 break; 233 } 234 currentNode = currentNode.getParent(); 235 } 236 return isBlock; 237 } 238 239 /** 240 * Checks, if description node is a description of in-line tag. 241 * 242 * @param description DESCRIPTION node. 243 * @return true, if description node is a description of in-line tag. 244 */ 245 private static boolean isInlineDescription(DetailNode description) { 246 boolean isInline = false; 247 DetailNode currentNode = description; 248 while (currentNode != null) { 249 if (currentNode.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) { 250 isInline = true; 251 break; 252 } 253 currentNode = currentNode.getParent(); 254 } 255 return isInline; 256 } 257 258}