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.nio.file.Path;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Collection;
26  import java.util.List;
27  import java.util.Set;
28  import java.util.function.Predicate;
29  import java.util.regex.Pattern;
30  import java.util.stream.Collectors;
31  import java.util.stream.Stream;
32  
33  import com.puppycrawl.tools.checkstyle.api.DetailAST;
34  import com.puppycrawl.tools.checkstyle.api.FullIdent;
35  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
36  import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
37  
38  /**
39   * Contains utility methods for the checks.
40   *
41   */
42  public final class CheckUtil {
43  
44      // constants for parseDouble()
45      /** Binary radix. */
46      private static final int BASE_2 = 2;
47  
48      /** Octal radix. */
49      private static final int BASE_8 = 8;
50  
51      /** Decimal radix. */
52      private static final int BASE_10 = 10;
53  
54      /** Hex radix. */
55      private static final int BASE_16 = 16;
56  
57      /** Pattern matching underscore characters ('_'). */
58      private static final Pattern UNDERSCORE_PATTERN = Pattern.compile("_");
59  
60      /** Compiled pattern for all system newlines. */
61      private static final Pattern ALL_NEW_LINES = Pattern.compile("\\R");
62  
63      /** Package separator. */
64      private static final char PACKAGE_SEPARATOR = '.';
65  
66      /** Prevent instances. */
67      private CheckUtil() {
68      }
69  
70      /**
71       * Tests whether a method definition AST defines an equals covariant.
72       *
73       * @param ast the method definition AST to test.
74       *     Precondition: ast is a TokenTypes.METHOD_DEF node.
75       * @return true if ast defines an equals covariant.
76       */
77      public static boolean isEqualsMethod(DetailAST ast) {
78          boolean equalsMethod = false;
79  
80          if (ast.getType() == TokenTypes.METHOD_DEF) {
81              final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
82              final boolean staticOrAbstract =
83                      modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null
84                      || modifiers.findFirstToken(TokenTypes.ABSTRACT) != null;
85  
86              if (!staticOrAbstract) {
87                  final DetailAST nameNode = ast.findFirstToken(TokenTypes.IDENT);
88                  final String name = nameNode.getText();
89  
90                  if ("equals".equals(name)) {
91                      // one parameter?
92                      final DetailAST paramsNode = ast.findFirstToken(TokenTypes.PARAMETERS);
93                      equalsMethod = paramsNode.getChildCount() == 1;
94                  }
95              }
96          }
97          return equalsMethod;
98      }
99  
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         final Path filename = Path.of(filePath).getFileName();
572         return filename != null && "package-info.java".equals(filename.toString());
573     }
574 }