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