View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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       * Creates a new {@code JavadocTagContinuationIndentationCheck} instance.
72       */
73      public JavadocTagContinuationIndentationCheck() {
74          // no code by default
75      }
76  
77      /**
78       * Setter to specify how many spaces to use for new indentation level.
79       *
80       * @param offset custom value.
81       * @since 6.0
82       */
83      public void setOffset(int offset) {
84          this.offset = offset;
85      }
86  
87      @Override
88      public int[] getDefaultJavadocTokens() {
89          return new int[] {
90              JavadocCommentsTokenTypes.HTML_ELEMENT,
91              JavadocCommentsTokenTypes.DESCRIPTION,
92          };
93      }
94  
95      @Override
96      public int[] getRequiredJavadocTokens() {
97          return getAcceptableJavadocTokens();
98      }
99  
100     @Override
101     public void visitJavadocToken(DetailNode ast) {
102         if (isBlockDescription(ast) && !isInlineDescription(ast)) {
103             final List<DetailNode> textNodes = getTargetedTextNodes(ast);
104             for (DetailNode textNode : textNodes) {
105                 if (isViolation(textNode)) {
106                     log(textNode.getLineNumber(), MSG_KEY, offset);
107                 }
108             }
109         }
110     }
111 
112     /**
113      * Returns all targeted text nodes from the given AST node.
114      * This method decides whether to process the node as a description node
115      * or as an HTML element node and delegates to the appropriate helper method.
116      *
117      * @param ast the AST node to process
118      * @return list of targeted text nodes
119      */
120     private static List<DetailNode> getTargetedTextNodes(DetailNode ast) {
121         final List<DetailNode> textNodes;
122         if (ast.getType() == JavadocCommentsTokenTypes.DESCRIPTION) {
123             textNodes = getTargetedTextNodesInsideDescription(ast);
124         }
125         else {
126             textNodes = getTargetedTextNodesInsideHtmlElement(ast);
127         }
128         return textNodes;
129     }
130 
131     /**
132      * Returns all targeted text nodes within an HTML element subtree.
133      *
134      * @param ast the HTML element AST node
135      * @return list of targeted text nodes inside the HTML element
136      */
137     private static List<DetailNode> getTargetedTextNodesInsideHtmlElement(DetailNode ast) {
138         final List<DetailNode> textNodes = new ArrayList<>();
139         if (!JavadocUtil.isTag(ast, PRE_TAG) && !isInsidePreTag(ast)) {
140             final DetailNode prevSibling = ast.getPreviousSibling();
141             if (prevSibling != null && isTargetTextNode(prevSibling)) {
142                 textNodes.add(prevSibling);
143             }
144             DetailNode node = ast.getFirstChild();
145             while (node != null) {
146                 if (node.getType() == JavadocCommentsTokenTypes.HTML_CONTENT) {
147                     // HTML_CONTENT contain text nodes only, so it can be treated as
148                     // DESCRIPTION node
149                     textNodes.addAll(getTargetedTextNodesInsideDescription(node));
150                 }
151                 else if (subtreeContainsAttributeValue(node)) {
152                     textNodes.addAll(getTargetedTextNodesInsideHtmlElement(node));
153                 }
154                 else if (isTargetTextNode(node)) {
155                     textNodes.add(node);
156                 }
157                 node = node.getNextSibling();
158             }
159         }
160         return textNodes;
161     }
162 
163     /**
164      * Checks whether the given subtree node represents part of an HTML tag
165      * structure that may contain attribute values.
166      *
167      * @param node the AST node to check
168      * @return true if the subtree may contain attribute values, false otherwise
169      */
170     private static boolean subtreeContainsAttributeValue(DetailNode node) {
171         return node.getType() == JavadocCommentsTokenTypes.HTML_TAG_START
172             || node.getType() == JavadocCommentsTokenTypes.HTML_ATTRIBUTES
173             || node.getType() == JavadocCommentsTokenTypes.HTML_ATTRIBUTE;
174     }
175 
176     /**
177      * Returns all targeted text nodes inside a description node.
178      *
179      * @param descriptionNode the DESCRIPTION node to process
180      * @return list of targeted text nodes inside the description node
181      */
182     private static List<DetailNode> getTargetedTextNodesInsideDescription(
183             DetailNode descriptionNode) {
184         final List<DetailNode> textNodes = new ArrayList<>();
185         DetailNode node = descriptionNode.getFirstChild();
186         final DetailNode previousSibling = descriptionNode.getPreviousSibling();
187 
188         // special case if the text node is previous sibling of the description node
189         if (isTargetTextNode(previousSibling)) {
190             textNodes.add(previousSibling);
191         }
192 
193         // special case for the first child, because leading asterisk
194         // will be previous sibling of the parent (description node) not the node itself
195         if (isLeadingAsterisk(descriptionNode.getPreviousSibling())) {
196             textNodes.add(node);
197         }
198 
199         while (node != null) {
200             if (isTargetTextNode(node)) {
201                 textNodes.add(node);
202             }
203             node = node.getNextSibling();
204         }
205 
206         return textNodes;
207     }
208 
209     /**
210      * Determines whether the given node is a targeted node.
211      *
212      * @param node the AST node to check
213      * @return true if the node is a targeted node, false otherwise
214      */
215     private static boolean isTargetTextNode(DetailNode node) {
216         final DetailNode previousSibling = node.getPreviousSibling();
217 
218         return previousSibling != null
219             && isTextOrAttributeValueNode(node)
220             && !isBeforePreTag(node)
221             && isLeadingAsterisk(previousSibling);
222     }
223 
224     /**
225      * Checks whether the given node is a leading asterisk.
226      *
227      * @param node the AST node to check
228      * @return true if the node is a leading asterisk, false otherwise
229      */
230     private static boolean isLeadingAsterisk(DetailNode node) {
231         return node.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK
232             || node.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISKS;
233     }
234 
235     /**
236      * Checks if a node is located before a {@code pre} tag.
237      *
238      * @param node the node to check
239      * @return true if the node is before a pre tag, false otherwise
240      */
241     private static boolean isBeforePreTag(DetailNode node) {
242         final DetailNode nextSibling = node.getNextSibling();
243         final boolean isBeforePreTag;
244         if (nextSibling != null
245                 && nextSibling.getType() == JavadocCommentsTokenTypes.DESCRIPTION) {
246             isBeforePreTag = JavadocUtil.isTag(nextSibling.getFirstChild(), PRE_TAG);
247         }
248         else if (nextSibling != null) {
249             isBeforePreTag = JavadocUtil.isTag(nextSibling, PRE_TAG);
250         }
251         else {
252             isBeforePreTag = false;
253         }
254         return isBeforePreTag;
255     }
256 
257     /**
258      * Checks if a node is inside a {@code pre} tag.
259      *
260      * @param node the node to check
261      * @return true if the node is inside a pre tag, false otherwise
262      */
263     private static boolean isInsidePreTag(DetailNode node) {
264         final DetailNode htmlElementParent = node.getParent().getParent();
265         return JavadocUtil.isTag(htmlElementParent, PRE_TAG);
266     }
267 
268     /**
269      * Checks whether the given node is either a TEXT node or an ATTRIBUTE_VALUE node.
270      *
271      * @param node the AST node to check
272      * @return true if the node is a TEXT or ATTRIBUTE_VALUE node, false otherwise
273      */
274     private static boolean isTextOrAttributeValueNode(DetailNode node) {
275         return node.getType() == JavadocCommentsTokenTypes.TEXT
276             || node.getType() == JavadocCommentsTokenTypes.ATTRIBUTE_VALUE;
277     }
278 
279     /**
280      * Checks if a text node meets the criteria for a violation.
281      * If the text is shorter than {@code offset} characters, then a violation is
282      * detected if the text is not blank or the next node is not a newline.
283      * If the text is longer than {@code offset} characters, then a violation is
284      * detected if any of the first {@code offset} characters are not blank.
285      *
286      * @param textNode the node to check.
287      * @return true if the node has a violation.
288      */
289     private boolean isViolation(DetailNode textNode) {
290         boolean result = false;
291         final String text = textNode.getText();
292         if (text.length() <= offset) {
293             if (CommonUtil.isBlank(text)) {
294                 final DetailNode nextNode = textNode.getNextSibling();
295                 if (nextNode.getType() != JavadocCommentsTokenTypes.NEWLINE) {
296                     // text is blank but line hasn't ended yet
297                     result = true;
298                 }
299             }
300             else {
301                 // text is not blank
302                 result = true;
303             }
304         }
305         else if (!CommonUtil.isBlank(text.substring(1, offset + 1))) {
306             // first offset number of characters are not blank
307             result = true;
308         }
309         return result;
310     }
311 
312     /**
313      * Checks if the given description node is part of a block Javadoc tag.
314      *
315      * @param description the node to check
316      * @return {@code true} if the node is inside a block tag, {@code false} otherwise
317      */
318     private static boolean isBlockDescription(DetailNode description) {
319         boolean isBlock = false;
320         DetailNode currentNode = description;
321         while (currentNode != null) {
322             if (currentNode.getType() == JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG) {
323                 isBlock = true;
324                 break;
325             }
326             currentNode = currentNode.getParent();
327         }
328         return isBlock;
329     }
330 
331     /**
332      * Checks, if description node is a description of in-line tag.
333      *
334      * @param description DESCRIPTION node.
335      * @return true, if description node is a description of in-line tag.
336      */
337     private static boolean isInlineDescription(DetailNode description) {
338         boolean isInline = false;
339         DetailNode currentNode = description;
340         while (currentNode != null) {
341             if (currentNode.getType() == JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG) {
342                 isInline = true;
343                 break;
344             }
345             currentNode = currentNode.getParent();
346         }
347         return isInline;
348     }
349 
350 }