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