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