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