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 * /**
26 * * Just one line here.
27 * <span style="color:red" >*</span>
28 * * @since 0.1
29 * */
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 * /**
36 * * First line.
37 * *
38 * * Second par.
39 * <span style="color:red" >* @since 0.1</span>
40 * */
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"
118 );
119 } else if (!multi && empty) {
120 this.log(
121 tag,
122 "Empty Javadoc line before at-clauses is not allowed"
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 }