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.utils;
21
22 import java.util.ArrayList;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Optional;
26 import java.util.regex.Pattern;
27
28 import javax.annotation.Nullable;
29
30 import com.puppycrawl.tools.checkstyle.api.DetailAST;
31 import com.puppycrawl.tools.checkstyle.api.DetailNode;
32 import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
33 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
34
35 /**
36 * Contains utility methods for working with Javadoc.
37 */
38 public final class JavadocUtil {
39
40 /** Maps from a token name to value. */
41 private static final Map<String, Integer> TOKEN_NAME_TO_VALUE;
42 /** Maps from a token value to name. */
43 private static final Map<Integer, String> TOKEN_VALUE_TO_NAME;
44
45 /** Exception message for unknown JavaDoc token id. */
46 private static final String UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE = "Unknown javadoc"
47 + " token id. Given id: ";
48
49 /** Newline pattern. */
50 private static final Pattern NEWLINE = Pattern.compile("\n");
51
52 /** Return pattern. */
53 private static final Pattern RETURN = Pattern.compile("\r");
54
55 /** Tab pattern. */
56 private static final Pattern TAB = Pattern.compile("\t");
57
58 // initialise the constants
59 static {
60 TOKEN_NAME_TO_VALUE =
61 TokenUtil.nameToValueMapFromPublicIntFields(JavadocCommentsTokenTypes.class);
62 TOKEN_VALUE_TO_NAME = TokenUtil.invertMap(TOKEN_NAME_TO_VALUE);
63 }
64
65 /** Prevent instantiation. */
66 private JavadocUtil() {
67 }
68
69 /**
70 * Checks that commentContent starts with '*' javadoc comment identifier.
71 *
72 * @param commentContent
73 * content of block comment
74 * @return true if commentContent starts with '*' javadoc comment
75 * identifier.
76 */
77 public static boolean isJavadocComment(String commentContent) {
78 boolean result = false;
79
80 if (!commentContent.isEmpty()) {
81 final char docCommentIdentifier = commentContent.charAt(0);
82 result = docCommentIdentifier == '*';
83 }
84
85 return result;
86 }
87
88 /**
89 * Checks block comment content starts with '*' javadoc comment identifier.
90 *
91 * @param blockCommentBegin
92 * block comment AST
93 * @return true if block comment content starts with '*' javadoc comment
94 * identifier.
95 */
96 public static boolean isJavadocComment(DetailAST blockCommentBegin) {
97 final String commentContent = getBlockCommentContent(blockCommentBegin);
98 return isJavadocComment(commentContent) && isCorrectJavadocPosition(blockCommentBegin);
99 }
100
101 /**
102 * Gets content of block comment.
103 *
104 * @param blockCommentBegin
105 * block comment AST.
106 * @return content of block comment.
107 */
108 public static String getBlockCommentContent(DetailAST blockCommentBegin) {
109 final DetailAST commentContent = blockCommentBegin.getFirstChild();
110 return commentContent.getText();
111 }
112
113 /**
114 * Get content of Javadoc comment.
115 *
116 * @param javadocCommentBegin
117 * Javadoc comment AST
118 * @return content of Javadoc comment.
119 */
120 public static String getJavadocCommentContent(DetailAST javadocCommentBegin) {
121 final DetailAST commentContent = javadocCommentBegin.getFirstChild();
122 return commentContent.getText().substring(1);
123 }
124
125 /**
126 * Returns the Javadoc block comment attached to the given declaration AST node.
127 *
128 * @param ast the declaration AST node
129 * @return the attached Javadoc block comment, or {@code null} if none is found
130 */
131 @Nullable
132 public static DetailAST getAttachedJavadocComment(final DetailAST ast) {
133 DetailAST result = null;
134 DetailAST child = ast.getFirstChild();
135 while (result == null && child.getType() != TokenTypes.IDENT) {
136 result = findJavadocComment(child);
137 child = child.getNextSibling();
138 }
139 return result;
140 }
141
142 /**
143 * Returns the Javadoc block comment attached to the given package AST node.
144 * Because of <a href="https://github.com/checkstyle/checkstyle/issues/4392">parser bug</a>
145 * parser can place javadoc comment either as previous sibling of package definition
146 * or (if there is annotation between package def and javadoc) inside package definition tree.
147 * So we should look for javadoc in both places.
148 *
149 * @param ast the package declaration AST node
150 * @return the attached Javadoc block comment, or {@code null} if none is found
151 */
152 @Nullable
153 public static DetailAST getAttachedJavadocCommentForPackage(final DetailAST ast) {
154 DetailAST result = null;
155 final DetailAST prevSibling = ast.getPreviousSibling();
156 if (prevSibling != null
157 && prevSibling.getType() == TokenTypes.BLOCK_COMMENT_BEGIN
158 && isJavadocComment(prevSibling)) {
159 result = prevSibling;
160 }
161 else {
162 final Optional<DetailAST> firstAnnotationChild =
163 Optional.ofNullable(ast.getFirstChild())
164 .map(DetailAST::getFirstChild)
165 .map(DetailAST::getFirstChild);
166 if (firstAnnotationChild.isPresent()) {
167 for (DetailAST child = firstAnnotationChild.orElseThrow(); child != null;
168 child = child.getNextSibling()) {
169 if (child.getType() == TokenTypes.BLOCK_COMMENT_BEGIN
170 && isJavadocComment(child)) {
171 result = child;
172 break;
173 }
174 }
175 }
176 }
177 return result;
178 }
179
180 /**
181 * Finds the first Javadoc block comment under the given AST node.
182 *
183 * @param ast the AST node to search
184 * @return the Javadoc block comment, or {@code null} if none is found
185 */
186 @Nullable
187 private static DetailAST findJavadocComment(DetailAST ast) {
188 DetailAST result = null;
189 if (ast.getType() == TokenTypes.BLOCK_COMMENT_BEGIN && isJavadocComment(ast)) {
190 result = ast;
191 }
192 else {
193 DetailAST child = ast.getFirstChild();
194 while (result == null && child != null) {
195 result = findJavadocComment(child);
196 child = child.getNextSibling();
197 }
198 }
199 return result;
200 }
201
202 /**
203 * Returns the first child token that has a specified type.
204 *
205 * @param detailNode
206 * Javadoc AST node
207 * @param type
208 * the token type to match
209 * @return the matching token, or null if no match
210 */
211 public static DetailNode findFirstToken(DetailNode detailNode, int type) {
212 DetailNode returnValue = null;
213 DetailNode node = detailNode.getFirstChild();
214 while (node != null) {
215 if (node.getType() == type) {
216 returnValue = node;
217 break;
218 }
219 node = node.getNextSibling();
220 }
221 return returnValue;
222 }
223
224 /**
225 * Returns all child tokens that have a specified type.
226 *
227 * @param detailNode Javadoc AST node
228 * @param type the token type to match
229 * @return the matching tokens, or an empty list if no match
230 */
231 public static List<DetailNode> getAllNodesOfType(DetailNode detailNode, int type) {
232 final List<DetailNode> nodes = new ArrayList<>();
233 DetailNode node = detailNode.getFirstChild();
234 while (node != null) {
235 if (node.getType() == type) {
236 nodes.add(node);
237 }
238 node = node.getNextSibling();
239 }
240 return nodes;
241 }
242
243 /**
244 * Checks whether the given AST node is an HTML element with the specified tag name.
245 * This method ignore void elements.
246 *
247 * @param ast the AST node to check
248 * (must be of type {@link JavadocCommentsTokenTypes#HTML_ELEMENT})
249 * @param expectedTagName the tag name to match (case-insensitive)
250 * @return {@code true} if the node has the given tag name, {@code false} otherwise
251 */
252 public static boolean isTag(DetailNode ast, String expectedTagName) {
253 final DetailNode htmlTagStart = findFirstToken(ast,
254 JavadocCommentsTokenTypes.HTML_TAG_START);
255 boolean isTag = false;
256 if (htmlTagStart != null) {
257 final String tagName = findFirstToken(htmlTagStart,
258 JavadocCommentsTokenTypes.TAG_NAME).getText();
259 isTag = expectedTagName.equalsIgnoreCase(tagName);
260 }
261 return isTag;
262 }
263
264 /**
265 * Gets next sibling of specified node with the specified type.
266 *
267 * @param node DetailNode
268 * @param tokenType javadoc token type
269 * @return next sibling.
270 */
271 public static DetailNode getNextSibling(DetailNode node, int tokenType) {
272 DetailNode nextSibling = node.getNextSibling();
273 while (nextSibling != null && nextSibling.getType() != tokenType) {
274 nextSibling = nextSibling.getNextSibling();
275 }
276 return nextSibling;
277 }
278
279 /**
280 * Returns the name of a token for a given ID.
281 *
282 * @param id
283 * the ID of the token name to get
284 * @return a token name
285 * @throws IllegalArgumentException if an unknown token ID was specified.
286 */
287 public static String getTokenName(int id) {
288 final String name = TOKEN_VALUE_TO_NAME.get(id);
289 if (name == null) {
290 throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id);
291 }
292 return name;
293 }
294
295 /**
296 * Returns the ID of a token for a given name.
297 *
298 * @param name
299 * the name of the token ID to get
300 * @return a token ID
301 * @throws IllegalArgumentException if an unknown token name was specified.
302 */
303 public static int getTokenId(String name) {
304 final Integer id = TOKEN_NAME_TO_VALUE.get(name);
305 if (id == null) {
306 throw new IllegalArgumentException("Unknown javadoc token name. Given name " + name);
307 }
308 return id;
309 }
310
311 /**
312 * Extracts the tag name from the given Javadoc tag section.
313 *
314 * @param javadocTagSection the node representing a Javadoc tag section.
315 * This node must be of type {@link JavadocCommentsTokenTypes#JAVADOC_BLOCK_TAG}
316 * or {@link JavadocCommentsTokenTypes#JAVADOC_INLINE_TAG}.
317 * @return the tag name (e.g., "param", "return", "link")
318 */
319 public static String getTagName(DetailNode javadocTagSection) {
320 return findFirstToken(javadocTagSection.getFirstChild(),
321 JavadocCommentsTokenTypes.TAG_NAME).getText();
322 }
323
324 /**
325 * Replace all control chars with escaped symbols.
326 *
327 * @param text the String to process.
328 * @return the processed String with all control chars escaped.
329 */
330 public static String escapeAllControlChars(String text) {
331 final String textWithoutNewlines = NEWLINE.matcher(text).replaceAll("\\\\n");
332 final String textWithoutReturns = RETURN.matcher(textWithoutNewlines).replaceAll("\\\\r");
333 return TAB.matcher(textWithoutReturns).replaceAll("\\\\t");
334 }
335
336 /**
337 * Checks Javadoc comment it's in right place.
338 *
339 * <p>From Javadoc util documentation:
340 * "Placement of comments - Documentation comments are recognized only when placed
341 * immediately before class, interface, constructor, method, field or annotation field
342 * declarations -- see the class example, method example, and field example.
343 * Documentation comments placed in the body of a method are ignored."</p>
344 *
345 * <p>If there are many documentation comments per declaration statement,
346 * only the last one will be recognized.</p>
347 *
348 * @param blockComment Block comment AST
349 * @return true if Javadoc is in right place
350 * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/tools/unix/javadoc.html">
351 * Javadoc util documentation</a>
352 */
353 public static boolean isCorrectJavadocPosition(DetailAST blockComment) {
354 // We must be sure that after this one there are no other documentation comments.
355 DetailAST sibling = blockComment.getNextSibling();
356 while (sibling != null) {
357 if (sibling.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
358 if (isJavadocComment(getBlockCommentContent(sibling))) {
359 // Found another javadoc comment, so this one should be ignored.
360 break;
361 }
362 sibling = sibling.getNextSibling();
363 }
364 else if (sibling.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
365 sibling = sibling.getNextSibling();
366 }
367 else {
368 // Annotation, declaration or modifier is here. Do not check further.
369 sibling = null;
370 }
371 }
372 return sibling == null
373 && (BlockCommentPosition.isOnType(blockComment)
374 || BlockCommentPosition.isOnMember(blockComment)
375 || BlockCommentPosition.isOnPackage(blockComment)
376 || BlockCommentPosition.isOnModule(blockComment));
377 }
378
379 }