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.regex.Pattern; 026 027import javax.annotation.Nullable; 028 029import com.puppycrawl.tools.checkstyle.api.DetailAST; 030import com.puppycrawl.tools.checkstyle.api.DetailNode; 031import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes; 032import com.puppycrawl.tools.checkstyle.api.LineColumn; 033import com.puppycrawl.tools.checkstyle.api.TextBlock; 034import com.puppycrawl.tools.checkstyle.api.TokenTypes; 035import com.puppycrawl.tools.checkstyle.checks.javadoc.InvalidJavadocTag; 036import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag; 037import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTags; 038import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.BlockTagUtil; 039import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.InlineTagUtil; 040import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.TagInfo; 041 042/** 043 * Contains utility methods for working with Javadoc. 044 */ 045public final class JavadocUtil { 046 047 /** 048 * The type of Javadoc tag we want returned. 049 */ 050 public enum JavadocTagType { 051 052 /** Block type. */ 053 BLOCK, 054 /** Inline type. */ 055 INLINE, 056 /** All validTags. */ 057 ALL, 058 059 } 060 061 /** Maps from a token name to value. */ 062 private static final Map<String, Integer> TOKEN_NAME_TO_VALUE; 063 /** Maps from a token value to name. */ 064 private static final Map<Integer, String> TOKEN_VALUE_TO_NAME; 065 066 /** Exception message for unknown JavaDoc token id. */ 067 private static final String UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE = "Unknown javadoc" 068 + " token id. Given id: "; 069 070 /** Newline pattern. */ 071 private static final Pattern NEWLINE = Pattern.compile("\n"); 072 073 /** Return pattern. */ 074 private static final Pattern RETURN = Pattern.compile("\r"); 075 076 /** Tab pattern. */ 077 private static final Pattern TAB = Pattern.compile("\t"); 078 079 // initialise the constants 080 static { 081 TOKEN_NAME_TO_VALUE = 082 TokenUtil.nameToValueMapFromPublicIntFields(JavadocCommentsTokenTypes.class); 083 TOKEN_VALUE_TO_NAME = TokenUtil.invertMap(TOKEN_NAME_TO_VALUE); 084 } 085 086 /** Prevent instantiation. */ 087 private JavadocUtil() { 088 } 089 090 /** 091 * Gets validTags from a given piece of Javadoc. 092 * 093 * @param textBlock 094 * the Javadoc comment to process. 095 * @param tagType 096 * the type of validTags we're interested in 097 * @return all standalone validTags from the given javadoc. 098 */ 099 public static JavadocTags getJavadocTags(TextBlock textBlock, 100 JavadocTagType tagType) { 101 final String[] text = textBlock.getText(); 102 final List<TagInfo> tags = new ArrayList<>(); 103 final boolean isBlockTags = tagType == JavadocTagType.ALL 104 || tagType == JavadocTagType.BLOCK; 105 if (isBlockTags) { 106 tags.addAll(BlockTagUtil.extractBlockTags(text)); 107 } 108 final boolean isInlineTags = tagType == JavadocTagType.ALL 109 || tagType == JavadocTagType.INLINE; 110 if (isInlineTags) { 111 tags.addAll(InlineTagUtil.extractInlineTags(text)); 112 } 113 114 final List<JavadocTag> validTags = new ArrayList<>(); 115 final List<InvalidJavadocTag> invalidTags = new ArrayList<>(); 116 117 for (TagInfo tag : tags) { 118 final LineColumn position = tag.getPosition(); 119 final int col = position.getColumn(); 120 // Add the starting line of the comment to the line number to get the actual line number 121 // in the source. 122 // Lines are one-indexed, so need an off-by-one correction. 123 final int line = textBlock.getStartLineNo() + position.getLine() - 1; 124 125 final String tagName = tag.getName(); 126 try { 127 validTags.add(new JavadocTag(line, col, tagName, tag.getValue())); 128 } 129 catch (IllegalArgumentException ignored) { 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 Javadoc block comment attached to the given declaration AST node. 195 * 196 * @param ast the declaration AST node 197 * @return the attached Javadoc block comment, or {@code null} if none is found 198 */ 199 @Nullable 200 public static DetailAST getAttachedJavadocComment(final DetailAST ast) { 201 DetailAST result = null; 202 DetailAST child = ast.getFirstChild(); 203 while (result == null && child.getType() != TokenTypes.IDENT) { 204 result = findJavadocComment(child); 205 child = child.getNextSibling(); 206 } 207 return result; 208 } 209 210 /** 211 * Finds the first Javadoc block comment under the given AST node. 212 * 213 * @param ast the AST node to search 214 * @return the Javadoc block comment, or {@code null} if none is found 215 */ 216 @Nullable 217 private static DetailAST findJavadocComment(DetailAST ast) { 218 DetailAST result = null; 219 if (ast.getType() == TokenTypes.BLOCK_COMMENT_BEGIN && isJavadocComment(ast)) { 220 result = ast; 221 } 222 else { 223 DetailAST child = ast.getFirstChild(); 224 while (result == null && child != null) { 225 result = findJavadocComment(child); 226 child = child.getNextSibling(); 227 } 228 } 229 return result; 230 } 231 232 /** 233 * Returns the first child token that has a specified type. 234 * 235 * @param detailNode 236 * Javadoc AST node 237 * @param type 238 * the token type to match 239 * @return the matching token, or null if no match 240 */ 241 public static DetailNode findFirstToken(DetailNode detailNode, int type) { 242 DetailNode returnValue = null; 243 DetailNode node = detailNode.getFirstChild(); 244 while (node != null) { 245 if (node.getType() == type) { 246 returnValue = node; 247 break; 248 } 249 node = node.getNextSibling(); 250 } 251 return returnValue; 252 } 253 254 /** 255 * Returns all child tokens that have a specified type. 256 * 257 * @param detailNode Javadoc AST node 258 * @param type the token type to match 259 * @return the matching tokens, or an empty list if no match 260 */ 261 public static List<DetailNode> getAllNodesOfType(DetailNode detailNode, int type) { 262 final List<DetailNode> nodes = new ArrayList<>(); 263 DetailNode node = detailNode.getFirstChild(); 264 while (node != null) { 265 if (node.getType() == type) { 266 nodes.add(node); 267 } 268 node = node.getNextSibling(); 269 } 270 return nodes; 271 } 272 273 /** 274 * Checks whether the given AST node is an HTML element with the specified tag name. 275 * This method ignore void elements. 276 * 277 * @param ast the AST node to check 278 * (must be of type {@link JavadocCommentsTokenTypes#HTML_ELEMENT}) 279 * @param expectedTagName the tag name to match (case-insensitive) 280 * @return {@code true} if the node has the given tag name, {@code false} otherwise 281 */ 282 public static boolean isTag(DetailNode ast, String expectedTagName) { 283 final DetailNode htmlTagStart = findFirstToken(ast, 284 JavadocCommentsTokenTypes.HTML_TAG_START); 285 boolean isTag = false; 286 if (htmlTagStart != null) { 287 final String tagName = findFirstToken(htmlTagStart, 288 JavadocCommentsTokenTypes.TAG_NAME).getText(); 289 isTag = expectedTagName.equalsIgnoreCase(tagName); 290 } 291 return isTag; 292 } 293 294 /** 295 * Gets next sibling of specified node with the specified type. 296 * 297 * @param node DetailNode 298 * @param tokenType javadoc token type 299 * @return next sibling. 300 */ 301 public static DetailNode getNextSibling(DetailNode node, int tokenType) { 302 DetailNode nextSibling = node.getNextSibling(); 303 while (nextSibling != null && nextSibling.getType() != tokenType) { 304 nextSibling = nextSibling.getNextSibling(); 305 } 306 return nextSibling; 307 } 308 309 /** 310 * Returns the name of a token for a given ID. 311 * 312 * @param id 313 * the ID of the token name to get 314 * @return a token name 315 * @throws IllegalArgumentException if an unknown token ID was specified. 316 */ 317 public static String getTokenName(int id) { 318 final String name = TOKEN_VALUE_TO_NAME.get(id); 319 if (name == null) { 320 throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id); 321 } 322 return name; 323 } 324 325 /** 326 * Returns the ID of a token for a given name. 327 * 328 * @param name 329 * the name of the token ID to get 330 * @return a token ID 331 * @throws IllegalArgumentException if an unknown token name was specified. 332 */ 333 public static int getTokenId(String name) { 334 final Integer id = TOKEN_NAME_TO_VALUE.get(name); 335 if (id == null) { 336 throw new IllegalArgumentException("Unknown javadoc token name. Given name " + name); 337 } 338 return id; 339 } 340 341 /** 342 * Extracts the tag name from the given Javadoc tag section. 343 * 344 * @param javadocTagSection the node representing a Javadoc tag section. 345 * This node must be of type {@link JavadocCommentsTokenTypes#JAVADOC_BLOCK_TAG} 346 * or {@link JavadocCommentsTokenTypes#JAVADOC_INLINE_TAG}. 347 * @return the tag name (e.g., "param", "return", "link") 348 */ 349 public static String getTagName(DetailNode javadocTagSection) { 350 return findFirstToken(javadocTagSection.getFirstChild(), 351 JavadocCommentsTokenTypes.TAG_NAME).getText(); 352 } 353 354 /** 355 * Replace all control chars with escaped symbols. 356 * 357 * @param text the String to process. 358 * @return the processed String with all control chars escaped. 359 */ 360 public static String escapeAllControlChars(String text) { 361 final String textWithoutNewlines = NEWLINE.matcher(text).replaceAll("\\\\n"); 362 final String textWithoutReturns = RETURN.matcher(textWithoutNewlines).replaceAll("\\\\r"); 363 return TAB.matcher(textWithoutReturns).replaceAll("\\\\t"); 364 } 365 366 /** 367 * Checks Javadoc comment it's in right place. 368 * 369 * <p>From Javadoc util documentation: 370 * "Placement of comments - Documentation comments are recognized only when placed 371 * immediately before class, interface, constructor, method, field or annotation field 372 * declarations -- see the class example, method example, and field example. 373 * Documentation comments placed in the body of a method are ignored."</p> 374 * 375 * <p>If there are many documentation comments per declaration statement, 376 * only the last one will be recognized.</p> 377 * 378 * @param blockComment Block comment AST 379 * @return true if Javadoc is in right place 380 * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/tools/unix/javadoc.html"> 381 * Javadoc util documentation</a> 382 */ 383 public static boolean isCorrectJavadocPosition(DetailAST blockComment) { 384 // We must be sure that after this one there are no other documentation comments. 385 DetailAST sibling = blockComment.getNextSibling(); 386 while (sibling != null) { 387 if (sibling.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) { 388 if (isJavadocComment(getBlockCommentContent(sibling))) { 389 // Found another javadoc comment, so this one should be ignored. 390 break; 391 } 392 sibling = sibling.getNextSibling(); 393 } 394 else if (sibling.getType() == TokenTypes.SINGLE_LINE_COMMENT) { 395 sibling = sibling.getNextSibling(); 396 } 397 else { 398 // Annotation, declaration or modifier is here. Do not check further. 399 sibling = null; 400 } 401 } 402 return sibling == null 403 && (BlockCommentPosition.isOnType(blockComment) 404 || BlockCommentPosition.isOnMember(blockComment) 405 || BlockCommentPosition.isOnPackage(blockComment)); 406 } 407 408}