View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.checkstyle;
6   
7   import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
8   import com.puppycrawl.tools.checkstyle.api.DetailAST;
9   import com.puppycrawl.tools.checkstyle.api.TokenTypes;
10  
11  /**
12   * Check for the empty Javadoc line before the group of at-clauses.
13   *
14   * <p>If the Javadoc body before at-clauses consists of a single paragraph,
15   * there must be no empty line between the body and the first at-clause.
16   * If the body contains more than one paragraph (separated by empty Javadoc
17   * lines), then an empty Javadoc line is required right before the first
18   * at-clause. See
19   * <a href="https://github.com/yegor256/qulice/issues/708">#708</a>.
20   *
21   * <p>The following Javadoc will be reported as a violation, since its body
22   * is a single paragraph and yet it is separated from the at-clauses by an
23   * empty line:
24   * <pre>
25   * &#47;**
26   *  * Just one line here.
27   *  <span style="color:red" >*</span>
28   *  * &#64;since 0.1
29   *  *&#47;
30   * </pre>
31   *
32   * <p>And this one will be reported too, since its body has more than one
33   * paragraph but there is no empty line before the at-clauses:
34   * <pre>
35   * &#47;**
36   *  * First line.
37   *  *
38   *  * Second par.
39   *  <span style="color:red" >* &#64;since 0.1</span>
40   *  *&#47;
41   * </pre>
42   *
43   * @since 0.27.0
44   */
45  public final class JavadocEmptyLineBeforeTagCheck extends AbstractCheck {
46  
47      @Override
48      public int[] getDefaultTokens() {
49          return new int[] {
50              TokenTypes.PACKAGE_DEF,
51              TokenTypes.CLASS_DEF,
52              TokenTypes.INTERFACE_DEF,
53              TokenTypes.ANNOTATION_DEF,
54              TokenTypes.ANNOTATION_FIELD_DEF,
55              TokenTypes.ENUM_DEF,
56              TokenTypes.ENUM_CONSTANT_DEF,
57              TokenTypes.VARIABLE_DEF,
58              TokenTypes.CTOR_DEF,
59              TokenTypes.METHOD_DEF,
60          };
61      }
62  
63      @Override
64      public int[] getAcceptableTokens() {
65          return this.getDefaultTokens();
66      }
67  
68      @Override
69      public int[] getRequiredTokens() {
70          return this.getDefaultTokens();
71      }
72  
73      @Override
74      public void visitToken(final DetailAST ast) {
75          final String[] lines = this.getLines();
76          final int current = ast.getLineNo();
77          final int start =
78              JavadocEmptyLineBeforeTagCheck.findCommentStart(lines, current) + 1;
79          final int end =
80              JavadocEmptyLineBeforeTagCheck.findCommentEnd(lines, current) - 1;
81          if (JavadocEmptyLineBeforeTagCheck.isNodeHavingJavadoc(ast, start)
82              && start < lines.length && end >= start) {
83              final int tag =
84                  JavadocEmptyLineBeforeTagCheck.findFirstTag(lines, start, end);
85              if (tag > start) {
86                  this.inspect(lines, start, tag);
87              }
88          }
89      }
90  
91      /**
92       * Inspect the part of the Javadoc that lies between the opening
93       * and the first at-clause.
94       * @param lines All lines of the source file
95       * @param start First Javadoc content line (0-based)
96       * @param tag Line of the first at-clause (0-based)
97       */
98      private void inspect(final String[] lines, final int start, final int tag) {
99          int body = tag - 1;
100         while (body >= start
101             && JavadocEmptyLineBeforeTagCheck.isJavadocLineEmpty(lines[body])) {
102             body -= 1;
103         }
104         if (body >= start) {
105             boolean multi = false;
106             for (int pos = start; pos <= body; pos += 1) {
107                 if (JavadocEmptyLineBeforeTagCheck.isJavadocLineEmpty(lines[pos])) {
108                     multi = true;
109                     break;
110                 }
111             }
112             final boolean empty =
113                 JavadocEmptyLineBeforeTagCheck.isJavadocLineEmpty(lines[tag - 1]);
114             if (multi && !empty) {
115                 this.log(
116                     tag + 1,
117                     "Empty Javadoc line required before at-clauses, since the description has multiple paragraphs"
118                 );
119             } else if (!multi && empty) {
120                 this.log(
121                     tag,
122                     "Empty Javadoc line before at-clauses is not allowed, since the description is a single paragraph"
123                 );
124             }
125         }
126     }
127 
128     /**
129      * Check if Javadoc line is empty.
130      * @param line Javadoc line
131      * @return True when Javadoc line is empty
132      */
133     private static boolean isJavadocLineEmpty(final String line) {
134         return "*".equals(line.trim());
135     }
136 
137     /**
138      * Check if node has Javadoc.
139      * @param node Node to be checked for Javadoc
140      * @param start Line number where comment starts
141      * @return True when node has Javadoc
142      */
143     private static boolean isNodeHavingJavadoc(final DetailAST node,
144         final int start) {
145         int previous = 0;
146         final DetailAST prev = node.getPreviousSibling();
147         if (prev != null) {
148             previous = prev.getLineNo();
149         }
150         return start > previous;
151     }
152 
153     /**
154      * Find Javadoc starting comment.
155      * @param lines List of lines to check
156      * @param start Start searching from this line number
157      * @return Line number with found starting comment or -1 otherwise
158      */
159     private static int findCommentStart(final String[] lines, final int start) {
160         return JavadocEmptyLineBeforeTagCheck.findTrimmedTextUp(lines, start, "/**");
161     }
162 
163     /**
164      * Find Javadoc ending comment.
165      * @param lines Array of lines to check
166      * @param start Start searching from this line number
167      * @return Line number with found ending comment, or -1 if it wasn't found
168      */
169     private static int findCommentEnd(final String[] lines, final int start) {
170         int found = -1;
171         for (int pos = start - 1; pos >= 0; pos -= 1) {
172             final String trimmed = lines[pos].trim();
173             if ("*/".equals(trimmed) || "**/".equals(trimmed)) {
174                 found = pos;
175                 break;
176             }
177         }
178         return found;
179     }
180 
181     /**
182      * Find the first at-clause line inside the Javadoc comment.
183      * @param lines All lines of the file
184      * @param start First Javadoc content line (0-based)
185      * @param end Last Javadoc content line (0-based)
186      * @return Line number of the first at-clause, or -1 if not found
187      */
188     private static int findFirstTag(final String[] lines, final int start,
189         final int end) {
190         int found = -1;
191         for (int pos = start; pos <= end; pos += 1) {
192             final String trimmed = lines[pos].trim();
193             if (trimmed.startsWith("* @") || trimmed.startsWith("*@")) {
194                 found = pos;
195                 break;
196             }
197         }
198         return found;
199     }
200 
201     /**
202      * Find a text in lines, by going up.
203      * @param lines Array of lines to check
204      * @param start Start searching from this line number
205      * @param text Text to find
206      * @return Line number with found text, or -1 if it wasn't found
207      */
208     private static int findTrimmedTextUp(final String[] lines,
209         final int start, final String text) {
210         int found = -1;
211         for (int pos = start - 1; pos >= 0; pos -= 1) {
212             if (lines[pos].trim().equals(text)) {
213                 found = pos;
214                 break;
215             }
216         }
217         return found;
218     }
219 }