001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2018 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.lang.reflect.Field; 023import java.lang.reflect.Modifier; 024import java.util.ArrayList; 025import java.util.List; 026import java.util.regex.Pattern; 027 028import com.google.common.collect.ImmutableMap; 029import com.puppycrawl.tools.checkstyle.api.DetailAST; 030import com.puppycrawl.tools.checkstyle.api.DetailNode; 031import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes; 032import com.puppycrawl.tools.checkstyle.api.TextBlock; 033import com.puppycrawl.tools.checkstyle.api.TokenTypes; 034import com.puppycrawl.tools.checkstyle.checks.javadoc.InvalidJavadocTag; 035import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTag; 036import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTagInfo; 037import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTags; 038import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.BlockTagUtils; 039import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.InlineTagUtils; 040import com.puppycrawl.tools.checkstyle.checks.javadoc.utils.TagInfo; 041 042/** 043 * Contains utility methods for working with Javadoc. 044 * @author Lyle Hanson 045 */ 046public final class JavadocUtils { 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 ImmutableMap<String, Integer> TOKEN_NAME_TO_VALUE; 064 /** Maps from a token value to name. */ 065 private static final 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 // Using reflection gets all token names and values from JavadocTokenTypes class 081 // and saves to TOKEN_NAME_TO_VALUE and TOKEN_VALUE_TO_NAME collections. 082 static { 083 final ImmutableMap.Builder<String, Integer> builder = ImmutableMap.builder(); 084 085 final Field[] fields = JavadocTokenTypes.class.getDeclaredFields(); 086 087 String[] tempTokenValueToName = CommonUtils.EMPTY_STRING_ARRAY; 088 089 for (final Field field : fields) { 090 // Only process public int fields. 091 if (!Modifier.isPublic(field.getModifiers()) 092 || field.getType() != Integer.TYPE) { 093 continue; 094 } 095 096 final String name = field.getName(); 097 098 final int tokenValue = TokenUtils.getIntFromField(field, name); 099 builder.put(name, tokenValue); 100 if (tokenValue > tempTokenValueToName.length - 1) { 101 final String[] temp = new String[tokenValue + 1]; 102 System.arraycopy(tempTokenValueToName, 0, temp, 0, tempTokenValueToName.length); 103 tempTokenValueToName = temp; 104 } 105 if (tokenValue == -1) { 106 tempTokenValueToName[0] = name; 107 } 108 else { 109 tempTokenValueToName[tokenValue] = name; 110 } 111 } 112 113 TOKEN_NAME_TO_VALUE = builder.build(); 114 TOKEN_VALUE_TO_NAME = tempTokenValueToName; 115 } 116 117 /** Prevent instantiation. */ 118 private JavadocUtils() { 119 } 120 121 /** 122 * Gets validTags from a given piece of Javadoc. 123 * @param textBlock 124 * the Javadoc comment to process. 125 * @param tagType 126 * the type of validTags we're interested in 127 * @return all standalone validTags from the given javadoc. 128 */ 129 public static JavadocTags getJavadocTags(TextBlock textBlock, 130 JavadocTagType tagType) { 131 final boolean getBlockTags = tagType == JavadocTagType.ALL 132 || tagType == JavadocTagType.BLOCK; 133 final boolean getInlineTags = tagType == JavadocTagType.ALL 134 || tagType == JavadocTagType.INLINE; 135 136 final List<TagInfo> tags = new ArrayList<>(); 137 138 if (getBlockTags) { 139 tags.addAll(BlockTagUtils.extractBlockTags(textBlock.getText())); 140 } 141 142 if (getInlineTags) { 143 tags.addAll(InlineTagUtils.extractInlineTags(textBlock.getText())); 144 } 145 146 final List<JavadocTag> validTags = new ArrayList<>(); 147 final List<InvalidJavadocTag> invalidTags = new ArrayList<>(); 148 149 for (TagInfo tag : tags) { 150 final int col = tag.getPosition().getColumn(); 151 152 // Add the starting line of the comment to the line number to get the actual line number 153 // in the source. 154 // Lines are one-indexed, so need a off-by-one correction. 155 final int line = textBlock.getStartLineNo() + tag.getPosition().getLine() - 1; 156 157 if (JavadocTagInfo.isValidName(tag.getName())) { 158 validTags.add( 159 new JavadocTag(line, col, tag.getName(), tag.getValue())); 160 } 161 else { 162 invalidTags.add(new InvalidJavadocTag(line, col, tag.getName())); 163 } 164 } 165 166 return new JavadocTags(validTags, invalidTags); 167 } 168 169 /** 170 * Checks that commentContent starts with '*' javadoc comment identifier. 171 * @param commentContent 172 * content of block comment 173 * @return true if commentContent starts with '*' javadoc comment 174 * identifier. 175 */ 176 public static boolean isJavadocComment(String commentContent) { 177 boolean result = false; 178 179 if (!commentContent.isEmpty()) { 180 final char docCommentIdentifier = commentContent.charAt(0); 181 result = docCommentIdentifier == '*'; 182 } 183 184 return result; 185 } 186 187 /** 188 * Checks block comment content starts with '*' javadoc comment identifier. 189 * @param blockCommentBegin 190 * block comment AST 191 * @return true if block comment content starts with '*' javadoc comment 192 * identifier. 193 */ 194 public static boolean isJavadocComment(DetailAST blockCommentBegin) { 195 final String commentContent = getBlockCommentContent(blockCommentBegin); 196 return isJavadocComment(commentContent) && isCorrectJavadocPosition(blockCommentBegin); 197 } 198 199 /** 200 * Gets content of block comment. 201 * @param blockCommentBegin 202 * block comment AST. 203 * @return content of block comment. 204 */ 205 private static String getBlockCommentContent(DetailAST blockCommentBegin) { 206 final DetailAST commentContent = blockCommentBegin.getFirstChild(); 207 return commentContent.getText(); 208 } 209 210 /** 211 * Get content of Javadoc comment. 212 * @param javadocCommentBegin 213 * Javadoc comment AST 214 * @return content of Javadoc comment. 215 */ 216 public static String getJavadocCommentContent(DetailAST javadocCommentBegin) { 217 final DetailAST commentContent = javadocCommentBegin.getFirstChild(); 218 return commentContent.getText().substring(1); 219 } 220 221 /** 222 * Returns the first child token that has a specified type. 223 * @param detailNode 224 * Javadoc AST node 225 * @param type 226 * the token type to match 227 * @return the matching token, or null if no match 228 */ 229 public static DetailNode findFirstToken(DetailNode detailNode, int type) { 230 DetailNode returnValue = null; 231 DetailNode node = getFirstChild(detailNode); 232 while (node != null) { 233 if (node.getType() == type) { 234 returnValue = node; 235 break; 236 } 237 node = getNextSibling(node); 238 } 239 return returnValue; 240 } 241 242 /** 243 * Gets first child node of specified node. 244 * 245 * @param node DetailNode 246 * @return first child 247 */ 248 public static DetailNode getFirstChild(DetailNode node) { 249 DetailNode resultNode = null; 250 251 if (node.getChildren().length > 0) { 252 resultNode = node.getChildren()[0]; 253 } 254 return resultNode; 255 } 256 257 /** 258 * Checks whether node contains any node of specified type among children on any deep level. 259 * 260 * @param node DetailNode 261 * @param type token type 262 * @return true if node contains any node of type type among children on any deep level. 263 */ 264 public static boolean containsInBranch(DetailNode node, int type) { 265 boolean result = true; 266 DetailNode curNode = node; 267 while (type != curNode.getType()) { 268 DetailNode toVisit = getFirstChild(curNode); 269 while (curNode != null && toVisit == null) { 270 toVisit = getNextSibling(curNode); 271 if (toVisit == null) { 272 curNode = curNode.getParent(); 273 } 274 } 275 276 if (curNode == toVisit) { 277 result = false; 278 break; 279 } 280 281 curNode = toVisit; 282 } 283 return result; 284 } 285 286 /** 287 * Gets next sibling of specified node. 288 * 289 * @param node DetailNode 290 * @return next sibling. 291 */ 292 public static DetailNode getNextSibling(DetailNode node) { 293 DetailNode nextSibling = null; 294 final DetailNode parent = node.getParent(); 295 if (parent != null) { 296 final int nextSiblingIndex = node.getIndex() + 1; 297 final DetailNode[] children = parent.getChildren(); 298 if (nextSiblingIndex <= children.length - 1) { 299 nextSibling = children[nextSiblingIndex]; 300 } 301 } 302 return nextSibling; 303 } 304 305 /** 306 * Gets next sibling of specified node with the specified type. 307 * 308 * @param node DetailNode 309 * @param tokenType javadoc token type 310 * @return next sibling. 311 */ 312 public static DetailNode getNextSibling(DetailNode node, int tokenType) { 313 DetailNode nextSibling = getNextSibling(node); 314 while (nextSibling != null && nextSibling.getType() != tokenType) { 315 nextSibling = getNextSibling(nextSibling); 316 } 317 return nextSibling; 318 } 319 320 /** 321 * Gets previous sibling of specified node. 322 * @param node DetailNode 323 * @return previous sibling 324 */ 325 public static DetailNode getPreviousSibling(DetailNode node) { 326 DetailNode previousSibling = null; 327 final int previousSiblingIndex = node.getIndex() - 1; 328 if (previousSiblingIndex >= 0) { 329 final DetailNode parent = node.getParent(); 330 final DetailNode[] children = parent.getChildren(); 331 previousSibling = children[previousSiblingIndex]; 332 } 333 return previousSibling; 334 } 335 336 /** 337 * Returns the name of a token for a given ID. 338 * @param id 339 * the ID of the token name to get 340 * @return a token name 341 */ 342 public static String getTokenName(int id) { 343 final String name; 344 if (id == JavadocTokenTypes.EOF) { 345 name = "EOF"; 346 } 347 else if (id > TOKEN_VALUE_TO_NAME.length - 1) { 348 throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id); 349 } 350 else { 351 name = TOKEN_VALUE_TO_NAME[id]; 352 if (name == null) { 353 throw new IllegalArgumentException(UNKNOWN_JAVADOC_TOKEN_ID_EXCEPTION_MESSAGE + id); 354 } 355 } 356 return name; 357 } 358 359 /** 360 * Returns the ID of a token for a given name. 361 * @param name 362 * the name of the token ID to get 363 * @return a token ID 364 */ 365 public static int getTokenId(String name) { 366 final Integer id = TOKEN_NAME_TO_VALUE.get(name); 367 if (id == null) { 368 throw new IllegalArgumentException("Unknown javadoc token name. Given name " + name); 369 } 370 return id; 371 } 372 373 /** 374 * Gets tag name from javadocTagSection. 375 * 376 * @param javadocTagSection to get tag name from. 377 * @return name, of the javadocTagSection's tag. 378 */ 379 public static String getTagName(DetailNode javadocTagSection) { 380 final String javadocTagName; 381 if (javadocTagSection.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) { 382 javadocTagName = getNextSibling( 383 getFirstChild(javadocTagSection)).getText(); 384 } 385 else { 386 javadocTagName = getFirstChild(javadocTagSection).getText(); 387 } 388 return javadocTagName; 389 } 390 391 /** 392 * Replace all control chars with escaped symbols. 393 * @param text the String to process. 394 * @return the processed String with all control chars escaped. 395 */ 396 public static String escapeAllControlChars(String text) { 397 final String textWithoutNewlines = NEWLINE.matcher(text).replaceAll("\\\\n"); 398 final String textWithoutReturns = RETURN.matcher(textWithoutNewlines).replaceAll("\\\\r"); 399 return TAB.matcher(textWithoutReturns).replaceAll("\\\\t"); 400 } 401 402 /** 403 * Checks Javadoc comment it's in right place. 404 * <p>From Javadoc util documentation: 405 * "Placement of comments - Documentation comments are recognized only when placed 406 * immediately before class, interface, constructor, method, field or annotation field 407 * declarations -- see the class example, method example, and field example. 408 * Documentation comments placed in the body of a method are ignored."</p> 409 * <p>If there are many documentation comments per declaration statement, 410 * only the last one will be recognized.</p> 411 * 412 * @param blockComment Block comment AST 413 * @return true if Javadoc is in right place 414 * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/tools/unix/javadoc.html"> 415 * Javadoc util documentation</a> 416 */ 417 private static boolean isCorrectJavadocPosition(DetailAST blockComment) { 418 // We must be sure that after this one there are no other documentation comments. 419 DetailAST sibling = blockComment.getNextSibling(); 420 while (sibling != null) { 421 if (sibling.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) { 422 if (isJavadocComment(getBlockCommentContent(sibling))) { 423 // Found another javadoc comment, so this one should be ignored. 424 break; 425 } 426 sibling = sibling.getNextSibling(); 427 } 428 else if (sibling.getType() == TokenTypes.SINGLE_LINE_COMMENT) { 429 sibling = sibling.getNextSibling(); 430 } 431 else { 432 // Annotation, declaration or modifier is here. Do not check further. 433 sibling = null; 434 } 435 } 436 return sibling == null 437 && (BlockCommentPosition.isOnType(blockComment) 438 || BlockCommentPosition.isOnMember(blockComment)); 439 } 440 441}