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