View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.javadoc;
21  
22  import java.util.ArrayList;
23  import java.util.List;
24  
25  import com.puppycrawl.tools.checkstyle.StatelessCheck;
26  import com.puppycrawl.tools.checkstyle.api.DetailNode;
27  import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
28  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
29  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
30  
31  /**
32   * <div>
33   * Checks the indentation of the continuation lines in block tags. That is whether the continued
34   * description of at clauses should be indented or not. If the text is not properly indented it
35   * throws a violation. A continuation line is when the description starts/spans past the line with
36   * the tag. Default indentation required is at least 4, but this can be changed with the help of
37   * properties below.
38   * </div>
39   * <ul>
40   * <li>
41   * Notes:
42   * This check does not validate the indentation of lines inside {@code pre} tags.
43   * </li>
44   * </ul>
45   *
46   * @since 6.0
47   */
48  @StatelessCheck
49  public class JavadocTagContinuationIndentationCheck extends AbstractJavadocCheck {
50  
51      /**
52       * A key is pointing to the warning message text in "messages.properties"
53       * file.
54       */
55      public static final String MSG_KEY = "tag.continuation.indent";
56  
57      /** Default tag continuation indentation. */
58      private static final int DEFAULT_INDENTATION = 4;
59  
60      /**
61       * Constant for the pre tag name.
62       */
63      private static final String PRE_TAG = "pre";
64  
65      /**
66       * Specify how many spaces to use for new indentation level.
67       */
68      private int offset = DEFAULT_INDENTATION;
69  
70      /**
71       * Setter to specify how many spaces to use for new indentation level.
72       *
73       * @param offset custom value.
74       * @since 6.0
75       */
76      public void setOffset(int offset) {
77          this.offset = offset;
78      }
79  
80      @Override
81      public int[] getDefaultJavadocTokens() {
82          return new int[] {
83              JavadocCommentsTokenTypes.HTML_ELEMENT,
84              JavadocCommentsTokenTypes.DESCRIPTION,
85          };
86      }
87  
88      @Override
89      public int[] getRequiredJavadocTokens() {
90          return getAcceptableJavadocTokens();
91      }
92  
93      @Override
94      public void visitJavadocToken(DetailNode ast) {
95          if (isBlockDescription(ast) && !isInlineDescription(ast)) {
96              final List<DetailNode> textNodes = getTargetedTextNodes(ast);
97              for (DetailNode textNode : textNodes) {
98                  if (isViolation(textNode)) {
99                      log(textNode.getLineNumber(), MSG_KEY, offset);
100                 }
101             }
102         }
103     }
104 
105     /**
106      * Returns all targeted text nodes from the given AST node.
107      * This method decides whether to process the node as a description node
108      * or as an HTML element node and delegates to the appropriate helper method.
109      *
110      * @param ast the AST node to process
111      * @return list of targeted text nodes
112      */
113     private static List<DetailNode> getTargetedTextNodes(DetailNode ast) {
114         final List<DetailNode> textNodes;
115         if (ast.getType() == JavadocCommentsTokenTypes.DESCRIPTION) {
116             textNodes = getTargetedTextNodesInsideDescription(ast);
117         }
118         else {
119             textNodes = getTargetedTextNodesInsideHtmlElement(ast);
120         }
121         return textNodes;
122     }
123 
124     /**
125      * Returns all targeted text nodes within an HTML element subtree.
126      *
127      * @param ast the HTML element AST node
128      * @return list of targeted text nodes inside the HTML element
129      */
130     private static List<DetailNode> getTargetedTextNodesInsideHtmlElement(DetailNode ast) {
131         final List<DetailNode> textNodes = new ArrayList<>();
132 
133         if (!JavadocUtil.isTag(ast, PRE_TAG) && !isInsidePreTag(ast)) {
134             DetailNode node = ast.getFirstChild();
135             while (node != null) {
136                 if (node.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) {
137                     // HTML_CONTENT contain text nodes only, so it can be treated as
138                     // DESCRIPTION node
139                     textNodes.addAll(getTargetedTextNodesInsideDescription(node));
140                 }
141                 else if (subtreeContainsAttributeValue(node)) {
142                     textNodes.addAll(getTargetedTextNodesInsideHtmlElement(node));
143                 }
144                 else if (isTargetTextNode(node)) {
145                     textNodes.add(node);
146                 }
147                 node = node.getNextSibling();
148             }
149         }
150         return textNodes;
151     }
152 
153     /**
154      * Checks whether the given subtree node represents part of an HTML tag
155      * structure that may contain attribute values.
156      *
157      * @param node the AST node to check
158      * @return true if the subtree may contain attribute values, false otherwise
159      */
160     private static boolean subtreeContainsAttributeValue(DetailNode node) {
161         return node.getType() == JavadocCommentsTokenTypes.HTML_TAG_START
162             || node.getType() == JavadocCommentsTokenTypes.HTML_ATTRIBUTES
163             || node.getType() == JavadocCommentsTokenTypes.HTML_ATTRIBUTE;
164     }
165 
166     /**
167      * Returns all targeted text nodes inside a description node.
168      *
169      * @param descriptionNode the DESCRIPTION node to process
170      * @return list of targeted text nodes inside the description node
171      */
172     private static List<DetailNode> getTargetedTextNodesInsideDescription(
173             DetailNode descriptionNode) {
174         final List<DetailNode> textNodes = new ArrayList<>();
175         DetailNode node = descriptionNode.getFirstChild();
176         final DetailNode previousSibling = descriptionNode.getPreviousSibling();
177 
178         // special case if the text node is previous sibling of the description node
179         if (isTargetTextNode(previousSibling)) {
180             textNodes.add(previousSibling);
181         }
182 
183         // special case for the first child, because leading asterisk
184         // will be previous sibling of the parent (description node) not the node itself
185         if (descriptionNode.getPreviousSibling().getType()
186                 == JavadocCommentsTokenTypes.LEADING_ASTERISK) {
187             textNodes.add(node);
188         }
189 
190         while (node != null) {
191             if (isTargetTextNode(node)) {
192                 textNodes.add(node);
193             }
194             node = node.getNextSibling();
195         }
196 
197         return textNodes;
198     }
199 
200     /**
201      * Determines whether the given node is a targeted node.
202      *
203      * @param node the AST node to check
204      * @return true if the node is a targeted node, false otherwise
205      */
206     private static boolean isTargetTextNode(DetailNode node) {
207         final DetailNode previousSibling = node.getPreviousSibling();
208 
209         return previousSibling != null
210             && isTextOrAttributeValueNode(node)
211             && !isBeforePreTag(node)
212             && previousSibling.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK;
213     }
214 
215     /**
216      * Checks if a node is located before a {@code pre} tag.
217      *
218      * @param node the node to check
219      * @return true if the node is before a pre tag, false otherwise
220      */
221     private static boolean isBeforePreTag(DetailNode node) {
222         final DetailNode nextSibling = node.getNextSibling();
223         final boolean isBeforePreTag;
224         if (nextSibling != null
225                 && nextSibling.getType() == JavadocCommentsTokenTypes.DESCRIPTION) {
226             isBeforePreTag = JavadocUtil.isTag(nextSibling.getFirstChild(), PRE_TAG);
227         }
228         else if (nextSibling != null) {
229             isBeforePreTag = JavadocUtil.isTag(nextSibling, PRE_TAG);
230         }
231         else {
232             isBeforePreTag = false;
233         }
234         return isBeforePreTag;
235     }
236 
237     /**
238      * Checks if a node is inside a {@code pre} tag.
239      *
240      * @param node the node to check
241      * @return true if the node is inside a pre tag, false otherwise
242      */
243     private static boolean isInsidePreTag(DetailNode node) {
244         final DetailNode htmlElementParent = node.getParent().getParent();
245         return JavadocUtil.isTag(htmlElementParent, PRE_TAG);
246     }
247 
248     /**
249      * Checks whether the given node is either a TEXT node or an ATTRIBUTE_VALUE node.
250      *
251      * @param node the AST node to check
252      * @return true if the node is a TEXT or ATTRIBUTE_VALUE node, false otherwise
253      */
254     private static boolean isTextOrAttributeValueNode(DetailNode node) {
255         return node.getType() == JavadocCommentsTokenTypes.TEXT
256             || node.getType() == JavadocCommentsTokenTypes.ATTRIBUTE_VALUE;
257     }
258 
259     /**
260      * Checks if a text node meets the criteria for a violation.
261      * If the text is shorter than {@code offset} characters, then a violation is
262      * detected if the text is not blank or the next node is not a newline.
263      * If the text is longer than {@code offset} characters, then a violation is
264      * detected if any of the first {@code offset} characters are not blank.
265      *
266      * @param textNode the node to check.
267      * @return true if the node has a violation.
268      */
269     private boolean isViolation(DetailNode textNode) {
270         boolean result = false;
271         final String text = textNode.getText();
272         if (text.length() <= offset) {
273             if (CommonUtil.isBlank(text)) {
274                 final DetailNode nextNode = textNode.getNextSibling();
275                 if (nextNode.getType() != JavadocCommentsTokenTypes.NEWLINE) {
276                     // text is blank but line hasn't ended yet
277                     result = true;
278                 }
279             }
280             else {
281                 // text is not blank
282                 result = true;
283             }
284         }
285         else if (!CommonUtil.isBlank(text.substring(1, offset + 1))) {
286             // first offset number of characters are not blank
287             result = true;
288         }
289         return result;
290     }
291 
292     /**
293      * Checks if the given description node is part of a block Javadoc tag.
294      *
295      * @param description the node to check
296      * @return {@code true} if the node is inside a block tag, {@code false} otherwise
297      */
298     private static boolean isBlockDescription(DetailNode description) {
299         boolean isBlock = false;
300         DetailNode currentNode = description;
301         while (currentNode != null) {
302             if (currentNode.getType() == JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG) {
303                 isBlock = true;
304                 break;
305             }
306             currentNode = currentNode.getParent();
307         }
308         return isBlock;
309     }
310 
311     /**
312      * Checks, if description node is a description of in-line tag.
313      *
314      * @param description DESCRIPTION node.
315      * @return true, if description node is a description of in-line tag.
316      */
317     private static boolean isInlineDescription(DetailNode description) {
318         boolean isInline = false;
319         DetailNode currentNode = description;
320         while (currentNode != null) {
321             if (currentNode.getType() == JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG) {
322                 isInline = true;
323                 break;
324             }
325             currentNode = currentNode.getParent();
326         }
327         return isInline;
328     }
329 }