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.checks.coding;
021
022import java.util.Collections;
023import java.util.HashSet;
024import java.util.Set;
025
026import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
027import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
028import com.puppycrawl.tools.checkstyle.api.DetailAST;
029import com.puppycrawl.tools.checkstyle.api.TokenTypes;
030import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
031
032/**
033 * <p>
034 * Checks that any combination of String literals
035 * is on the left side of an {@code equals()} comparison.
036 * Also checks for String literals assigned to some field
037 * (such as {@code someString.equals(anotherString = "text")}).
038 * </p>
039 * <p>Rationale: Calling the {@code equals()} method on String literals
040 * will avoid a potential {@code NullPointerException}. Also, it is
041 * pretty common to see null checks right before equals comparisons,
042 * which is not necessary in the example below.
043 * </p>
044 * <p>
045 * For example, this code:
046 * </p>
047 * <pre>
048 * String nullString = null;
049 * nullString.equals(&quot;My_Sweet_String&quot;);
050 * </pre>
051 * <p>
052 * should be refactored to:
053 * </p>
054 * <pre>
055 * String nullString = null;
056 * &quot;My_Sweet_String&quot;.equals(nullString);
057 * </pre>
058 * <ul>
059 * <li>
060 * Property {@code ignoreEqualsIgnoreCase} - Control whether to ignore
061 * {@code String.equalsIgnoreCase(String)} invocations.
062 * Type is {@code boolean}.
063 * Default value is {@code false}.
064 * </li>
065 * </ul>
066 * <p>
067 * To configure the check:
068 * </p>
069 * <pre>
070 * &lt;module name=&quot;EqualsAvoidNull&quot;/&gt;
071 * </pre>
072 * <p>
073 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
074 * </p>
075 * <p>
076 * Violation Message Keys:
077 * </p>
078 * <ul>
079 * <li>
080 * {@code equals.avoid.null}
081 * </li>
082 * <li>
083 * {@code equalsIgnoreCase.avoid.null}
084 * </li>
085 * </ul>
086 *
087 * @since 5.0
088 */
089@FileStatefulCheck
090public class EqualsAvoidNullCheck extends AbstractCheck {
091
092    /**
093     * A key is pointing to the warning message text in "messages.properties"
094     * file.
095     */
096    public static final String MSG_EQUALS_AVOID_NULL = "equals.avoid.null";
097
098    /**
099     * A key is pointing to the warning message text in "messages.properties"
100     * file.
101     */
102    public static final String MSG_EQUALS_IGNORE_CASE_AVOID_NULL = "equalsIgnoreCase.avoid.null";
103
104    /** Method name for comparison. */
105    private static final String EQUALS = "equals";
106
107    /** Type name for comparison. */
108    private static final String STRING = "String";
109
110    /** Curly for comparison. */
111    private static final String LEFT_CURLY = "{";
112
113    /** Control whether to ignore {@code String.equalsIgnoreCase(String)} invocations. */
114    private boolean ignoreEqualsIgnoreCase;
115
116    /** Stack of sets of field names, one for each class of a set of nested classes. */
117    private FieldFrame currentFrame;
118
119    @Override
120    public int[] getDefaultTokens() {
121        return getRequiredTokens();
122    }
123
124    @Override
125    public int[] getAcceptableTokens() {
126        return getRequiredTokens();
127    }
128
129    @Override
130    public int[] getRequiredTokens() {
131        return new int[] {
132            TokenTypes.METHOD_CALL,
133            TokenTypes.CLASS_DEF,
134            TokenTypes.METHOD_DEF,
135            TokenTypes.LITERAL_FOR,
136            TokenTypes.LITERAL_CATCH,
137            TokenTypes.LITERAL_TRY,
138            TokenTypes.LITERAL_SWITCH,
139            TokenTypes.VARIABLE_DEF,
140            TokenTypes.PARAMETER_DEF,
141            TokenTypes.CTOR_DEF,
142            TokenTypes.SLIST,
143            TokenTypes.OBJBLOCK,
144            TokenTypes.ENUM_DEF,
145            TokenTypes.ENUM_CONSTANT_DEF,
146            TokenTypes.LITERAL_NEW,
147            TokenTypes.LAMBDA,
148            TokenTypes.PATTERN_VARIABLE_DEF,
149        };
150    }
151
152    /**
153     * Setter to control whether to ignore {@code String.equalsIgnoreCase(String)} invocations.
154     *
155     * @param newValue whether to ignore checking
156     *    {@code String.equalsIgnoreCase(String)}.
157     */
158    public void setIgnoreEqualsIgnoreCase(boolean newValue) {
159        ignoreEqualsIgnoreCase = newValue;
160    }
161
162    @Override
163    public void beginTree(DetailAST rootAST) {
164        currentFrame = new FieldFrame(null);
165    }
166
167    @Override
168    public void visitToken(final DetailAST ast) {
169        switch (ast.getType()) {
170            case TokenTypes.VARIABLE_DEF:
171            case TokenTypes.PARAMETER_DEF:
172            case TokenTypes.PATTERN_VARIABLE_DEF:
173                currentFrame.addField(ast);
174                break;
175            case TokenTypes.METHOD_CALL:
176                processMethodCall(ast);
177                break;
178            case TokenTypes.SLIST:
179                processSlist(ast);
180                break;
181            case TokenTypes.LITERAL_NEW:
182                processLiteralNew(ast);
183                break;
184            case TokenTypes.OBJBLOCK:
185                final int parentType = ast.getParent().getType();
186                if (parentType != TokenTypes.CLASS_DEF && parentType != TokenTypes.ENUM_DEF) {
187                    processFrame(ast);
188                }
189                break;
190            default:
191                processFrame(ast);
192        }
193    }
194
195    @Override
196    public void leaveToken(DetailAST ast) {
197        final int astType = ast.getType();
198        if (astType == TokenTypes.SLIST) {
199            leaveSlist(ast);
200        }
201        else if (astType == TokenTypes.LITERAL_NEW) {
202            leaveLiteralNew(ast);
203        }
204        else if (astType == TokenTypes.OBJBLOCK) {
205            final int parentType = ast.getParent().getType();
206            if (parentType != TokenTypes.CLASS_DEF && parentType != TokenTypes.ENUM_DEF) {
207                currentFrame = currentFrame.getParent();
208            }
209        }
210        else if (astType != TokenTypes.VARIABLE_DEF
211                && astType != TokenTypes.PARAMETER_DEF
212                && astType != TokenTypes.METHOD_CALL
213                && astType != TokenTypes.PATTERN_VARIABLE_DEF) {
214            currentFrame = currentFrame.getParent();
215        }
216    }
217
218    @Override
219    public void finishTree(DetailAST ast) {
220        traverseFieldFrameTree(currentFrame);
221    }
222
223    /**
224     * Determine whether SLIST begins a block, determined by braces, and add it as
225     * a frame in this case.
226     *
227     * @param ast SLIST ast.
228     */
229    private void processSlist(DetailAST ast) {
230        if (LEFT_CURLY.equals(ast.getText())) {
231            final FieldFrame frame = new FieldFrame(currentFrame);
232            currentFrame.addChild(frame);
233            currentFrame = frame;
234        }
235    }
236
237    /**
238     * Determine whether SLIST begins a block, determined by braces.
239     *
240     * @param ast SLIST ast.
241     */
242    private void leaveSlist(DetailAST ast) {
243        if (LEFT_CURLY.equals(ast.getText())) {
244            currentFrame = currentFrame.getParent();
245        }
246    }
247
248    /**
249     * Process CLASS_DEF, METHOD_DEF, LITERAL_IF, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO,
250     * LITERAL_CATCH, LITERAL_TRY, CTOR_DEF, ENUM_DEF, ENUM_CONSTANT_DEF.
251     *
252     * @param ast processed ast.
253     */
254    private void processFrame(DetailAST ast) {
255        final FieldFrame frame = new FieldFrame(currentFrame);
256        final int astType = ast.getType();
257        if (astType == TokenTypes.CLASS_DEF
258                || astType == TokenTypes.ENUM_DEF) {
259            frame.setClassOrEnumOrEnumConstDef(true);
260            frame.setFrameName(ast.findFirstToken(TokenTypes.IDENT).getText());
261        }
262        currentFrame.addChild(frame);
263        currentFrame = frame;
264    }
265
266    /**
267     * Add the method call to the current frame if it should be processed.
268     *
269     * @param methodCall METHOD_CALL ast.
270     */
271    private void processMethodCall(DetailAST methodCall) {
272        final DetailAST dot = methodCall.getFirstChild();
273        if (dot.getType() == TokenTypes.DOT) {
274            final String methodName = dot.getLastChild().getText();
275            if (EQUALS.equals(methodName)
276                    || !ignoreEqualsIgnoreCase && "equalsIgnoreCase".equals(methodName)) {
277                currentFrame.addMethodCall(methodCall);
278            }
279        }
280    }
281
282    /**
283     * Determine whether LITERAL_NEW is an anonymous class definition and add it as
284     * a frame in this case.
285     *
286     * @param ast LITERAL_NEW ast.
287     */
288    private void processLiteralNew(DetailAST ast) {
289        if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
290            final FieldFrame frame = new FieldFrame(currentFrame);
291            currentFrame.addChild(frame);
292            currentFrame = frame;
293        }
294    }
295
296    /**
297     * Determine whether LITERAL_NEW is an anonymous class definition and leave
298     * the frame it is in.
299     *
300     * @param ast LITERAL_NEW ast.
301     */
302    private void leaveLiteralNew(DetailAST ast) {
303        if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
304            currentFrame = currentFrame.getParent();
305        }
306    }
307
308    /**
309     * Traverse the tree of the field frames to check all equals method calls.
310     *
311     * @param frame to check method calls in.
312     */
313    private void traverseFieldFrameTree(FieldFrame frame) {
314        for (FieldFrame child: frame.getChildren()) {
315            traverseFieldFrameTree(child);
316
317            currentFrame = child;
318            child.getMethodCalls().forEach(this::checkMethodCall);
319        }
320    }
321
322    /**
323     * Check whether the method call should be violated.
324     *
325     * @param methodCall method call to check.
326     */
327    private void checkMethodCall(DetailAST methodCall) {
328        DetailAST objCalledOn = methodCall.getFirstChild().getFirstChild();
329        if (objCalledOn.getType() == TokenTypes.DOT) {
330            objCalledOn = objCalledOn.getLastChild();
331        }
332        final DetailAST expr = methodCall.findFirstToken(TokenTypes.ELIST).getFirstChild();
333        if (containsOneArgument(methodCall)
334                && containsAllSafeTokens(expr)
335                && isCalledOnStringFieldOrVariable(objCalledOn)) {
336            final String methodName = methodCall.getFirstChild().getLastChild().getText();
337            if (EQUALS.equals(methodName)) {
338                log(methodCall, MSG_EQUALS_AVOID_NULL);
339            }
340            else {
341                log(methodCall, MSG_EQUALS_IGNORE_CASE_AVOID_NULL);
342            }
343        }
344    }
345
346    /**
347     * Verify that method call has one argument.
348     *
349     * @param methodCall METHOD_CALL DetailAST
350     * @return true if method call has one argument.
351     */
352    private static boolean containsOneArgument(DetailAST methodCall) {
353        final DetailAST elist = methodCall.findFirstToken(TokenTypes.ELIST);
354        return elist.getChildCount() == 1;
355    }
356
357    /**
358     * Looks for all "safe" Token combinations in the argument
359     * expression branch.
360     *
361     * @param expr the argument expression
362     * @return - true if any child matches the set of tokens, false if not
363     */
364    private static boolean containsAllSafeTokens(final DetailAST expr) {
365        DetailAST arg = expr.getFirstChild();
366        arg = skipVariableAssign(arg);
367
368        boolean argIsNotNull = false;
369        if (arg.getType() == TokenTypes.PLUS) {
370            DetailAST child = arg.getFirstChild();
371            while (child != null
372                    && !argIsNotNull) {
373                argIsNotNull = child.getType() == TokenTypes.STRING_LITERAL
374                        || child.getType() == TokenTypes.IDENT;
375                child = child.getNextSibling();
376            }
377        }
378        else {
379            argIsNotNull = arg.getType() == TokenTypes.STRING_LITERAL;
380        }
381
382        return argIsNotNull;
383    }
384
385    /**
386     * Skips over an inner assign portion of an argument expression.
387     *
388     * @param currentAST current token in the argument expression
389     * @return the next relevant token
390     */
391    private static DetailAST skipVariableAssign(final DetailAST currentAST) {
392        DetailAST result = currentAST;
393        while (result.getType() == TokenTypes.LPAREN) {
394            result = result.getNextSibling();
395        }
396        if (result.getType() == TokenTypes.ASSIGN) {
397            result = result.getFirstChild().getNextSibling();
398        }
399        return result;
400    }
401
402    /**
403     * Determine, whether equals method is called on a field of String type.
404     *
405     * @param objCalledOn object ast.
406     * @return true if the object is of String type.
407     */
408    private boolean isCalledOnStringFieldOrVariable(DetailAST objCalledOn) {
409        final boolean result;
410        final DetailAST previousSiblingAst = objCalledOn.getPreviousSibling();
411        if (previousSiblingAst == null) {
412            result = isStringFieldOrVariable(objCalledOn);
413        }
414        else {
415            if (previousSiblingAst.getType() == TokenTypes.LITERAL_THIS) {
416                result = isStringFieldOrVariableFromThisInstance(objCalledOn);
417            }
418            else {
419                final String className = previousSiblingAst.getText();
420                result = isStringFieldOrVariableFromClass(objCalledOn, className);
421            }
422        }
423        return result;
424    }
425
426    /**
427     * Whether the field or the variable is of String type.
428     *
429     * @param objCalledOn the field or the variable to check.
430     * @return true if the field or the variable is of String type.
431     */
432    private boolean isStringFieldOrVariable(DetailAST objCalledOn) {
433        boolean result = false;
434        final String name = objCalledOn.getText();
435        FieldFrame frame = currentFrame;
436        while (frame != null) {
437            final DetailAST field = frame.findField(name);
438            if (field != null
439                    && (frame.isClassOrEnumOrEnumConstDef()
440                            || checkLineNo(field, objCalledOn))) {
441                result = STRING.equals(getFieldType(field));
442                break;
443            }
444            frame = frame.getParent();
445        }
446        return result;
447    }
448
449    /**
450     * Whether the field or the variable from THIS instance is of String type.
451     *
452     * @param objCalledOn the field or the variable from THIS instance to check.
453     * @return true if the field or the variable from THIS instance is of String type.
454     */
455    private boolean isStringFieldOrVariableFromThisInstance(DetailAST objCalledOn) {
456        final String name = objCalledOn.getText();
457        final DetailAST field = getObjectFrame(currentFrame).findField(name);
458        return STRING.equals(getFieldType(field));
459    }
460
461    /**
462     * Whether the field or the variable from the specified class is of String type.
463     *
464     * @param objCalledOn the field or the variable from the specified class to check.
465     * @param className the name of the class to check in.
466     * @return true if the field or the variable from the specified class is of String type.
467     */
468    private boolean isStringFieldOrVariableFromClass(DetailAST objCalledOn,
469            final String className) {
470        boolean result = false;
471        final String name = objCalledOn.getText();
472        FieldFrame frame = getObjectFrame(currentFrame);
473        while (frame != null) {
474            if (className.equals(frame.getFrameName())) {
475                final DetailAST field = frame.findField(name);
476                result = STRING.equals(getFieldType(field));
477                break;
478            }
479            frame = getObjectFrame(frame.getParent());
480        }
481        return result;
482    }
483
484    /**
485     * Get the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF.
486     *
487     * @param frame to start the search from.
488     * @return the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF.
489     */
490    private static FieldFrame getObjectFrame(FieldFrame frame) {
491        FieldFrame objectFrame = frame;
492        while (objectFrame != null && !objectFrame.isClassOrEnumOrEnumConstDef()) {
493            objectFrame = objectFrame.getParent();
494        }
495        return objectFrame;
496    }
497
498    /**
499     * Check whether the field is declared before the method call in case of
500     * methods and initialization blocks.
501     *
502     * @param field field to check.
503     * @param objCalledOn object equals method called on.
504     * @return true if the field is declared before the method call.
505     */
506    private static boolean checkLineNo(DetailAST field, DetailAST objCalledOn) {
507        boolean result = false;
508        if (CheckUtil.isBeforeInSource(field, objCalledOn)) {
509            result = true;
510        }
511        return result;
512    }
513
514    /**
515     * Get field type.
516     *
517     * @param field to get the type from.
518     * @return type of the field.
519     */
520    private static String getFieldType(DetailAST field) {
521        String fieldType = null;
522        final DetailAST identAst = field.findFirstToken(TokenTypes.TYPE)
523                .findFirstToken(TokenTypes.IDENT);
524        if (identAst != null) {
525            fieldType = identAst.getText();
526        }
527        return fieldType;
528    }
529
530    /**
531     * Holds the names of fields of a type.
532     */
533    private static class FieldFrame {
534
535        /** Parent frame. */
536        private final FieldFrame parent;
537
538        /** Set of frame's children. */
539        private final Set<FieldFrame> children = new HashSet<>();
540
541        /** Set of fields. */
542        private final Set<DetailAST> fields = new HashSet<>();
543
544        /** Set of equals calls. */
545        private final Set<DetailAST> methodCalls = new HashSet<>();
546
547        /** Name of the class, enum or enum constant declaration. */
548        private String frameName;
549
550        /** Whether the frame is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF. */
551        private boolean classOrEnumOrEnumConstDef;
552
553        /**
554         * Creates new frame.
555         *
556         * @param parent parent frame.
557         */
558        /* package */ FieldFrame(FieldFrame parent) {
559            this.parent = parent;
560        }
561
562        /**
563         * Set the frame name.
564         *
565         * @param frameName value to set.
566         */
567        public void setFrameName(String frameName) {
568            this.frameName = frameName;
569        }
570
571        /**
572         * Getter for the frame name.
573         *
574         * @return frame name.
575         */
576        public String getFrameName() {
577            return frameName;
578        }
579
580        /**
581         * Getter for the parent frame.
582         *
583         * @return parent frame.
584         */
585        public FieldFrame getParent() {
586            return parent;
587        }
588
589        /**
590         * Getter for frame's children.
591         *
592         * @return children of this frame.
593         */
594        public Set<FieldFrame> getChildren() {
595            return Collections.unmodifiableSet(children);
596        }
597
598        /**
599         * Add child frame to this frame.
600         *
601         * @param child frame to add.
602         */
603        public void addChild(FieldFrame child) {
604            children.add(child);
605        }
606
607        /**
608         * Add field to this FieldFrame.
609         *
610         * @param field the ast of the field.
611         */
612        public void addField(DetailAST field) {
613            if (field.findFirstToken(TokenTypes.IDENT) != null) {
614                fields.add(field);
615            }
616        }
617
618        /**
619         * Sets isClassOrEnum.
620         *
621         * @param value value to set.
622         */
623        public void setClassOrEnumOrEnumConstDef(boolean value) {
624            classOrEnumOrEnumConstDef = value;
625        }
626
627        /**
628         * Getter for classOrEnumOrEnumConstDef.
629         *
630         * @return classOrEnumOrEnumConstDef.
631         */
632        public boolean isClassOrEnumOrEnumConstDef() {
633            return classOrEnumOrEnumConstDef;
634        }
635
636        /**
637         * Add method call to this frame.
638         *
639         * @param methodCall METHOD_CALL ast.
640         */
641        public void addMethodCall(DetailAST methodCall) {
642            methodCalls.add(methodCall);
643        }
644
645        /**
646         * Determines whether this FieldFrame contains the field.
647         *
648         * @param name name of the field to check.
649         * @return true if this FieldFrame contains instance field field.
650         */
651        public DetailAST findField(String name) {
652            DetailAST resultField = null;
653            for (DetailAST field: fields) {
654                if (getFieldName(field).equals(name)) {
655                    resultField = field;
656                    break;
657                }
658            }
659            return resultField;
660        }
661
662        /**
663         * Getter for frame's method calls.
664         *
665         * @return method calls of this frame.
666         */
667        public Set<DetailAST> getMethodCalls() {
668            return Collections.unmodifiableSet(methodCalls);
669        }
670
671        /**
672         * Get the name of the field.
673         *
674         * @param field to get the name from.
675         * @return name of the field.
676         */
677        private static String getFieldName(DetailAST field) {
678            return field.findFirstToken(TokenTypes.IDENT).getText();
679        }
680
681    }
682
683}