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>The group of at-clauses ({@code @param}, {@code @return},
15   * {@code @since}, {@code @throws} and the rest) must always be separated
16   * from the description above it by an empty Javadoc line, no matter how
17   * many paragraphs that description has. This holds for every Javadoc
18   * block: packages, classes, interfaces, enums, enum constants,
19   * annotations, annotation fields, records, fields, constructors and
20   * methods. See
21   * <a href="https://github.com/yegor256/qulice/issues/1810">#1810</a>.</p>
22   *
23   * <p>The following Javadoc will be reported as a violation, since its
24   * description touches the first at-clause:</p>
25   * <pre>
26   * &#47;**
27   *  * Just one line here.
28   *  <span style="color:red" >* &#64;since 0.1</span>
29   *  *&#47;
30   * </pre>
31   *
32   * <p>And this is how it should be written instead:</p>
33   * <pre>
34   * &#47;**
35   *  * Just one line here.
36   *  *
37   *  * &#64;since 0.1
38   *  *&#47;
39   * </pre>
40   *
41   * <p>A Javadoc block with no at-clauses at all, and a block whose very
42   * first line already is an at-clause, are both left alone.</p>
43   *
44   * @since 0.27.0
45   */
46  public final class JavadocEmptyLineBeforeTagCheck extends AbstractCheck {
47  
48      /**
49       * Default constructor.
50       */
51      public JavadocEmptyLineBeforeTagCheck() {
52          // nothing to initialize
53      }
54  
55      @Override
56      public int[] getDefaultTokens() {
57          return new int[] {
58              TokenTypes.PACKAGE_DEF,
59              TokenTypes.CLASS_DEF,
60              TokenTypes.INTERFACE_DEF,
61              TokenTypes.ANNOTATION_DEF,
62              TokenTypes.ANNOTATION_FIELD_DEF,
63              TokenTypes.ENUM_DEF,
64              TokenTypes.ENUM_CONSTANT_DEF,
65              TokenTypes.RECORD_DEF,
66              TokenTypes.VARIABLE_DEF,
67              TokenTypes.CTOR_DEF,
68              TokenTypes.COMPACT_CTOR_DEF,
69              TokenTypes.METHOD_DEF,
70          };
71      }
72  
73      @Override
74      public int[] getAcceptableTokens() {
75          return this.getDefaultTokens();
76      }
77  
78      @Override
79      public int[] getRequiredTokens() {
80          return this.getDefaultTokens();
81      }
82  
83      @Override
84      public void visitToken(final DetailAST ast) {
85          final String[] lines = this.getLines();
86          final int current = ast.getLineNo();
87          final int start =
88              JavadocEmptyLineBeforeTagCheck.findCommentStart(lines, current) + 1;
89          final int end =
90              JavadocEmptyLineBeforeTagCheck.findCommentEnd(lines, current) - 1;
91          if (JavadocEmptyLineBeforeTagCheck.isNodeHavingJavadoc(ast, start)
92              && start < lines.length && end >= start) {
93              final int tag =
94                  JavadocEmptyLineBeforeTagCheck.findFirstTag(lines, start, end);
95              if (tag > start
96                  && !JavadocEmptyLineBeforeTagCheck.isJavadocLineEmpty(lines[tag - 1])) {
97                  this.log(
98                      tag + 1,
99                      "Empty Javadoc line required before the block of at-clauses"
100                 );
101             }
102         }
103     }
104 
105     private static boolean isJavadocLineEmpty(final String line) {
106         return "*".equals(line.trim());
107     }
108 
109     private static boolean isNodeHavingJavadoc(final DetailAST node,
110         final int start) {
111         int previous = 0;
112         final DetailAST prev = node.getPreviousSibling();
113         if (prev != null) {
114             previous = prev.getLineNo();
115         }
116         return start > previous;
117     }
118 
119     private static int findCommentStart(final String[] lines, final int start) {
120         return JavadocEmptyLineBeforeTagCheck.findTrimmedTextUp(lines, start, "/**");
121     }
122 
123     private static int findCommentEnd(final String[] lines, final int start) {
124         int found = -1;
125         for (int pos = start - 1; pos >= 0; pos -= 1) {
126             final String trimmed = lines[pos].trim();
127             if ("*/".equals(trimmed) || "**/".equals(trimmed)) {
128                 found = pos;
129                 break;
130             }
131         }
132         return found;
133     }
134 
135     private static int findFirstTag(final String[] lines, final int start,
136         final int end) {
137         int found = -1;
138         for (int pos = start; pos <= end; pos += 1) {
139             final String trimmed = lines[pos].trim();
140             if (trimmed.startsWith("* @") || trimmed.startsWith("*@")) {
141                 found = pos;
142                 break;
143             }
144         }
145         return found;
146     }
147 
148     private static int findTrimmedTextUp(final String[] lines,
149         final int start, final String text) {
150         int found = -1;
151         for (int pos = start - 1; pos >= 0; pos -= 1) {
152             if (lines[pos].trim().equals(text)) {
153                 found = pos;
154                 break;
155             }
156         }
157         return found;
158     }
159 }