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