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