View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.utils;
21  
22  import java.io.Closeable;
23  import java.io.File;
24  import java.io.IOException;
25  import java.lang.reflect.Constructor;
26  import java.lang.reflect.InvocationTargetException;
27  import java.net.MalformedURLException;
28  import java.net.URI;
29  import java.net.URISyntaxException;
30  import java.net.URL;
31  import java.nio.file.Path;
32  import java.util.BitSet;
33  import java.util.Objects;
34  import java.util.regex.Matcher;
35  import java.util.regex.Pattern;
36  import java.util.regex.PatternSyntaxException;
37  
38  import org.xml.sax.InputSource;
39  
40  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
41  
42  /**
43   * Contains utility methods.
44   *
45   */
46  public final class CommonUtil {
47  
48      /** Default tab width for column reporting. */
49      public static final int DEFAULT_TAB_WIDTH = 8;
50  
51      /** For cases where no tokens should be accepted. */
52      public static final BitSet EMPTY_BIT_SET = new BitSet();
53      /** Copied from org.apache.commons.lang3.ArrayUtils. */
54      public static final String[] EMPTY_STRING_ARRAY = new String[0];
55      /** Copied from org.apache.commons.lang3.ArrayUtils. */
56      public static final Integer[] EMPTY_INTEGER_OBJECT_ARRAY = new Integer[0];
57      /** Copied from org.apache.commons.lang3.ArrayUtils. */
58      public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
59      /** Copied from org.apache.commons.lang3.ArrayUtils. */
60      public static final int[] EMPTY_INT_ARRAY = new int[0];
61      /** Copied from org.apache.commons.lang3.ArrayUtils. */
62      public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
63      /** Copied from org.apache.commons.lang3.ArrayUtils. */
64      public static final double[] EMPTY_DOUBLE_ARRAY = new double[0];
65      /** Pseudo URL protocol for loading from the class path. */
66      public static final String CLASSPATH_URL_PROTOCOL = "classpath:";
67  
68      /** Prefix for the exception when unable to find resource. */
69      private static final String UNABLE_TO_FIND_EXCEPTION_PREFIX = "Unable to find: ";
70  
71      /** The extension separator. */
72      private static final String EXTENSION_SEPARATOR = ".";
73  
74      /** Stop instances being created. **/
75      private CommonUtil() {
76      }
77  
78      /**
79       * Helper method to create a regular expression.
80       *
81       * @param pattern
82       *            the pattern to match
83       * @return a created regexp object
84       * @throws IllegalArgumentException
85       *             if unable to create Pattern object.
86       **/
87      public static Pattern createPattern(String pattern) {
88          return createPattern(pattern, 0);
89      }
90  
91      /**
92       * Helper method to create a regular expression with a specific flags.
93       *
94       * @param pattern
95       *            the pattern to match
96       * @param flags
97       *            the flags to set
98       * @return a created regexp object
99       * @throws IllegalArgumentException
100      *             if unable to create Pattern object.
101      **/
102     public static Pattern createPattern(String pattern, int flags) {
103         try {
104             return Pattern.compile(pattern, flags);
105         }
106         catch (final PatternSyntaxException exc) {
107             throw new IllegalArgumentException(
108                 "Failed to initialise regular expression " + pattern, exc);
109         }
110     }
111 
112     /**
113      * Returns whether the file extension matches what we are meant to process.
114      *
115      * @param file
116      *            the file to be checked.
117      * @param fileExtensions
118      *            files extensions, empty property in config makes it matches to all.
119      * @return whether there is a match.
120      */
121     public static boolean matchesFileExtension(File file, String... fileExtensions) {
122         boolean result = false;
123         if (fileExtensions == null || fileExtensions.length == 0) {
124             result = true;
125         }
126         else {
127             // normalize extensions so all of them have a leading dot
128             final String[] withDotExtensions = new String[fileExtensions.length];
129             for (int i = 0; i < fileExtensions.length; i++) {
130                 final String extension = fileExtensions[i];
131                 if (extension.startsWith(EXTENSION_SEPARATOR)) {
132                     withDotExtensions[i] = extension;
133                 }
134                 else {
135                     withDotExtensions[i] = EXTENSION_SEPARATOR + extension;
136                 }
137             }
138 
139             final String fileName = file.getName();
140             for (final String fileExtension : withDotExtensions) {
141                 if (fileName.endsWith(fileExtension)) {
142                     result = true;
143                     break;
144                 }
145             }
146         }
147 
148         return result;
149     }
150 
151     /**
152      * Returns whether the specified string contains only whitespace up to the specified index.
153      *
154      * @param index
155      *            index to check up to
156      * @param line
157      *            the line to check
158      * @return whether there is only whitespace
159      */
160     public static boolean hasWhitespaceBefore(int index, String line) {
161         boolean result = true;
162         for (int i = 0; i < index; i++) {
163             if (!Character.isWhitespace(line.charAt(i))) {
164                 result = false;
165                 break;
166             }
167         }
168         return result;
169     }
170 
171     /**
172      * Returns the length of a string ignoring all trailing whitespace.
173      * It is a pity that there is not a trim() like
174      * method that only removed the trailing whitespace.
175      *
176      * @param line
177      *            the string to process
178      * @return the length of the string ignoring all trailing whitespace
179      **/
180     public static int lengthMinusTrailingWhitespace(String line) {
181         int len = line.length();
182         for (int i = len - 1; i >= 0; i--) {
183             if (!Character.isWhitespace(line.charAt(i))) {
184                 break;
185             }
186             len--;
187         }
188         return len;
189     }
190 
191     /**
192      * Returns the length of a String prefix with tabs expanded.
193      * Each tab is counted as the number of characters is
194      * takes to jump to the next tab stop.
195      *
196      * @param inputString
197      *            the input String
198      * @param toIdx
199      *            index in string (exclusive) where the calculation stops
200      * @param tabWidth
201      *            the distance between tab stop position.
202      * @return the length of string.substring(0, toIdx) with tabs expanded.
203      */
204     public static int lengthExpandedTabs(String inputString,
205             int toIdx,
206             int tabWidth) {
207         int len = 0;
208         for (int idx = 0; idx < toIdx; idx++) {
209             if (inputString.codePointAt(idx) == '\t') {
210                 len = (len / tabWidth + 1) * tabWidth;
211             }
212             else {
213                 len++;
214             }
215         }
216         return len;
217     }
218 
219     /**
220      * Validates whether passed string is a valid pattern or not.
221      *
222      * @param pattern
223      *            string to validate
224      * @return true if the pattern is valid false otherwise
225      */
226     public static boolean isPatternValid(String pattern) {
227         boolean isValid = true;
228         try {
229             Pattern.compile(pattern);
230         }
231         catch (final PatternSyntaxException ignored) {
232             isValid = false;
233         }
234         return isValid;
235     }
236 
237     /**
238      * Returns base class name from qualified name.
239      *
240      * @param type
241      *            the fully qualified name. Cannot be null
242      * @return the base class name from a fully qualified name
243      */
244     public static String baseClassName(String type) {
245         final int index = type.lastIndexOf('.');
246         return type.substring(index + 1);
247     }
248 
249     /**
250      * Constructs a relative path between base directory and a given path.
251      *
252      * @param baseDirectory
253      *            the base path to which given path is relativized
254      * @param path
255      *            the path to relativize against base directory
256      * @return the relative normalized path between base directory and
257      *     path or path if base directory is null.
258      */
259     public static String relativizePath(final String baseDirectory, final String path) {
260         final String resultPath;
261         if (baseDirectory == null) {
262             resultPath = path;
263         }
264         else {
265             final Path pathAbsolute = Path.of(path);
266             final Path pathBase = Path.of(baseDirectory);
267             resultPath = pathBase.relativize(pathAbsolute).toString();
268         }
269         return resultPath;
270     }
271 
272     /**
273      * Gets constructor of targetClass.
274      *
275      * @param <T> type of the target class object.
276      * @param targetClass
277      *            from which constructor is returned
278      * @param parameterTypes
279      *            of constructor
280      * @return constructor of targetClass
281      * @throws IllegalStateException if any exception occurs
282      * @see Class#getConstructor(Class[])
283      */
284     public static <T> Constructor<T> getConstructor(Class<T> targetClass,
285                                                     Class<?>... parameterTypes) {
286         try {
287             return targetClass.getConstructor(parameterTypes);
288         }
289         catch (NoSuchMethodException exc) {
290             throw new IllegalStateException(exc);
291         }
292     }
293 
294     /**
295      * Returns new instance of a class.
296      *
297      * @param <T>
298      *            type of constructor
299      * @param constructor
300      *            to invoke
301      * @param parameters
302      *            to pass to constructor
303      * @return new instance of class
304      * @throws IllegalStateException if any exception occurs
305      * @see Constructor#newInstance(Object...)
306      */
307     public static <T> T invokeConstructor(Constructor<T> constructor, Object... parameters) {
308         try {
309             return constructor.newInstance(parameters);
310         }
311         catch (InstantiationException | IllegalAccessException | InvocationTargetException exc) {
312             throw new IllegalStateException(exc);
313         }
314     }
315 
316     /**
317      * Closes a stream re-throwing IOException as IllegalStateException.
318      *
319      * @param closeable
320      *            Closeable object
321      * @throws IllegalStateException when any IOException occurs
322      */
323     public static void close(Closeable closeable) {
324         if (closeable != null) {
325             try {
326                 closeable.close();
327             }
328             catch (IOException exc) {
329                 throw new IllegalStateException("Cannot close the stream", exc);
330             }
331         }
332     }
333 
334     /**
335      * Creates an input source from a file.
336      *
337      * @param filename name of the file.
338      * @return input source.
339      * @throws CheckstyleException if an error occurs.
340      */
341     public static InputSource sourceFromFilename(String filename) throws CheckstyleException {
342         // figure out if this is a File or a URL
343         final URI uri = getUriByFilename(filename);
344         return new InputSource(uri.toASCIIString());
345     }
346 
347     /**
348      * Resolve the specified filename to a URI.
349      *
350      * @param filename name of the file
351      * @return resolved file URI
352      * @throws CheckstyleException on failure
353      */
354     public static URI getUriByFilename(String filename) throws CheckstyleException {
355         URI uri = getWebOrFileProtocolUri(filename);
356 
357         if (uri == null) {
358             uri = getFilepathOrClasspathUri(filename);
359         }
360 
361         return uri;
362     }
363 
364     /**
365      * Resolves the specified filename containing 'http', 'https', 'ftp',
366      * and 'file' protocols (or any RFC 2396 compliant URL) to a URI.
367      *
368      * @param filename name of the file
369      * @return resolved file URI or null if URL is malformed or non-existent
370      * @noinspection deprecation
371      * @noinspectionreason Disabled until #17646
372      */
373     public static URI getWebOrFileProtocolUri(String filename) {
374         URI uri;
375         try {
376             final URL url = new URL(filename);
377             uri = url.toURI();
378         }
379         catch (URISyntaxException | MalformedURLException ignored) {
380             uri = null;
381         }
382         return uri;
383     }
384 
385     /**
386      * Resolves the specified local filename, possibly with 'classpath:'
387      * protocol, to a URI.  First we attempt to create a new file with
388      * given filename, then attempt to load file from class path.
389      *
390      * @param filename name of the file
391      * @return resolved file URI
392      * @throws CheckstyleException on failure
393      */
394     private static URI getFilepathOrClasspathUri(String filename) throws CheckstyleException {
395         final URI uri;
396         final File file = new File(filename);
397 
398         if (file.exists()) {
399             uri = file.toURI();
400         }
401         else {
402             final int lastIndexOfClasspathProtocol;
403             if (filename.lastIndexOf(CLASSPATH_URL_PROTOCOL) == 0) {
404                 lastIndexOfClasspathProtocol = CLASSPATH_URL_PROTOCOL.length();
405             }
406             else {
407                 lastIndexOfClasspathProtocol = 0;
408             }
409             uri = getResourceFromClassPath(filename
410                 .substring(lastIndexOfClasspathProtocol));
411         }
412         return uri;
413     }
414 
415     /**
416      * Gets a resource from the classpath.
417      *
418      * @param filename name of file
419      * @return URI of file in classpath
420      * @throws CheckstyleException on failure
421      */
422     public static URI getResourceFromClassPath(String filename) throws CheckstyleException {
423         final URL configUrl;
424         if (filename.charAt(0) == '/') {
425             configUrl = getCheckstyleResource(filename);
426         }
427         else {
428             configUrl = ClassLoader.getSystemResource(filename);
429         }
430 
431         if (configUrl == null) {
432             throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename);
433         }
434 
435         final URI uri;
436         try {
437             uri = configUrl.toURI();
438         }
439         catch (final URISyntaxException exc) {
440             throw new CheckstyleException(UNABLE_TO_FIND_EXCEPTION_PREFIX + filename, exc);
441         }
442 
443         return uri;
444     }
445 
446     /**
447      * Finds a resource with a given name in the Checkstyle resource bundle.
448      * This method is intended only for internal use in Checkstyle tests for
449      * easy mocking to gain 100% coverage.
450      *
451      * @param name name of the desired resource
452      * @return URI of the resource
453      */
454     public static URL getCheckstyleResource(String name) {
455         return CommonUtil.class.getResource(name);
456     }
457 
458     /**
459      * Puts part of line, which matches regexp into given template
460      * on positions $n where 'n' is number of matched part in line.
461      *
462      * @param template the string to expand.
463      * @param lineToPlaceInTemplate contains expression which should be placed into string.
464      * @param regexp expression to find in comment.
465      * @return the string, based on template filled with given lines
466      */
467     public static String fillTemplateWithStringsByRegexp(
468         String template, String lineToPlaceInTemplate, Pattern regexp) {
469         final Matcher matcher = regexp.matcher(lineToPlaceInTemplate);
470         String result = template;
471         if (matcher.find()) {
472             for (int i = 0; i <= matcher.groupCount(); i++) {
473                 // $n expands comment match like in Pattern.subst().
474                 result = result.replaceAll("\\$" + i, matcher.group(i));
475             }
476         }
477         return result;
478     }
479 
480     /**
481      * Returns file name without extension.
482      * We do not use the method from Guava library to reduce Checkstyle's dependencies
483      * on external libraries.
484      *
485      * @param fullFilename file name with extension.
486      * @return file name without extension.
487      */
488     public static String getFileNameWithoutExtension(String fullFilename) {
489         final String fileName = new File(fullFilename).getName();
490         final int dotIndex = fileName.lastIndexOf('.');
491         final String fileNameWithoutExtension;
492         if (dotIndex == -1) {
493             fileNameWithoutExtension = fileName;
494         }
495         else {
496             fileNameWithoutExtension = fileName.substring(0, dotIndex);
497         }
498         return fileNameWithoutExtension;
499     }
500 
501     /**
502      * Returns file extension for the given file name
503      * or empty string if file does not have an extension.
504      * We do not use the method from Guava library to reduce Checkstyle's dependencies
505      * on external libraries.
506      *
507      * @param fileNameWithExtension file name with extension.
508      * @return file extension for the given file name
509      *         or empty string if file does not have an extension.
510      */
511     public static String getFileExtension(String fileNameWithExtension) {
512         final String fileName = Path.of(fileNameWithExtension).toString();
513         final int dotIndex = fileName.lastIndexOf('.');
514         final String extension;
515         if (dotIndex == -1) {
516             extension = "";
517         }
518         else {
519             extension = fileName.substring(dotIndex + 1);
520         }
521         return extension;
522     }
523 
524     /**
525      * Checks whether the given string is a valid identifier.
526      *
527      * @param str A string to check.
528      * @return true when the given string contains valid identifier.
529      */
530     public static boolean isIdentifier(String str) {
531         boolean isIdentifier = !str.isEmpty();
532 
533         for (int i = 0; isIdentifier && i < str.length(); i++) {
534             if (i == 0) {
535                 isIdentifier = Character.isJavaIdentifierStart(str.charAt(0));
536             }
537             else {
538                 isIdentifier = Character.isJavaIdentifierPart(str.charAt(i));
539             }
540         }
541 
542         return isIdentifier;
543     }
544 
545     /**
546      * Checks whether the given string is a valid name.
547      *
548      * @param str A string to check.
549      * @return true when the given string contains valid name.
550      */
551     public static boolean isName(String str) {
552         boolean isName = false;
553 
554         final String[] identifiers = str.split("\\.", -1);
555         for (String identifier : identifiers) {
556             isName = isIdentifier(identifier);
557             if (!isName) {
558                 break;
559             }
560         }
561 
562         return isName;
563     }
564 
565     /**
566      * Checks if the value arg is blank by either being null,
567      * empty, or contains only whitespace characters.
568      *
569      * @param value A string to check.
570      * @return true if the arg is blank.
571      */
572     public static boolean isBlank(String value) {
573         return Objects.isNull(value)
574                 || indexOfNonWhitespace(value) >= value.length();
575     }
576 
577     /**
578      * Method to find the index of the first non-whitespace character in a string.
579      *
580      * @param value the string to find the first index of a non-whitespace character for.
581      * @return the index of the first non-whitespace character.
582      */
583     public static int indexOfNonWhitespace(String value) {
584         final int length = value.length();
585         int left = 0;
586         while (left < length) {
587             final int codePointAt = value.codePointAt(left);
588             if (!Character.isWhitespace(codePointAt)) {
589                 break;
590             }
591             left += Character.charCount(codePointAt);
592         }
593         return left;
594     }
595 
596     /**
597      * Converts the Unicode code point at index {@code index} to it's UTF-16
598      * representation, then checks if the character is whitespace. Note that the given
599      * index {@code index} should correspond to the location of the character
600      * to check in the string, not in code points.
601      *
602      * @param codePoints the array of Unicode code points
603      * @param index the index of the character to check
604      * @return true if character at {@code index} is whitespace
605      */
606     public static boolean isCodePointWhitespace(int[] codePoints, int index) {
607         //  We only need to check the first member of a surrogate pair to verify that
608         //  it is not whitespace.
609         final char character = Character.toChars(codePoints[index])[0];
610         return Character.isWhitespace(character);
611     }
612 
613 }