001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2023 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.Closeable; 023import java.io.File; 024import java.io.IOException; 025import java.lang.reflect.Constructor; 026import java.lang.reflect.InvocationTargetException; 027import java.net.MalformedURLException; 028import java.net.URI; 029import java.net.URISyntaxException; 030import java.net.URL; 031import java.nio.file.Path; 032import java.nio.file.Paths; 033import java.util.BitSet; 034import java.util.Objects; 035import java.util.regex.Matcher; 036import java.util.regex.Pattern; 037import java.util.regex.PatternSyntaxException; 038 039import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 040 041/** 042 * Contains utility methods. 043 * 044 */ 045public final class CommonUtil { 046 047 /** Default tab width for column reporting. */ 048 public static final int DEFAULT_TAB_WIDTH = 8; 049 050 /** For cases where no tokens should be accepted. */ 051 public static final BitSet EMPTY_BIT_SET = new BitSet(); 052 /** Copied from org.apache.commons.lang3.ArrayUtils. */ 053 public static final String[] EMPTY_STRING_ARRAY = new String[0]; 054 /** Copied from org.apache.commons.lang3.ArrayUtils. */ 055 public static final Integer[] EMPTY_INTEGER_OBJECT_ARRAY = new Integer[0]; 056 /** Copied from org.apache.commons.lang3.ArrayUtils. */ 057 public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; 058 /** Copied from org.apache.commons.lang3.ArrayUtils. */ 059 public static final int[] EMPTY_INT_ARRAY = new int[0]; 060 /** Copied from org.apache.commons.lang3.ArrayUtils. */ 061 public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; 062 /** Copied from org.apache.commons.lang3.ArrayUtils. */ 063 public static final double[] EMPTY_DOUBLE_ARRAY = new double[0]; 064 /** Pseudo URL protocol for loading from the class path. */ 065 public static final String CLASSPATH_URL_PROTOCOL = "classpath:"; 066 067 /** Prefix for the exception when unable to find resource. */ 068 private static final String UNABLE_TO_FIND_EXCEPTION_PREFIX = "Unable to find: "; 069 070 /** Stop instances being created. **/ 071 private CommonUtil() { 072 } 073 074 /** 075 * Helper method to create a regular expression. 076 * 077 * @param pattern 078 * the pattern to match 079 * @return a created regexp object 080 * @throws IllegalArgumentException 081 * if unable to create Pattern object. 082 **/ 083 public static Pattern createPattern(String pattern) { 084 return createPattern(pattern, 0); 085 } 086 087 /** 088 * Helper method to create a regular expression with a specific flags. 089 * 090 * @param pattern 091 * the pattern to match 092 * @param flags 093 * the flags to set 094 * @return a created regexp object 095 * @throws IllegalArgumentException 096 * if unable to create Pattern object. 097 **/ 098 public static Pattern createPattern(String pattern, int flags) { 099 try { 100 return Pattern.compile(pattern, flags); 101 } 102 catch (final PatternSyntaxException ex) { 103 throw new IllegalArgumentException( 104 "Failed to initialise regular expression " + pattern, ex); 105 } 106 } 107 108 /** 109 * Returns whether the file extension matches what we are meant to process. 110 * 111 * @param file 112 * the file to be checked. 113 * @param fileExtensions 114 * files extensions, empty property in config makes it matches to all. 115 * @return whether there is a match. 116 */ 117 public static boolean matchesFileExtension(File file, String... fileExtensions) { 118 boolean result = false; 119 if (fileExtensions == null || fileExtensions.length == 0) { 120 result = true; 121 } 122 else { 123 // normalize extensions so all of them have a leading dot 124 final String[] withDotExtensions = new String[fileExtensions.length]; 125 for (int i = 0; i < fileExtensions.length; i++) { 126 final String extension = fileExtensions[i]; 127 if (startsWithChar(extension, '.')) { 128 withDotExtensions[i] = extension; 129 } 130 else { 131 withDotExtensions[i] = "." + extension; 132 } 133 } 134 135 final String fileName = file.getName(); 136 for (final String fileExtension : withDotExtensions) { 137 if (fileName.endsWith(fileExtension)) { 138 result = true; 139 break; 140 } 141 } 142 } 143 144 return result; 145 } 146 147 /** 148 * Returns whether the specified string contains only whitespace up to the specified index. 149 * 150 * @param index 151 * index to check up to 152 * @param line 153 * the line to check 154 * @return whether there is only whitespace 155 */ 156 public static boolean hasWhitespaceBefore(int index, String line) { 157 boolean result = true; 158 for (int i = 0; i < index; i++) { 159 if (!Character.isWhitespace(line.charAt(i))) { 160 result = false; 161 break; 162 } 163 } 164 return result; 165 } 166 167 /** 168 * Returns the length of a string ignoring all trailing whitespace. 169 * It is a pity that there is not a trim() like 170 * method that only removed the trailing whitespace. 171 * 172 * @param line 173 * the string to process 174 * @return the length of the string ignoring all trailing whitespace 175 **/ 176 public static int lengthMinusTrailingWhitespace(String line) { 177 int len = line.length(); 178 for (int i = len - 1; i >= 0; i--) { 179 if (!Character.isWhitespace(line.charAt(i))) { 180 break; 181 } 182 len--; 183 } 184 return len; 185 } 186 187 /** 188 * Returns the length of a String prefix with tabs expanded. 189 * Each tab is counted as the number of characters is 190 * takes to jump to the next tab stop. 191 * 192 * @param inputString 193 * the input String 194 * @param toIdx 195 * index in string (exclusive) where the calculation stops 196 * @param tabWidth 197 * the distance between tab stop position. 198 * @return the length of string.substring(0, toIdx) with tabs expanded. 199 */ 200 public static int lengthExpandedTabs(String inputString, 201 int toIdx, 202 int tabWidth) { 203 int len = 0; 204 for (int idx = 0; idx < toIdx; idx++) { 205 if (inputString.codePointAt(idx) == '\t') { 206 len = (len / tabWidth + 1) * tabWidth; 207 } 208 else { 209 len++; 210 } 211 } 212 return len; 213 } 214 215 /** 216 * Validates whether passed string is a valid pattern or not. 217 * 218 * @param pattern 219 * string to validate 220 * @return true if the pattern is valid false otherwise 221 */ 222 public static boolean isPatternValid(String pattern) { 223 boolean isValid = true; 224 try { 225 Pattern.compile(pattern); 226 } 227 catch (final PatternSyntaxException ignored) { 228 isValid = false; 229 } 230 return isValid; 231 } 232 233 /** 234 * Returns base class name from qualified name. 235 * 236 * @param type 237 * the fully qualified name. Cannot be null 238 * @return the base class name from a fully qualified name 239 */ 240 public static String baseClassName(String type) { 241 final String className; 242 final int index = type.lastIndexOf('.'); 243 if (index == -1) { 244 className = type; 245 } 246 else { 247 className = type.substring(index + 1); 248 } 249 return className; 250 } 251 252 /** 253 * Constructs a normalized relative path between base directory and a given path. 254 * 255 * @param baseDirectory 256 * the base path to which given path is relativized 257 * @param path 258 * the path to relativize against base directory 259 * @return the relative normalized path between base directory and 260 * path or path if base directory is null. 261 */ 262 public static String relativizeAndNormalizePath(final String baseDirectory, final String path) { 263 final String resultPath; 264 if (baseDirectory == null) { 265 resultPath = path; 266 } 267 else { 268 final Path pathAbsolute = Paths.get(path).normalize(); 269 final Path pathBase = Paths.get(baseDirectory).normalize(); 270 resultPath = pathBase.relativize(pathAbsolute).toString(); 271 } 272 return resultPath; 273 } 274 275 /** 276 * Tests if this string starts with the specified prefix. 277 * <p> 278 * It is faster version of {@link String#startsWith(String)} optimized for 279 * one-character prefixes at the expense of 280 * some readability. Suggested by 281 * <a href="https://pmd.github.io/latest/pmd_rules_java_performance.html#simplifystartswith"> 282 * SimplifyStartsWith</a> PMD rule: 283 * </p> 284 * 285 * @param value 286 * the {@code String} to check 287 * @param prefix 288 * the prefix to find 289 * @return {@code true} if the {@code char} is a prefix of the given {@code String}; 290 * {@code false} otherwise. 291 */ 292 public static boolean startsWithChar(String value, char prefix) { 293 return !value.isEmpty() && value.charAt(0) == prefix; 294 } 295 296 /** 297 * Tests if this string ends with the specified suffix. 298 * <p> 299 * It is faster version of {@link String#endsWith(String)} optimized for 300 * one-character suffixes at the expense of 301 * some readability. Suggested by 302 * <a href="https://pmd.github.io/latest/pmd_rules_java_performance.html#simplifystartswith"> 303 * SimplifyStartsWith</a> PMD rule: 304 * </p> 305 * 306 * @param value 307 * the {@code String} to check 308 * @param suffix 309 * the suffix to find 310 * @return {@code true} if the {@code char} is a suffix of the given {@code String}; 311 * {@code false} otherwise. 312 */ 313 public static boolean endsWithChar(String value, char suffix) { 314 return !value.isEmpty() && value.charAt(value.length() - 1) == suffix; 315 } 316 317 /** 318 * Gets constructor of targetClass. 319 * 320 * @param <T> type of the target class object. 321 * @param targetClass 322 * from which constructor is returned 323 * @param parameterTypes 324 * of constructor 325 * @return constructor of targetClass 326 * @throws IllegalStateException if any exception occurs 327 * @see Class#getConstructor(Class[]) 328 */ 329 public static <T> Constructor<T> getConstructor(Class<T> targetClass, 330 Class<?>... parameterTypes) { 331 try { 332 return targetClass.getConstructor(parameterTypes); 333 } 334 catch (NoSuchMethodException ex) { 335 throw new IllegalStateException(ex); 336 } 337 } 338 339 /** 340 * Returns new instance of a class. 341 * 342 * @param <T> 343 * type of constructor 344 * @param constructor 345 * to invoke 346 * @param parameters 347 * to pass to constructor 348 * @return new instance of class 349 * @throws IllegalStateException if any exception occurs 350 * @see Constructor#newInstance(Object...) 351 */ 352 public static <T> T invokeConstructor(Constructor<T> constructor, Object... parameters) { 353 try { 354 return constructor.newInstance(parameters); 355 } 356 catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) { 357 throw new IllegalStateException(ex); 358 } 359 } 360 361 /** 362 * Closes a stream re-throwing IOException as IllegalStateException. 363 * 364 * @param closeable 365 * Closeable object 366 * @throws IllegalStateException when any IOException occurs 367 */ 368 public static void close(Closeable closeable) { 369 if (closeable != null) { 370 try { 371 closeable.close(); 372 } 373 catch (IOException ex) { 374 throw new IllegalStateException("Cannot close the stream", ex); 375 } 376 } 377 } 378 379 /** 380 * Resolve the specified filename to a URI. 381 * 382 * @param filename name of the file 383 * @return resolved file URI 384 * @throws CheckstyleException on failure 385 */ 386 public static URI getUriByFilename(String filename) throws CheckstyleException { 387 URI uri = getWebOrFileProtocolUri(filename); 388 389 if (uri == null) { 390 uri = getFilepathOrClasspathUri(filename); 391 } 392 393 return uri; 394 } 395 396 /** 397 * Resolves the specified filename containing 'http', 'https', 'ftp', 398 * and 'file' protocols (or any RFC 2396 compliant URL) to a URI. 399 * 400 * @param filename name of the file 401 * @return resolved file URI or null if URL is malformed or non-existent 402 */ 403 public static URI getWebOrFileProtocolUri(String filename) { 404 URI uri; 405 try { 406 final URL url = new URL(filename); 407 uri = url.toURI(); 408 } 409 catch (URISyntaxException | MalformedURLException ignored) { 410 uri = null; 411 } 412 return uri; 413 } 414 415 /** 416 * Resolves the specified local filename, possibly with 'classpath:' 417 * protocol, to a URI. First we attempt to create a new file with 418 * given filename, then attempt to load file from class path. 419 * 420 * @param filename name of the file 421 * @return resolved file URI 422 * @throws CheckstyleException on failure 423 */ 424 private static URI getFilepathOrClasspathUri(String filename) throws CheckstyleException { 425 final URI uri; 426 final File file = new File(filename); 427 428 if (file.exists()) { 429 uri = file.toURI(); 430 } 431 else { 432 final int lastIndexOfClasspathProtocol; 433 if (filename.lastIndexOf(CLASSPATH_URL_PROTOCOL) == 0) { 434 lastIndexOfClasspathProtocol = CLASSPATH_URL_PROTOCOL.length(); 435 } 436 else { 437 lastIndexOfClasspathProtocol = 0; 438 } 439 uri = getResourceFromClassPath(filename 440 .substring(lastIndexOfClasspathProtocol)); 441 } 442 return uri; 443 } 444 445 /** 446 * Gets a resource from the classpath. 447 * 448 * @param filename name of file 449 * @return URI of file in classpath 450 * @throws CheckstyleException on failure 451 */ 452 public static URI getResourceFromClassPath(String filename) throws CheckstyleException { 453 final URL configUrl; 454 if (filename.charAt(0) == '/') { 455 configUrl = getCheckstyleResource(filename); 456 } 457 else { 458 configUrl = ClassLoader.getSystemResource(filename); 459 } 460 461 if (configUrl == null) { 462 throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename); 463 } 464 465 final URI uri; 466 try { 467 uri = configUrl.toURI(); 468 } 469 catch (final URISyntaxException ex) { 470 throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename, ex); 471 } 472 473 return uri; 474 } 475 476 /** 477 * Finds a resource with a given name in the Checkstyle resource bundle. 478 * This method is intended only for internal use in Checkstyle tests for 479 * easy mocking to gain 100% coverage. 480 * 481 * @param name name of the desired resource 482 * @return URI of the resource 483 */ 484 public static URL getCheckstyleResource(String name) { 485 return CommonUtil.class.getResource(name); 486 } 487 488 /** 489 * Puts part of line, which matches regexp into given template 490 * on positions $n where 'n' is number of matched part in line. 491 * 492 * @param template the string to expand. 493 * @param lineToPlaceInTemplate contains expression which should be placed into string. 494 * @param regexp expression to find in comment. 495 * @return the string, based on template filled with given lines 496 */ 497 public static String fillTemplateWithStringsByRegexp( 498 String template, String lineToPlaceInTemplate, Pattern regexp) { 499 final Matcher matcher = regexp.matcher(lineToPlaceInTemplate); 500 String result = template; 501 if (matcher.find()) { 502 for (int i = 0; i <= matcher.groupCount(); i++) { 503 // $n expands comment match like in Pattern.subst(). 504 result = result.replaceAll("\\$" + i, matcher.group(i)); 505 } 506 } 507 return result; 508 } 509 510 /** 511 * Returns file name without extension. 512 * We do not use the method from Guava library to reduce Checkstyle's dependencies 513 * on external libraries. 514 * 515 * @param fullFilename file name with extension. 516 * @return file name without extension. 517 */ 518 public static String getFileNameWithoutExtension(String fullFilename) { 519 final String fileName = new File(fullFilename).getName(); 520 final int dotIndex = fileName.lastIndexOf('.'); 521 final String fileNameWithoutExtension; 522 if (dotIndex == -1) { 523 fileNameWithoutExtension = fileName; 524 } 525 else { 526 fileNameWithoutExtension = fileName.substring(0, dotIndex); 527 } 528 return fileNameWithoutExtension; 529 } 530 531 /** 532 * Returns file extension for the given file name 533 * or empty string if file does not have an extension. 534 * We do not use the method from Guava library to reduce Checkstyle's dependencies 535 * on external libraries. 536 * 537 * @param fileNameWithExtension file name with extension. 538 * @return file extension for the given file name 539 * or empty string if file does not have an extension. 540 */ 541 public static String getFileExtension(String fileNameWithExtension) { 542 final String fileName = Paths.get(fileNameWithExtension).toString(); 543 final int dotIndex = fileName.lastIndexOf('.'); 544 final String extension; 545 if (dotIndex == -1) { 546 extension = ""; 547 } 548 else { 549 extension = fileName.substring(dotIndex + 1); 550 } 551 return extension; 552 } 553 554 /** 555 * Checks whether the given string is a valid identifier. 556 * 557 * @param str A string to check. 558 * @return true when the given string contains valid identifier. 559 */ 560 public static boolean isIdentifier(String str) { 561 boolean isIdentifier = !str.isEmpty(); 562 563 for (int i = 0; isIdentifier && i < str.length(); i++) { 564 if (i == 0) { 565 isIdentifier = Character.isJavaIdentifierStart(str.charAt(0)); 566 } 567 else { 568 isIdentifier = Character.isJavaIdentifierPart(str.charAt(i)); 569 } 570 } 571 572 return isIdentifier; 573 } 574 575 /** 576 * Checks whether the given string is a valid name. 577 * 578 * @param str A string to check. 579 * @return true when the given string contains valid name. 580 */ 581 public static boolean isName(String str) { 582 boolean isName = !str.isEmpty(); 583 584 final String[] identifiers = str.split("\\.", -1); 585 for (int i = 0; isName && i < identifiers.length; i++) { 586 isName = isIdentifier(identifiers[i]); 587 } 588 589 return isName; 590 } 591 592 /** 593 * Checks if the value arg is blank by either being null, 594 * empty, or contains only whitespace characters. 595 * 596 * @param value A string to check. 597 * @return true if the arg is blank. 598 */ 599 public static boolean isBlank(String value) { 600 return Objects.isNull(value) 601 || indexOfNonWhitespace(value) >= value.length(); 602 } 603 604 /** 605 * Method to find the index of the first non-whitespace character in a string. 606 * 607 * @param value the string to find the first index of a non-whitespace character for. 608 * @return the index of the first non-whitespace character. 609 */ 610 public static int indexOfNonWhitespace(String value) { 611 final int length = value.length(); 612 int left = 0; 613 while (left < length) { 614 final int codePointAt = value.codePointAt(left); 615 if (!Character.isWhitespace(codePointAt)) { 616 break; 617 } 618 left += Character.charCount(codePointAt); 619 } 620 return left; 621 } 622 623 /** 624 * Checks whether the string contains an integer value. 625 * 626 * @param str a string to check 627 * @return true if the given string is an integer, false otherwise. 628 */ 629 public static boolean isInt(String str) { 630 boolean isInt; 631 if (str == null) { 632 isInt = false; 633 } 634 else { 635 try { 636 Integer.parseInt(str); 637 isInt = true; 638 } 639 catch (NumberFormatException ignored) { 640 isInt = false; 641 } 642 } 643 return isInt; 644 } 645 646 /** 647 * Converts the Unicode code point at index {@code index} to it's UTF-16 648 * representation, then checks if the character is whitespace. Note that the given 649 * index {@code index} should correspond to the location of the character 650 * to check in the string, not in code points. 651 * 652 * @param codePoints the array of Unicode code points 653 * @param index the index of the character to check 654 * @return true if character at {@code index} is whitespace 655 */ 656 public static boolean isCodePointWhitespace(int[] codePoints, int index) { 657 // We only need to check the first member of a surrogate pair to verify that 658 // it is not whitespace. 659 final char character = Character.toChars(codePoints[index])[0]; 660 return Character.isWhitespace(character); 661 } 662 663}