001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2022 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.Arrays; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Set; 027import java.util.regex.Pattern; 028import java.util.stream.Collectors; 029 030import com.puppycrawl.tools.checkstyle.api.DetailAST; 031import com.puppycrawl.tools.checkstyle.api.TokenTypes; 032import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption; 033 034/** 035 * Contains utility methods for the checks. 036 * 037 */ 038public final class CheckUtil { 039 040 // constants for parseDouble() 041 /** Binary radix. */ 042 private static final int BASE_2 = 2; 043 044 /** Octal radix. */ 045 private static final int BASE_8 = 8; 046 047 /** Decimal radix. */ 048 private static final int BASE_10 = 10; 049 050 /** Hex radix. */ 051 private static final int BASE_16 = 16; 052 053 /** Maximum children allowed in setter/getter. */ 054 private static final int SETTER_GETTER_MAX_CHILDREN = 7; 055 056 /** Maximum nodes allowed in a body of setter. */ 057 private static final int SETTER_BODY_SIZE = 3; 058 059 /** Maximum nodes allowed in a body of getter. */ 060 private static final int GETTER_BODY_SIZE = 2; 061 062 /** Pattern matching underscore characters ('_'). */ 063 private static final Pattern UNDERSCORE_PATTERN = Pattern.compile("_"); 064 065 /** Pattern matching names of setter methods. */ 066 private static final Pattern SETTER_PATTERN = Pattern.compile("^set[A-Z].*"); 067 068 /** Pattern matching names of getter methods. */ 069 private static final Pattern GETTER_PATTERN = Pattern.compile("^(is|get)[A-Z].*"); 070 071 /** Compiled pattern for all system newlines. */ 072 private static final Pattern ALL_NEW_LINES = Pattern.compile("\\R"); 073 074 /** Prevent instances. */ 075 private CheckUtil() { 076 } 077 078 /** 079 * Tests whether a method definition AST defines an equals covariant. 080 * 081 * @param ast the method definition AST to test. 082 * Precondition: ast is a TokenTypes.METHOD_DEF node. 083 * @return true if ast defines an equals covariant. 084 */ 085 public static boolean isEqualsMethod(DetailAST ast) { 086 boolean equalsMethod = false; 087 088 if (ast.getType() == TokenTypes.METHOD_DEF) { 089 final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS); 090 final boolean staticOrAbstract = 091 modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null 092 || modifiers.findFirstToken(TokenTypes.ABSTRACT) != null; 093 094 if (!staticOrAbstract) { 095 final DetailAST nameNode = ast.findFirstToken(TokenTypes.IDENT); 096 final String name = nameNode.getText(); 097 098 if ("equals".equals(name)) { 099 // one parameter? 100 final DetailAST paramsNode = ast.findFirstToken(TokenTypes.PARAMETERS); 101 equalsMethod = paramsNode.getChildCount() == 1; 102 } 103 } 104 } 105 return equalsMethod; 106 } 107 108 /** 109 * Returns whether a token represents an ELSE as part of an ELSE / IF set. 110 * 111 * @param ast the token to check 112 * @return whether it is 113 */ 114 public static boolean isElseIf(DetailAST ast) { 115 final DetailAST parentAST = ast.getParent(); 116 117 return ast.getType() == TokenTypes.LITERAL_IF 118 && (isElse(parentAST) || isElseWithCurlyBraces(parentAST)); 119 } 120 121 /** 122 * Returns whether a token represents an ELSE. 123 * 124 * @param ast the token to check 125 * @return whether the token represents an ELSE 126 */ 127 private static boolean isElse(DetailAST ast) { 128 return ast.getType() == TokenTypes.LITERAL_ELSE; 129 } 130 131 /** 132 * Returns whether a token represents an SLIST as part of an ELSE 133 * statement. 134 * 135 * @param ast the token to check 136 * @return whether the toke does represent an SLIST as part of an ELSE 137 */ 138 private static boolean isElseWithCurlyBraces(DetailAST ast) { 139 return ast.getType() == TokenTypes.SLIST 140 && ast.getChildCount() == 2 141 && isElse(ast.getParent()); 142 } 143 144 /** 145 * Returns the value represented by the specified string of the specified 146 * type. Returns 0 for types other than float, double, int, and long. 147 * 148 * @param text the string to be parsed. 149 * @param type the token type of the text. Should be a constant of 150 * {@link TokenTypes}. 151 * @return the double value represented by the string argument. 152 */ 153 public static double parseDouble(String text, int type) { 154 String txt = UNDERSCORE_PATTERN.matcher(text).replaceAll(""); 155 final double result; 156 switch (type) { 157 case TokenTypes.NUM_FLOAT: 158 case TokenTypes.NUM_DOUBLE: 159 result = Double.parseDouble(txt); 160 break; 161 case TokenTypes.NUM_INT: 162 case TokenTypes.NUM_LONG: 163 int radix = BASE_10; 164 if (txt.startsWith("0x") || txt.startsWith("0X")) { 165 radix = BASE_16; 166 txt = txt.substring(2); 167 } 168 else if (txt.startsWith("0b") || txt.startsWith("0B")) { 169 radix = BASE_2; 170 txt = txt.substring(2); 171 } 172 else if (CommonUtil.startsWithChar(txt, '0')) { 173 radix = BASE_8; 174 txt = txt.substring(1); 175 } 176 result = parseNumber(txt, radix, type); 177 break; 178 default: 179 result = Double.NaN; 180 break; 181 } 182 return result; 183 } 184 185 /** 186 * Parses the string argument as an integer or a long in the radix specified by 187 * the second argument. The characters in the string must all be digits of 188 * the specified radix. 189 * 190 * @param text the String containing the integer representation to be 191 * parsed. Precondition: text contains a parsable int. 192 * @param radix the radix to be used while parsing text. 193 * @param type the token type of the text. Should be a constant of 194 * {@link TokenTypes}. 195 * @return the number represented by the string argument in the specified radix. 196 */ 197 private static double parseNumber(final String text, final int radix, final int type) { 198 String txt = text; 199 if (CommonUtil.endsWithChar(txt, 'L') || CommonUtil.endsWithChar(txt, 'l')) { 200 txt = txt.substring(0, txt.length() - 1); 201 } 202 final double result; 203 if (txt.isEmpty()) { 204 result = 0.0; 205 } 206 else { 207 final boolean negative = txt.charAt(0) == '-'; 208 if (type == TokenTypes.NUM_INT) { 209 if (negative) { 210 result = Integer.parseInt(txt, radix); 211 } 212 else { 213 result = Integer.parseUnsignedInt(txt, radix); 214 } 215 } 216 else { 217 if (negative) { 218 result = Long.parseLong(txt, radix); 219 } 220 else { 221 result = Long.parseUnsignedLong(txt, radix); 222 } 223 } 224 } 225 return result; 226 } 227 228 /** 229 * Finds sub-node for given node minimal (line, column) pair. 230 * 231 * @param node the root of tree for search. 232 * @return sub-node with minimal (line, column) pair. 233 */ 234 public static DetailAST getFirstNode(final DetailAST node) { 235 DetailAST currentNode = node; 236 DetailAST child = node.getFirstChild(); 237 while (child != null) { 238 final DetailAST newNode = getFirstNode(child); 239 if (isBeforeInSource(newNode, currentNode)) { 240 currentNode = newNode; 241 } 242 child = child.getNextSibling(); 243 } 244 245 return currentNode; 246 } 247 248 /** 249 * Retrieves whether ast1 is located before ast2. 250 * 251 * @param ast1 the first node. 252 * @param ast2 the second node. 253 * @return true, if ast1 is located before ast2. 254 */ 255 public static boolean isBeforeInSource(DetailAST ast1, DetailAST ast2) { 256 return ast1.getLineNo() < ast2.getLineNo() 257 || TokenUtil.areOnSameLine(ast1, ast2) 258 && ast1.getColumnNo() < ast2.getColumnNo(); 259 } 260 261 /** 262 * Retrieves the names of the type parameters to the node. 263 * 264 * @param node the parameterized AST node 265 * @return a list of type parameter names 266 */ 267 public static List<String> getTypeParameterNames(final DetailAST node) { 268 final DetailAST typeParameters = 269 node.findFirstToken(TokenTypes.TYPE_PARAMETERS); 270 271 final List<String> typeParameterNames = new ArrayList<>(); 272 if (typeParameters != null) { 273 final DetailAST typeParam = 274 typeParameters.findFirstToken(TokenTypes.TYPE_PARAMETER); 275 typeParameterNames.add( 276 typeParam.findFirstToken(TokenTypes.IDENT).getText()); 277 278 DetailAST sibling = typeParam.getNextSibling(); 279 while (sibling != null) { 280 if (sibling.getType() == TokenTypes.TYPE_PARAMETER) { 281 typeParameterNames.add( 282 sibling.findFirstToken(TokenTypes.IDENT).getText()); 283 } 284 sibling = sibling.getNextSibling(); 285 } 286 } 287 288 return typeParameterNames; 289 } 290 291 /** 292 * Retrieves the type parameters to the node. 293 * 294 * @param node the parameterized AST node 295 * @return a list of type parameter names 296 */ 297 public static List<DetailAST> getTypeParameters(final DetailAST node) { 298 final DetailAST typeParameters = 299 node.findFirstToken(TokenTypes.TYPE_PARAMETERS); 300 301 final List<DetailAST> typeParams = new ArrayList<>(); 302 if (typeParameters != null) { 303 final DetailAST typeParam = 304 typeParameters.findFirstToken(TokenTypes.TYPE_PARAMETER); 305 typeParams.add(typeParam); 306 307 DetailAST sibling = typeParam.getNextSibling(); 308 while (sibling != null) { 309 if (sibling.getType() == TokenTypes.TYPE_PARAMETER) { 310 typeParams.add(sibling); 311 } 312 sibling = sibling.getNextSibling(); 313 } 314 } 315 316 return typeParams; 317 } 318 319 /** 320 * Returns whether an AST represents a setter method. 321 * 322 * @param ast the AST to check with 323 * @return whether the AST represents a setter method 324 */ 325 public static boolean isSetterMethod(final DetailAST ast) { 326 boolean setterMethod = false; 327 328 // Check have a method with exactly 7 children which are all that 329 // is allowed in a proper setter method which does not throw any 330 // exceptions. 331 if (ast.getType() == TokenTypes.METHOD_DEF 332 && ast.getChildCount() == SETTER_GETTER_MAX_CHILDREN) { 333 final DetailAST type = ast.findFirstToken(TokenTypes.TYPE); 334 final String name = type.getNextSibling().getText(); 335 final boolean matchesSetterFormat = SETTER_PATTERN.matcher(name).matches(); 336 final boolean voidReturnType = type.findFirstToken(TokenTypes.LITERAL_VOID) != null; 337 338 final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS); 339 final boolean singleParam = params.getChildCount(TokenTypes.PARAMETER_DEF) == 1; 340 341 if (matchesSetterFormat && voidReturnType && singleParam) { 342 // Now verify that the body consists of: 343 // SLIST -> EXPR -> ASSIGN 344 // SEMI 345 // RCURLY 346 final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST); 347 348 if (slist != null && slist.getChildCount() == SETTER_BODY_SIZE) { 349 final DetailAST expr = slist.getFirstChild(); 350 setterMethod = expr.getFirstChild().getType() == TokenTypes.ASSIGN; 351 } 352 } 353 } 354 return setterMethod; 355 } 356 357 /** 358 * Returns whether an AST represents a getter method. 359 * 360 * @param ast the AST to check with 361 * @return whether the AST represents a getter method 362 */ 363 public static boolean isGetterMethod(final DetailAST ast) { 364 boolean getterMethod = false; 365 366 // Check have a method with exactly 7 children which are all that 367 // is allowed in a proper getter method which does not throw any 368 // exceptions. 369 if (ast.getType() == TokenTypes.METHOD_DEF 370 && ast.getChildCount() == SETTER_GETTER_MAX_CHILDREN) { 371 final DetailAST type = ast.findFirstToken(TokenTypes.TYPE); 372 final String name = type.getNextSibling().getText(); 373 final boolean matchesGetterFormat = GETTER_PATTERN.matcher(name).matches(); 374 final boolean noVoidReturnType = type.findFirstToken(TokenTypes.LITERAL_VOID) == null; 375 376 final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS); 377 final boolean noParams = params.getChildCount(TokenTypes.PARAMETER_DEF) == 0; 378 379 if (matchesGetterFormat && noVoidReturnType && noParams) { 380 // Now verify that the body consists of: 381 // SLIST -> RETURN 382 // RCURLY 383 final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST); 384 385 if (slist != null && slist.getChildCount() == GETTER_BODY_SIZE) { 386 final DetailAST expr = slist.getFirstChild(); 387 getterMethod = expr.getType() == TokenTypes.LITERAL_RETURN; 388 } 389 } 390 } 391 return getterMethod; 392 } 393 394 /** 395 * Checks whether a method is a not void one. 396 * 397 * @param methodDefAst the method node. 398 * @return true if method is a not void one. 399 */ 400 public static boolean isNonVoidMethod(DetailAST methodDefAst) { 401 boolean returnValue = false; 402 if (methodDefAst.getType() == TokenTypes.METHOD_DEF) { 403 final DetailAST typeAST = methodDefAst.findFirstToken(TokenTypes.TYPE); 404 if (typeAST.findFirstToken(TokenTypes.LITERAL_VOID) == null) { 405 returnValue = true; 406 } 407 } 408 return returnValue; 409 } 410 411 /** 412 * Checks whether a parameter is a receiver. 413 * 414 * @param parameterDefAst the parameter node. 415 * @return true if the parameter is a receiver. 416 */ 417 public static boolean isReceiverParameter(DetailAST parameterDefAst) { 418 return parameterDefAst.getType() == TokenTypes.PARAMETER_DEF 419 && parameterDefAst.findFirstToken(TokenTypes.IDENT) == null; 420 } 421 422 /** 423 * Returns the access modifier of the method/constructor at the specified AST. If 424 * the method is in an interface or annotation block, the access modifier is assumed 425 * to be public. 426 * 427 * @param ast the token of the method/constructor. 428 * @return the access modifier of the method/constructor. 429 */ 430 public static AccessModifierOption getAccessModifierFromModifiersToken(DetailAST ast) { 431 final DetailAST modsToken = ast.findFirstToken(TokenTypes.MODIFIERS); 432 AccessModifierOption accessModifier = 433 getAccessModifierFromModifiersTokenDirectly(modsToken); 434 435 if (accessModifier == AccessModifierOption.PACKAGE) { 436 if (ScopeUtil.isInEnumBlock(ast) && ast.getType() == TokenTypes.CTOR_DEF) { 437 accessModifier = AccessModifierOption.PRIVATE; 438 } 439 else if (ScopeUtil.isInInterfaceOrAnnotationBlock(ast)) { 440 accessModifier = AccessModifierOption.PUBLIC; 441 } 442 } 443 444 return accessModifier; 445 } 446 447 /** 448 * Returns {@link AccessModifierOption} based on the information about access modifier 449 * taken from the given token of type {@link TokenTypes#MODIFIERS}. 450 * 451 * @param modifiersToken token of type {@link TokenTypes#MODIFIERS}. 452 * @return {@link AccessModifierOption}. 453 * @throws IllegalArgumentException when expected non-null modifiersToken with type 'MODIFIERS' 454 */ 455 private static AccessModifierOption getAccessModifierFromModifiersTokenDirectly( 456 DetailAST modifiersToken) { 457 if (modifiersToken == null) { 458 throw new IllegalArgumentException("expected non-null AST-token with type 'MODIFIERS'"); 459 } 460 461 AccessModifierOption accessModifier = AccessModifierOption.PACKAGE; 462 for (DetailAST token = modifiersToken.getFirstChild(); token != null; 463 token = token.getNextSibling()) { 464 final int tokenType = token.getType(); 465 if (tokenType == TokenTypes.LITERAL_PUBLIC) { 466 accessModifier = AccessModifierOption.PUBLIC; 467 } 468 else if (tokenType == TokenTypes.LITERAL_PROTECTED) { 469 accessModifier = AccessModifierOption.PROTECTED; 470 } 471 else if (tokenType == TokenTypes.LITERAL_PRIVATE) { 472 accessModifier = AccessModifierOption.PRIVATE; 473 } 474 } 475 return accessModifier; 476 } 477 478 /** 479 * Returns the access modifier of the surrounding "block". 480 * 481 * @param node the node to return the access modifier for 482 * @return the access modifier of the surrounding block 483 */ 484 public static AccessModifierOption getSurroundingAccessModifier(DetailAST node) { 485 AccessModifierOption returnValue = null; 486 for (DetailAST token = node.getParent(); 487 returnValue == null && !TokenUtil.isRootNode(token); 488 token = token.getParent()) { 489 final int type = token.getType(); 490 if (type == TokenTypes.CLASS_DEF 491 || type == TokenTypes.INTERFACE_DEF 492 || type == TokenTypes.ANNOTATION_DEF 493 || type == TokenTypes.ENUM_DEF) { 494 returnValue = getAccessModifierFromModifiersToken(token); 495 } 496 else if (type == TokenTypes.LITERAL_NEW) { 497 break; 498 } 499 } 500 501 return returnValue; 502 } 503 504 /** 505 * Create set of class names and short class names. 506 * 507 * @param classNames array of class names. 508 * @return set of class names and short class names. 509 */ 510 public static Set<String> parseClassNames(String... classNames) { 511 final Set<String> illegalClassNames = new HashSet<>(); 512 for (final String name : classNames) { 513 illegalClassNames.add(name); 514 final int lastDot = name.lastIndexOf('.'); 515 if (lastDot != -1 && lastDot < name.length() - 1) { 516 final String shortName = name 517 .substring(name.lastIndexOf('.') + 1); 518 illegalClassNames.add(shortName); 519 } 520 } 521 return illegalClassNames; 522 } 523 524 /** 525 * Strip initial newline and preceding whitespace on each line from text block content. 526 * In order to be consistent with how javac handles this task, we have modeled this 527 * implementation after the code from: 528 * github.com/openjdk/jdk14u/blob/master/src/java.base/share/classes/java/lang/String.java 529 * 530 * @param textBlockContent the actual content of the text block. 531 * @return string consistent with javac representation. 532 */ 533 public static String stripIndentAndInitialNewLineFromTextBlock(String textBlockContent) { 534 final String contentWithInitialNewLineRemoved = 535 ALL_NEW_LINES.matcher(textBlockContent).replaceFirst(""); 536 final List<String> lines = 537 Arrays.asList(ALL_NEW_LINES.split(contentWithInitialNewLineRemoved)); 538 final int indent = getSmallestIndent(lines); 539 final String suffix = ""; 540 541 return lines.stream() 542 .map(line -> stripIndentAndTrailingWhitespaceFromLine(line, indent)) 543 .collect(Collectors.joining(System.lineSeparator(), suffix, suffix)); 544 } 545 546 /** 547 * Helper method for stripIndentAndInitialNewLineFromTextBlock, strips correct indent 548 * from string, and trailing whitespace, or returns empty string if no text. 549 * 550 * @param line the string to strip indent and trailing whitespace from 551 * @param indent the amount of indent to remove 552 * @return modified string with removed indent and trailing whitespace, or empty string. 553 */ 554 private static String stripIndentAndTrailingWhitespaceFromLine(String line, int indent) { 555 final int lastNonWhitespace = lastIndexOfNonWhitespace(line); 556 String returnString = ""; 557 if (lastNonWhitespace > 0) { 558 returnString = line.substring(indent, lastNonWhitespace); 559 } 560 return returnString; 561 } 562 563 /** 564 * Helper method for stripIndentAndInitialNewLineFromTextBlock, to determine the smallest 565 * indent in a text block string literal. 566 * 567 * @param lines list of actual text block content, split by line. 568 * @return number of spaces representing the smallest indent in this text block. 569 */ 570 private static int getSmallestIndent(List<String> lines) { 571 return lines.stream() 572 .mapToInt(CommonUtil::indexOfNonWhitespace) 573 .min() 574 .orElse(0); 575 } 576 577 /** 578 * Helper method to find the index of the last non-whitespace character in a string. 579 * 580 * @param line the string to find the last index of a non-whitespace character for. 581 * @return the index of the last non-whitespace character. 582 */ 583 private static int lastIndexOfNonWhitespace(String line) { 584 int length; 585 for (length = line.length(); length > 0; length--) { 586 if (!Character.isWhitespace(line.charAt(length - 1))) { 587 break; 588 } 589 } 590 return length; 591 } 592}