View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.Arrays;
24  import java.util.List;
25  
26  import com.puppycrawl.tools.checkstyle.StatelessCheck;
27  import com.puppycrawl.tools.checkstyle.api.DetailNode;
28  import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
29  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
30  
31  /**
32   * <div>
33   * Checks that one blank line before the block tag if it is present in Javadoc.
34   * </div>
35   *
36   * <ul>
37   * <li>
38   * Property {@code violateExecutionOnNonTightHtml} - Control when to print violations
39   * if the Javadoc being examined by this check violates the tight html rules defined at
40   * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
41   * Tight-HTML Rules</a>.
42   * Type is {@code boolean}.
43   * Default value is {@code false}.
44   * </li>
45   * </ul>
46   *
47   * <p>
48   * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
49   * </p>
50   *
51   * <p>
52   * Violation Message Keys:
53   * </p>
54   * <ul>
55   * <li>
56   * {@code javadoc.missed.html.close}
57   * </li>
58   * <li>
59   * {@code javadoc.parse.rule.error}
60   * </li>
61   * <li>
62   * {@code javadoc.tag.line.before}
63   * </li>
64   * <li>
65   * {@code javadoc.unclosedHtml}
66   * </li>
67   * <li>
68   * {@code javadoc.wrong.singleton.html.tag}
69   * </li>
70   * </ul>
71   *
72   * @since 8.36
73   */
74  @StatelessCheck
75  public class RequireEmptyLineBeforeBlockTagGroupCheck extends AbstractJavadocCheck {
76  
77      /**
78       * The key in "messages.properties" for the message that describes a tag in javadoc
79       * requiring an empty line before it.
80       */
81      public static final String MSG_JAVADOC_TAG_LINE_BEFORE = "javadoc.tag.line.before";
82  
83      /**
84       * Case when space separates the tag and the asterisk like in the below example.
85       * <pre>
86       *  /**
87       *   * &#64;param noSpace there is no space here
88       * </pre>
89       */
90      private static final List<Integer> ONLY_TAG_VARIATION_1 = Arrays.asList(
91              JavadocTokenTypes.WS,
92              JavadocTokenTypes.LEADING_ASTERISK,
93              JavadocTokenTypes.NEWLINE);
94  
95      /**
96       * Case when no space separates the tag and the asterisk like in the below example.
97       * <pre>
98       *  /**
99       *   *&#64;param noSpace there is no space here
100      * </pre>
101      */
102     private static final List<Integer> ONLY_TAG_VARIATION_2 = Arrays.asList(
103             JavadocTokenTypes.LEADING_ASTERISK,
104             JavadocTokenTypes.NEWLINE);
105 
106     /**
107      * Returns only javadoc tags so visitJavadocToken only receives javadoc tags.
108      *
109      * @return only javadoc tags.
110      */
111     @Override
112     public int[] getDefaultJavadocTokens() {
113         return new int[] {
114             JavadocTokenTypes.JAVADOC_TAG,
115         };
116     }
117 
118     @Override
119     public int[] getRequiredJavadocTokens() {
120         return getAcceptableJavadocTokens();
121     }
122 
123     /**
124      * Logs when there is no empty line before the tag.
125      *
126      * @param tagNode the at tag node to check for an empty space before it.
127      */
128     @Override
129     public void visitJavadocToken(DetailNode tagNode) {
130         // No need to filter token because overridden getDefaultJavadocTokens ensures that we only
131         // receive JAVADOC_TAG DetailNode.
132         if (!isAnotherTagBefore(tagNode)
133                 && !isOnlyTagInWholeJavadoc(tagNode)
134                 && hasInsufficientConsecutiveNewlines(tagNode)) {
135             log(tagNode.getLineNumber(),
136                     MSG_JAVADOC_TAG_LINE_BEFORE,
137                     tagNode.getChildren()[0].getText());
138         }
139     }
140 
141     /**
142      * Returns true when there is a javadoc tag before the provided tagNode.
143      *
144      * @param tagNode the javadoc tag node, to look for more tags before it.
145      * @return true when there is a javadoc tag before the provided tagNode.
146      */
147     private static boolean isAnotherTagBefore(DetailNode tagNode) {
148         boolean found = false;
149         DetailNode currentNode = JavadocUtil.getPreviousSibling(tagNode);
150         while (currentNode != null) {
151             if (currentNode.getType() == JavadocTokenTypes.JAVADOC_TAG) {
152                 found = true;
153                 break;
154             }
155             currentNode = JavadocUtil.getPreviousSibling(currentNode);
156         }
157         return found;
158     }
159 
160     /**
161      * Returns true when there are is only whitespace and asterisks before the provided tagNode.
162      * When javadoc has only a javadoc tag like {@literal @} in it, the JAVADOC_TAG in a JAVADOC
163      * detail node will always have 2 or 3 siblings before it. The parse tree looks like:
164      * <pre>
165      * JAVADOC[3x0]
166      * |--NEWLINE[3x0] : [\n]
167      * |--LEADING_ASTERISK[4x0] : [ *]
168      * |--WS[4x2] : [ ]
169      * |--JAVADOC_TAG[4x3] : [@param T The bar.\n ]
170      * </pre>
171      * Or it can also look like:
172      * <pre>
173      * JAVADOC[3x0]
174      * |--NEWLINE[3x0] : [\n]
175      * |--LEADING_ASTERISK[4x0] : [ *]
176      * |--JAVADOC_TAG[4x3] : [@param T The bar.\n ]
177      * </pre>
178      * We do not include the variation
179      * <pre>
180      *  /**&#64;param noSpace there is no space here
181      * </pre>
182      * which results in the tree
183      * <pre>
184      * JAVADOC[3x0]
185      * |--JAVADOC_TAG[4x3] : [@param noSpace there is no space here\n ]
186      * </pre>
187      * because this one is invalid. We must recommend placing a blank line to separate &#64;param
188      * from the first javadoc asterisks.
189      *
190      * @param tagNode the at tag node to check if there is nothing before it
191      * @return true if there is no text before the tagNode
192      */
193     private static boolean isOnlyTagInWholeJavadoc(DetailNode tagNode) {
194         final List<Integer> previousNodeTypes = new ArrayList<>();
195         DetailNode currentNode = JavadocUtil.getPreviousSibling(tagNode);
196         while (currentNode != null) {
197             previousNodeTypes.add(currentNode.getType());
198             currentNode = JavadocUtil.getPreviousSibling(currentNode);
199         }
200         return ONLY_TAG_VARIATION_1.equals(previousNodeTypes)
201                 || ONLY_TAG_VARIATION_2.equals(previousNodeTypes);
202     }
203 
204     /**
205      * Returns true when there are not enough empty lines before the provided tagNode.
206      *
207      * <p>Iterates through the previous siblings of the tagNode looking for empty lines until
208      * there are no more siblings or it hits something other than asterisk, whitespace or newline.
209      * If it finds at least one empty line, return true. Return false otherwise.</p>
210      *
211      * @param tagNode the tagNode to check if there are sufficient empty lines before it.
212      * @return true if there are not enough empty lines before the tagNode.
213      */
214     private static boolean hasInsufficientConsecutiveNewlines(DetailNode tagNode) {
215         int count = 0;
216         DetailNode currentNode = JavadocUtil.getPreviousSibling(tagNode);
217         while (currentNode != null
218                 && (currentNode.getType() == JavadocTokenTypes.NEWLINE
219                 || currentNode.getType() == JavadocTokenTypes.WS
220                 || currentNode.getType() == JavadocTokenTypes.LEADING_ASTERISK)) {
221             if (currentNode.getType() == JavadocTokenTypes.NEWLINE) {
222                 count++;
223             }
224             currentNode = JavadocUtil.getPreviousSibling(currentNode);
225         }
226 
227         return count <= 1;
228     }
229 }