001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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.Optional; 026import java.util.regex.Pattern; 027 028import javax.annotation.Nullable; 029 030import com.puppycrawl.tools.checkstyle.api.DetailAST; 031import com.puppycrawl.tools.checkstyle.api.DetailNode; 032import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes; 033import com.puppycrawl.tools.checkstyle.api.TokenTypes; 034 035/** 036 * Contains utility methods for working with Javadoc. 037 */ 038public final class JavadocUtil { 039 040 /** Maps from a token name to value. */ 041 private static final Map<String, Integer> TOKEN_NAME_TO_VALUE; 042 /** Maps from a token value to name. */ 043 private static final Map<Integer, String> TOKEN_VALUE_TO_NAME; 044 045 /** Exception message for unknown JavaDoc token id. */ 046 private static final String UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE = "Unknown javadoc" 047 + " token id. Given id: "; 048 049 /** Newline pattern. */ 050 private static final Pattern NEWLINE = Pattern.compile("\n"); 051 052 /** Return pattern. */ 053 private static final Pattern RETURN = Pattern.compile("\r"); 054 055 /** Tab pattern. */ 056 private static final Pattern TAB = Pattern.compile("\t"); 057 058 // initialise the constants 059 static { 060 TOKEN_NAME_TO_VALUE = 061 TokenUtil.nameToValueMapFromPublicIntFields(JavadocCommentsTokenTypes.class); 062 TOKEN_VALUE_TO_NAME = TokenUtil.invertMap(TOKEN_NAME_TO_VALUE); 063 } 064 065 /** Prevent instantiation. */ 066 private JavadocUtil() { 067 } 068 069 /** 070 * Checks that commentContent starts with '*' javadoc comment identifier. 071 * 072 * @param commentContent 073 * content of block comment 074 * @return true if commentContent starts with '*' javadoc comment identifier. 075 */ 076 public static boolean isJavadocComment(String commentContent) { 077 boolean result = false; 078 079 if (!commentContent.isEmpty()) { 080 final char docCommentIdentifier = commentContent.charAt(0); 081 result = docCommentIdentifier == '*'; 082 } 083 084 return result; 085 } 086 087 /** 088 * Checks block comment content starts with '*' javadoc comment identifier. 089 * 090 * @param blockCommentBegin 091 * block comment AST 092 * @return true if block comment content starts with '*' javadoc comment identifier. 093 */ 094 public static boolean isJavadocComment(DetailAST blockCommentBegin) { 095 final String commentContent = getBlockCommentContent(blockCommentBegin); 096 return isJavadocComment(commentContent) && isCorrectJavadocPosition(blockCommentBegin); 097 } 098 099 /** 100 * Gets content of block comment. 101 * 102 * @param blockCommentBegin 103 * block comment AST. 104 * @return content of block comment. 105 */ 106 public static String getBlockCommentContent(DetailAST blockCommentBegin) { 107 final DetailAST commentContent = blockCommentBegin.getFirstChild(); 108 return commentContent.getText(); 109 } 110 111 /** 112 * Get content of Javadoc comment. 113 * 114 * @param javadocCommentBegin 115 * Javadoc comment AST 116 * @return content of Javadoc comment. 117 */ 118 public static String getJavadocCommentContent(DetailAST javadocCommentBegin) { 119 final DetailAST commentContent = javadocCommentBegin.getFirstChild(); 120 return commentContent.getText().substring(1); 121 } 122 123 /** 124 * Returns the Javadoc block comment attached to the given declaration AST node. 125 * 126 * @param ast the declaration AST node 127 * @return the attached Javadoc block comment, or {@code null} if none is found 128 */ 129 @Nullable 130 public static DetailAST getAttachedJavadocComment(final DetailAST ast) { 131 DetailAST result = null; 132 DetailAST child = ast.getFirstChild(); 133 while (result == null && child.getType() != TokenTypes.IDENT) { 134 result = findJavadocComment(child); 135 child = child.getNextSibling(); 136 } 137 return result; 138 } 139 140 /** 141 * Returns the Javadoc block comment attached to the given package AST node. 142 * Because of <a href="https://github.com/checkstyle/checkstyle/issues/4392">parser bug</a> 143 * parser can place javadoc comment either as previous sibling of package definition 144 * or (if there is annotation between package def and javadoc) inside package definition tree. 145 * So we should look for javadoc in both places. 146 * 147 * @param ast the package declaration AST node 148 * @return the attached Javadoc block comment, or {@code null} if none is found 149 */ 150 @Nullable 151 public static DetailAST getAttachedJavadocCommentForPackage(final DetailAST ast) { 152 DetailAST result = null; 153 final DetailAST prevSibling = ast.getPreviousSibling(); 154 if (prevSibling != null 155 && prevSibling.getType() == TokenTypes.BLOCK_COMMENT_BEGIN 156 && isJavadocComment(prevSibling)) { 157 result = prevSibling; 158 } 159 else { 160 final Optional<DetailAST> firstAnnotationChild = 161 Optional.ofNullable(ast.getFirstChild()) 162 .map(DetailAST::getFirstChild) 163 .map(DetailAST::getFirstChild); 164 if (firstAnnotationChild.isPresent()) { 165 for (DetailAST child = firstAnnotationChild.orElseThrow(); child != null; 166 child = child.getNextSibling()) { 167 if (child.getType() == TokenTypes.BLOCK_COMMENT_BEGIN 168 && isJavadocComment(child)) { 169 result = child; 170 break; 171 } 172 } 173 } 174 } 175 return result; 176 } 177 178 /** 179 * Finds the first Javadoc block comment under the given AST node. 180 * 181 * @param ast the AST node to search 182 * @return the Javadoc block comment, or {@code null} if none is found 183 */ 184 @Nullable 185 private static DetailAST findJavadocComment(DetailAST ast) { 186 DetailAST result = null; 187 if (ast.getType() == TokenTypes.BLOCK_COMMENT_BEGIN && isJavadocComment(ast)) { 188 result = ast; 189 } 190 else { 191 DetailAST child = ast.getFirstChild(); 192 while (result == null && child != null) { 193 result = findJavadocComment(child); 194 child = child.getNextSibling(); 195 } 196 } 197 return result; 198 } 199 200 /** 201 * Returns the first child token that has a specified type. 202 * 203 * @param detailNode 204 * Javadoc AST node 205 * @param type 206 * the token type to match 207 * @return the matching token, or null if no match 208 */ 209 public static DetailNode findFirstToken(DetailNode detailNode, int type) { 210 DetailNode returnValue = null; 211 DetailNode node = detailNode.getFirstChild(); 212 while (node != null) { 213 if (node.getType() == type) { 214 returnValue = node; 215 break; 216 } 217 node = node.getNextSibling(); 218 } 219 return returnValue; 220 } 221 222 /** 223 * Returns all child tokens that have a specified type. 224 * 225 * @param detailNode Javadoc AST node 226 * @param type the token type to match 227 * @return the matching tokens, or an empty list if no match 228 */ 229 public static List<DetailNode> getAllNodesOfType(DetailNode detailNode, int type) { 230 final List<DetailNode> nodes = new ArrayList<>(); 231 DetailNode node = detailNode.getFirstChild(); 232 while (node != null) { 233 if (node.getType() == type) { 234 nodes.add(node); 235 } 236 node = node.getNextSibling(); 237 } 238 return nodes; 239 } 240 241 /** 242 * Checks whether the given AST node is an HTML element with the specified tag name. 243 * This method ignore void elements. 244 * 245 * @param ast the AST node to check 246 * (must be of type {@link JavadocCommentsTokenTypes#HTML_ELEMENT}) 247 * @param expectedTagName the tag name to match (case-insensitive) 248 * @return {@code true} if the node has the given tag name, {@code false} otherwise 249 */ 250 public static boolean isTag(DetailNode ast, String expectedTagName) { 251 final DetailNode htmlTagStart = findFirstToken(ast, 252 JavadocCommentsTokenTypes.HTML_TAG_START); 253 boolean isTag = false; 254 if (htmlTagStart != null) { 255 final String tagName = findFirstToken(htmlTagStart, 256 JavadocCommentsTokenTypes.TAG_NAME).getText(); 257 isTag = expectedTagName.equalsIgnoreCase(tagName); 258 } 259 return isTag; 260 } 261 262 /** 263 * Gets next sibling of specified node with the specified type. 264 * 265 * @param node DetailNode 266 * @param tokenType javadoc token type 267 * @return next sibling. 268 */ 269 public static DetailNode getNextSibling(DetailNode node, int tokenType) { 270 DetailNode nextSibling = node.getNextSibling(); 271 while (nextSibling != null && nextSibling.getType() != tokenType) { 272 nextSibling = nextSibling.getNextSibling(); 273 } 274 return nextSibling; 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 * Extracts the tag name from the given Javadoc tag section. 311 * 312 * @param javadocTagSection the node representing a Javadoc tag section. 313 * This node must be of type {@link JavadocCommentsTokenTypes#JAVADOC_BLOCK_TAG} 314 * or {@link JavadocCommentsTokenTypes#JAVADOC_INLINE_TAG}. 315 * @return the tag name (e.g., "param", "return", "link") 316 */ 317 public static String getTagName(DetailNode javadocTagSection) { 318 return findFirstToken(javadocTagSection.getFirstChild(), 319 JavadocCommentsTokenTypes.TAG_NAME).getText(); 320 } 321 322 /** 323 * Replace all control chars with escaped symbols. 324 * 325 * @param text the String to process. 326 * @return the processed String with all control chars escaped. 327 */ 328 public static String escapeAllControlChars(String text) { 329 final String textWithoutNewlines = NEWLINE.matcher(text).replaceAll("\\\\n"); 330 final String textWithoutReturns = RETURN.matcher(textWithoutNewlines).replaceAll("\\\\r"); 331 return TAB.matcher(textWithoutReturns).replaceAll("\\\\t"); 332 } 333 334 /** 335 * Checks Javadoc comment it's in right place. 336 * 337 * <p>From Javadoc util documentation: 338 * "Placement of comments - Documentation comments are recognized only when placed 339 * immediately before class, interface, constructor, method, field or annotation field 340 * declarations -- see the class example, method example, and field example. 341 * Documentation comments placed in the body of a method are ignored."</p> 342 * 343 * <p>If there are many documentation comments per declaration statement, 344 * only the last one will be recognized.</p> 345 * 346 * @param blockComment Block comment AST 347 * @return true if Javadoc is in right place 348 * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/tools/unix/javadoc.html"> 349 * Javadoc util documentation</a> 350 */ 351 public static boolean isCorrectJavadocPosition(DetailAST blockComment) { 352 // We must be sure that after this one there are no other documentation comments. 353 DetailAST sibling = blockComment.getNextSibling(); 354 while (sibling != null) { 355 if (sibling.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) { 356 if (isJavadocComment(getBlockCommentContent(sibling))) { 357 // Found another javadoc comment, so this one should be ignored. 358 break; 359 } 360 sibling = sibling.getNextSibling(); 361 } 362 else if (sibling.getType() == TokenTypes.SINGLE_LINE_COMMENT) { 363 sibling = sibling.getNextSibling(); 364 } 365 else { 366 // Annotation, declaration or modifier is here. Do not check further. 367 sibling = null; 368 } 369 } 370 return sibling == null 371 && (BlockCommentPosition.isOnType(blockComment) 372 || BlockCommentPosition.isOnMember(blockComment) 373 || BlockCommentPosition.isOnPackage(blockComment) 374 || BlockCommentPosition.isOnModule(blockComment)); 375 } 376 377}