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 075 * identifier. 076 */ 077 public static boolean isJavadocComment(String commentContent) { 078 boolean result = false; 079 080 if (!commentContent.isEmpty()) { 081 final char docCommentIdentifier = commentContent.charAt(0); 082 result = docCommentIdentifier == '*'; 083 } 084 085 return result; 086 } 087 088 /** 089 * Checks block comment content starts with '*' javadoc comment identifier. 090 * 091 * @param blockCommentBegin 092 * block comment AST 093 * @return true if block comment content starts with '*' javadoc comment 094 * identifier. 095 */ 096 public static boolean isJavadocComment(DetailAST blockCommentBegin) { 097 final String commentContent = getBlockCommentContent(blockCommentBegin); 098 return isJavadocComment(commentContent) && isCorrectJavadocPosition(blockCommentBegin); 099 } 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}