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