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 * Property {@code offset} - Specify how many spaces to use for new indentation level. 042 * Type is {@code int}. 043 * Default value is {@code 4}. 044 * </li> 045 * <li> 046 * Property {@code violateExecutionOnNonTightHtml} - Control when to print violations 047 * if the Javadoc being examined by this check violates the tight html rules defined at 048 * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">Tight-HTML Rules</a>. 049 * Type is {@code boolean}. 050 * Default value is {@code false}. 051 * </li> 052 * </ul> 053 * 054 * <p> 055 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 056 * </p> 057 * 058 * <p> 059 * Violation Message Keys: 060 * </p> 061 * <ul> 062 * <li> 063 * {@code javadoc.missed.html.close} 064 * </li> 065 * <li> 066 * {@code javadoc.parse.rule.error} 067 * </li> 068 * <li> 069 * {@code javadoc.unclosedHtml} 070 * </li> 071 * <li> 072 * {@code javadoc.wrong.singleton.html.tag} 073 * </li> 074 * <li> 075 * {@code tag.continuation.indent} 076 * </li> 077 * </ul> 078 * 079 * @since 6.0 080 * 081 */ 082@StatelessCheck 083public class JavadocTagContinuationIndentationCheck extends AbstractJavadocCheck { 084 085 /** 086 * A key is pointing to the warning message text in "messages.properties" 087 * file. 088 */ 089 public static final String MSG_KEY = "tag.continuation.indent"; 090 091 /** Default tag continuation indentation. */ 092 private static final int DEFAULT_INDENTATION = 4; 093 094 /** 095 * Specify how many spaces to use for new indentation level. 096 */ 097 private int offset = DEFAULT_INDENTATION; 098 099 /** 100 * Setter to specify how many spaces to use for new indentation level. 101 * 102 * @param offset custom value. 103 * @since 6.0 104 */ 105 public void setOffset(int offset) { 106 this.offset = offset; 107 } 108 109 @Override 110 public int[] getDefaultJavadocTokens() { 111 return new int[] {JavadocTokenTypes.HTML_TAG, JavadocTokenTypes.DESCRIPTION}; 112 113 } 114 115 @Override 116 public int[] getRequiredJavadocTokens() { 117 return getAcceptableJavadocTokens(); 118 } 119 120 @Override 121 public void visitJavadocToken(DetailNode ast) { 122 if (isBlockDescription(ast) && !isInlineDescription(ast)) { 123 final List<DetailNode> textNodes = getAllNewlineNodes(ast); 124 for (DetailNode newlineNode : textNodes) { 125 final DetailNode textNode = JavadocUtil.getNextSibling(newlineNode); 126 if (textNode.getType() != JavadocTokenTypes.NEWLINE && isViolation(textNode)) { 127 log(textNode.getLineNumber(), MSG_KEY, offset); 128 } 129 } 130 } 131 } 132 133 /** 134 * Checks if a text node meets the criteria for a violation. 135 * If the text is shorter than {@code offset} characters, then a violation is 136 * detected if the text is not blank or the next node is not a newline. 137 * If the text is longer than {@code offset} characters, then a violation is 138 * detected if any of the first {@code offset} characters are not blank. 139 * 140 * @param textNode the node to check. 141 * @return true if the node has a violation. 142 */ 143 private boolean isViolation(DetailNode textNode) { 144 boolean result = false; 145 final String text = textNode.getText(); 146 if (text.length() <= offset) { 147 if (CommonUtil.isBlank(text)) { 148 final DetailNode nextNode = JavadocUtil.getNextSibling(textNode); 149 if (nextNode != null && nextNode.getType() != JavadocTokenTypes.NEWLINE) { 150 // text is blank but line hasn't ended yet 151 result = true; 152 } 153 } 154 else { 155 // text is not blank 156 result = true; 157 } 158 } 159 else if (!CommonUtil.isBlank(text.substring(1, offset + 1))) { 160 // first offset number of characters are not blank 161 result = true; 162 } 163 return result; 164 } 165 166 /** 167 * Finds and collects all NEWLINE nodes inside DESCRIPTION node. 168 * 169 * @param descriptionNode DESCRIPTION node. 170 * @return List with NEWLINE nodes. 171 */ 172 private static List<DetailNode> getAllNewlineNodes(DetailNode descriptionNode) { 173 final List<DetailNode> textNodes = new ArrayList<>(); 174 DetailNode node = JavadocUtil.getFirstChild(descriptionNode); 175 while (JavadocUtil.getNextSibling(node) != null) { 176 if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) { 177 final DetailNode descriptionNodeChild = JavadocUtil.getFirstChild(node); 178 textNodes.addAll(getAllNewlineNodes(descriptionNodeChild)); 179 } 180 else if (node.getType() == JavadocTokenTypes.HTML_ELEMENT_START 181 || node.getType() == JavadocTokenTypes.ATTRIBUTE) { 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 return textNodes; 190 } 191 192 /** 193 * Checks if the given description node is part of a block Javadoc tag. 194 * 195 * @param description the node to check 196 * @return {@code true} if the node is inside a block tag, {@code false} otherwise 197 */ 198 private static boolean isBlockDescription(DetailNode description) { 199 boolean isBlock = false; 200 DetailNode currentNode = description; 201 while (currentNode != null) { 202 if (currentNode.getType() == JavadocTokenTypes.JAVADOC_TAG) { 203 isBlock = true; 204 break; 205 } 206 currentNode = currentNode.getParent(); 207 } 208 return isBlock; 209 } 210 211 /** 212 * Checks, if description node is a description of in-line tag. 213 * 214 * @param description DESCRIPTION node. 215 * @return true, if description node is a description of in-line tag. 216 */ 217 private static boolean isInlineDescription(DetailNode description) { 218 boolean isInline = false; 219 DetailNode currentNode = description; 220 while (currentNode != null) { 221 if (currentNode.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) { 222 isInline = true; 223 break; 224 } 225 currentNode = currentNode.getParent(); 226 } 227 return isInline; 228 } 229 230}