001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.utils;
021
022import java.util.ArrayList;
023import java.util.List;
024import java.util.Map;
025import java.util.regex.Pattern;
026
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.DetailNode;
029import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
030import com.puppycrawl.tools.checkstyle.api.TextBlock;
031import com.puppycrawl.tools.checkstyle.api.TokenTypes;
032import com.puppycrawl.tools.checkstyle.checks.javadoc.InvalidJavadocTag;
033import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag;
034import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTagInfo;
035import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTags;
036import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.BlockTagUtil;
037import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.InlineTagUtil;
038import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.TagInfo;
039
040/**
041 * Contains utility methods for working with Javadoc.
042 */
043public final class JavadocUtil {
044
045 /**
046 * The type of Javadoc tag we want returned.
047 */
048 public enum JavadocTagType {
049
050 /** Block type. */
051 BLOCK,
052 /** Inline type. */
053 INLINE,
054 /** All validTags. */
055 ALL,
056
057 }
058
059 /** Maps from a token name to value. */
060 private static final Map<String, Integer> TOKEN_NAME_TO_VALUE;
061 /** Maps from a token value to name. */
062 private static final Map<Integer, String> TOKEN_VALUE_TO_NAME;
063
064 /** Exception message for unknown JavaDoc token id. */
065 private static final String UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE = "Unknown javadoc"
066 + " token id. Given id: ";
067
068 /** Newline pattern. */
069 private static final Pattern NEWLINE = Pattern.compile("\n");
070
071 /** Return pattern. */
072 private static final Pattern RETURN = Pattern.compile("\r");
073
074 /** Tab pattern. */
075 private static final Pattern TAB = Pattern.compile("\t");
076
077 // initialise the constants
078 static {
079 TOKEN_NAME_TO_VALUE = TokenUtil.nameToValueMapFromPublicIntFields(JavadocTokenTypes.class);
080 TOKEN_VALUE_TO_NAME = TokenUtil.invertMap(TOKEN_NAME_TO_VALUE);
081 }
082
083 /** Prevent instantiation. */
084 private JavadocUtil() {
085 }
086
087 /**
088 * Gets validTags from a given piece of Javadoc.
089 *
090 * @param textBlock
091 * the Javadoc comment to process.
092 * @param tagType
093 * the type of validTags we're interested in
094 * @return all standalone validTags from the given javadoc.
095 */
096 public static JavadocTags getJavadocTags(TextBlock textBlock,
097 JavadocTagType tagType) {
098 final List<TagInfo> tags = new ArrayList<>();
099 final boolean isBlockTags = tagType == JavadocTagType.ALL
100 || tagType == JavadocTagType.BLOCK;
101 if (isBlockTags) {
102 tags.addAll(BlockTagUtil.extractBlockTags(textBlock.getText()));
103 }
104 final boolean isInlineTags = tagType == JavadocTagType.ALL
105 || tagType == JavadocTagType.INLINE;
106 if (isInlineTags) {
107 tags.addAll(InlineTagUtil.extractInlineTags(textBlock.getText()));
108 }
109
110 final List<JavadocTag> validTags = new ArrayList<>();
111 final List<InvalidJavadocTag> invalidTags = new ArrayList<>();
112
113 for (TagInfo tag : tags) {
114 final int col = tag.getPosition().getColumn();
115
116 // Add the starting line of the comment to the line number to get the actual line number
117 // in the source.
118 // Lines are one-indexed, so need an off-by-one correction.
119 final int line = textBlock.getStartLineNo() + tag.getPosition().getLine() - 1;
120
121 if (JavadocTagInfo.isValidName(tag.getName())) {
122 validTags.add(
123 new JavadocTag(line, col, tag.getName(), tag.getValue()));
124 }
125 else {
126 invalidTags.add(new InvalidJavadocTag(line, col, tag.getName()));
127 }
128 }
129
130 return new JavadocTags(validTags, invalidTags);
131 }
132
133 /**
134 * Checks that commentContent starts with '*' javadoc comment identifier.
135 *
136 * @param commentContent
137 * content of block comment
138 * @return true if commentContent starts with '*' javadoc comment
139 * identifier.
140 */
141 public static boolean isJavadocComment(String commentContent) {
142 boolean result = false;
143
144 if (!commentContent.isEmpty()) {
145 final char docCommentIdentifier = commentContent.charAt(0);
146 result = docCommentIdentifier == '*';
147 }
148
149 return result;
150 }
151
152 /**
153 * Checks block comment content starts with '*' javadoc comment identifier.
154 *
155 * @param blockCommentBegin
156 * block comment AST
157 * @return true if block comment content starts with '*' javadoc comment
158 * identifier.
159 */
160 public static boolean isJavadocComment(DetailAST blockCommentBegin) {
161 final String commentContent = getBlockCommentContent(blockCommentBegin);
162 return isJavadocComment(commentContent) && isCorrectJavadocPosition(blockCommentBegin);
163 }
164
165 /**
166 * Gets content of block comment.
167 *
168 * @param blockCommentBegin
169 * block comment AST.
170 * @return content of block comment.
171 */
172 public static String getBlockCommentContent(DetailAST blockCommentBegin) {
173 final DetailAST commentContent = blockCommentBegin.getFirstChild();
174 return commentContent.getText();
175 }
176
177 /**
178 * Get content of Javadoc comment.
179 *
180 * @param javadocCommentBegin
181 * Javadoc comment AST
182 * @return content of Javadoc comment.
183 */
184 public static String getJavadocCommentContent(DetailAST javadocCommentBegin) {
185 final DetailAST commentContent = javadocCommentBegin.getFirstChild();
186 return commentContent.getText().substring(1);
187 }
188
189 /**
190 * Returns the first child token that has a specified type.
191 *
192 * @param detailNode
193 * Javadoc AST node
194 * @param type
195 * the token type to match
196 * @return the matching token, or null if no match
197 */
198 public static DetailNode findFirstToken(DetailNode detailNode, int type) {
199 DetailNode returnValue = null;
200 DetailNode node = getFirstChild(detailNode);
201 while (node != null) {
202 if (node.getType() == type) {
203 returnValue = node;
204 break;
205 }
206 node = getNextSibling(node);
207 }
208 return returnValue;
209 }
210
211 /**
212 * Gets first child node of specified node.
213 *
214 * @param node DetailNode
215 * @return first child
216 */
217 public static DetailNode getFirstChild(DetailNode node) {
218 DetailNode resultNode = null;
219
220 if (node.getChildren().length > 0) {
221 resultNode = node.getChildren()[0];
222 }
223 return resultNode;
224 }
225
226 /**
227 * Gets next sibling of specified node.
228 *
229 * @param node DetailNode
230 * @return next sibling.
231 */
232 public static DetailNode getNextSibling(DetailNode node) {
233 DetailNode nextSibling = null;
234 final DetailNode parent = node.getParent();
235 if (parent != null) {
236 final int nextSiblingIndex = node.getIndex() + 1;
237 final DetailNode[] children = parent.getChildren();
238 if (nextSiblingIndex <= children.length - 1) {
239 nextSibling = children[nextSiblingIndex];
240 }
241 }
242 return nextSibling;
243 }
244
245 /**
246 * Gets next sibling of specified node with the specified type.
247 *
248 * @param node DetailNode
249 * @param tokenType javadoc token type
250 * @return next sibling.
251 */
252 public static DetailNode getNextSibling(DetailNode node, int tokenType) {
253 DetailNode nextSibling = getNextSibling(node);
254 while (nextSibling != null && nextSibling.getType() != tokenType) {
255 nextSibling = getNextSibling(nextSibling);
256 }
257 return nextSibling;
258 }
259
260 /**
261 * Gets previous sibling of specified node.
262 *
263 * @param node DetailNode
264 * @return previous sibling
265 */
266 public static DetailNode getPreviousSibling(DetailNode node) {
267 DetailNode previousSibling = null;
268 final int previousSiblingIndex = node.getIndex() - 1;
269 if (previousSiblingIndex >= 0) {
270 final DetailNode parent = node.getParent();
271 final DetailNode[] children = parent.getChildren();
272 previousSibling = children[previousSiblingIndex];
273 }
274 return previousSibling;
275 }
276
277 /**
278 * Returns the name of a token for a given ID.
279 *
280 * @param id
281 * the ID of the token name to get
282 * @return a token name
283 * @throws IllegalArgumentException if an unknown token ID was specified.
284 */
285 public static String getTokenName(int id) {
286 final String name = TOKEN_VALUE_TO_NAME.get(id);
287 if (name == null) {
288 throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id);
289 }
290 return name;
291 }
292
293 /**
294 * Returns the ID of a token for a given name.
295 *
296 * @param name
297 * the name of the token ID to get
298 * @return a token ID
299 * @throws IllegalArgumentException if an unknown token name was specified.
300 */
301 public static int getTokenId(String name) {
302 final Integer id = TOKEN_NAME_TO_VALUE.get(name);
303 if (id == null) {
304 throw new IllegalArgumentException("Unknown javadoc token name. Given name " + name);
305 }
306 return id;
307 }
308
309 /**
310 * Gets tag name from javadocTagSection.
311 *
312 * @param javadocTagSection to get tag name from.
313 * @return name, of the javadocTagSection's tag.
314 */
315 public static String getTagName(DetailNode javadocTagSection) {
316 final String javadocTagName;
317 if (javadocTagSection.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
318 javadocTagName = getNextSibling(
319 getFirstChild(javadocTagSection)).getText();
320 }
321 else {
322 javadocTagName = getFirstChild(javadocTagSection).getText();
323 }
324 return javadocTagName;
325 }
326
327 /**
328 * Replace all control chars with escaped symbols.
329 *
330 * @param text the String to process.
331 * @return the processed String with all control chars escaped.
332 */
333 public static String escapeAllControlChars(String text) {
334 final String textWithoutNewlines = NEWLINE.matcher(text).replaceAll("\\\\n");
335 final String textWithoutReturns = RETURN.matcher(textWithoutNewlines).replaceAll("\\\\r");
336 return TAB.matcher(textWithoutReturns).replaceAll("\\\\t");
337 }
338
339 /**
340 * Checks Javadoc comment it's in right place.
341 *
342 * <p>From Javadoc util documentation:
343 * "Placement of comments - Documentation comments are recognized only when placed
344 * immediately before class, interface, constructor, method, field or annotation field
345 * declarations -- see the class example, method example, and field example.
346 * Documentation comments placed in the body of a method are ignored."</p>
347 *
348 * <p>If there are many documentation comments per declaration statement,
349 * only the last one will be recognized.</p>
350 *
351 * @param blockComment Block comment AST
352 * @return true if Javadoc is in right place
353 * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/tools/unix/javadoc.html">
354 * Javadoc util documentation</a>
355 */
356 public static boolean isCorrectJavadocPosition(DetailAST blockComment) {
357 // We must be sure that after this one there are no other documentation comments.
358 DetailAST sibling = blockComment.getNextSibling();
359 while (sibling != null) {
360 if (sibling.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
361 if (isJavadocComment(getBlockCommentContent(sibling))) {
362 // Found another javadoc comment, so this one should be ignored.
363 break;
364 }
365 sibling = sibling.getNextSibling();
366 }
367 else if (sibling.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
368 sibling = sibling.getNextSibling();
369 }
370 else {
371 // Annotation, declaration or modifier is here. Do not check further.
372 sibling = null;
373 }
374 }
375 return sibling == null
376 && (BlockCommentPosition.isOnType(blockComment)
377 || BlockCommentPosition.isOnMember(blockComment)
378 || BlockCommentPosition.isOnPackage(blockComment));
379 }
380
381}