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