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        };
149    }
150
151    /**
152     * Setter to control whether to ignore {@code String.equalsIgnoreCase(String)} invocations.
153     *
154     * @param newValue whether to ignore checking
155     *    {@code String.equalsIgnoreCase(String)}.
156     */
157    public void setIgnoreEqualsIgnoreCase(boolean newValue) {
158        ignoreEqualsIgnoreCase = newValue;
159    }
160
161    @Override
162    public void beginTree(DetailAST rootAST) {
163        currentFrame = new FieldFrame(null);
164    }
165
166    @Override
167    public void visitToken(final DetailAST ast) {
168        switch (ast.getType()) {
169            case TokenTypes.VARIABLE_DEF:
170            case TokenTypes.PARAMETER_DEF:
171                currentFrame.addField(ast);
172                break;
173            case TokenTypes.METHOD_CALL:
174                processMethodCall(ast);
175                break;
176            case TokenTypes.SLIST:
177                processSlist(ast);
178                break;
179            case TokenTypes.LITERAL_NEW:
180                processLiteralNew(ast);
181                break;
182            case TokenTypes.OBJBLOCK:
183                final int parentType = ast.getParent().getType();
184                if (parentType != TokenTypes.CLASS_DEF && parentType != TokenTypes.ENUM_DEF) {
185                    processFrame(ast);
186                }
187                break;
188            default:
189                processFrame(ast);
190        }
191    }
192
193    @Override
194    public void leaveToken(DetailAST ast) {
195        final int astType = ast.getType();
196        if (astType == TokenTypes.SLIST) {
197            leaveSlist(ast);
198        }
199        else if (astType == TokenTypes.LITERAL_NEW) {
200            leaveLiteralNew(ast);
201        }
202        else if (astType == TokenTypes.OBJBLOCK) {
203            final int parentType = ast.getParent().getType();
204            if (parentType != TokenTypes.CLASS_DEF && parentType != TokenTypes.ENUM_DEF) {
205                currentFrame = currentFrame.getParent();
206            }
207        }
208        else if (astType != TokenTypes.VARIABLE_DEF
209                && astType != TokenTypes.PARAMETER_DEF
210                && astType != TokenTypes.METHOD_CALL) {
211            currentFrame = currentFrame.getParent();
212        }
213    }
214
215    @Override
216    public void finishTree(DetailAST ast) {
217        traverseFieldFrameTree(currentFrame);
218    }
219
220    /**
221     * Determine whether SLIST begins a block, determined by braces, and add it as
222     * a frame in this case.
223     *
224     * @param ast SLIST ast.
225     */
226    private void processSlist(DetailAST ast) {
227        if (LEFT_CURLY.equals(ast.getText())) {
228            final FieldFrame frame = new FieldFrame(currentFrame);
229            currentFrame.addChild(frame);
230            currentFrame = frame;
231        }
232    }
233
234    /**
235     * Determine whether SLIST begins a block, determined by braces.
236     *
237     * @param ast SLIST ast.
238     */
239    private void leaveSlist(DetailAST ast) {
240        if (LEFT_CURLY.equals(ast.getText())) {
241            currentFrame = currentFrame.getParent();
242        }
243    }
244
245    /**
246     * Process CLASS_DEF, METHOD_DEF, LITERAL_IF, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO,
247     * LITERAL_CATCH, LITERAL_TRY, CTOR_DEF, ENUM_DEF, ENUM_CONSTANT_DEF.
248     *
249     * @param ast processed ast.
250     */
251    private void processFrame(DetailAST ast) {
252        final FieldFrame frame = new FieldFrame(currentFrame);
253        final int astType = ast.getType();
254        if (astType == TokenTypes.CLASS_DEF
255                || astType == TokenTypes.ENUM_DEF) {
256            frame.setClassOrEnumOrEnumConstDef(true);
257            frame.setFrameName(ast.findFirstToken(TokenTypes.IDENT).getText());
258        }
259        currentFrame.addChild(frame);
260        currentFrame = frame;
261    }
262
263    /**
264     * Add the method call to the current frame if it should be processed.
265     *
266     * @param methodCall METHOD_CALL ast.
267     */
268    private void processMethodCall(DetailAST methodCall) {
269        final DetailAST dot = methodCall.getFirstChild();
270        if (dot.getType() == TokenTypes.DOT) {
271            final String methodName = dot.getLastChild().getText();
272            if (EQUALS.equals(methodName)
273                    || !ignoreEqualsIgnoreCase && "equalsIgnoreCase".equals(methodName)) {
274                currentFrame.addMethodCall(methodCall);
275            }
276        }
277    }
278
279    /**
280     * Determine whether LITERAL_NEW is an anonymous class definition and add it as
281     * a frame in this case.
282     *
283     * @param ast LITERAL_NEW ast.
284     */
285    private void processLiteralNew(DetailAST ast) {
286        if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
287            final FieldFrame frame = new FieldFrame(currentFrame);
288            currentFrame.addChild(frame);
289            currentFrame = frame;
290        }
291    }
292
293    /**
294     * Determine whether LITERAL_NEW is an anonymous class definition and leave
295     * the frame it is in.
296     *
297     * @param ast LITERAL_NEW ast.
298     */
299    private void leaveLiteralNew(DetailAST ast) {
300        if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
301            currentFrame = currentFrame.getParent();
302        }
303    }
304
305    /**
306     * Traverse the tree of the field frames to check all equals method calls.
307     *
308     * @param frame to check method calls in.
309     */
310    private void traverseFieldFrameTree(FieldFrame frame) {
311        for (FieldFrame child: frame.getChildren()) {
312            traverseFieldFrameTree(child);
313
314            currentFrame = child;
315            child.getMethodCalls().forEach(this::checkMethodCall);
316        }
317    }
318
319    /**
320     * Check whether the method call should be violated.
321     *
322     * @param methodCall method call to check.
323     */
324    private void checkMethodCall(DetailAST methodCall) {
325        DetailAST objCalledOn = methodCall.getFirstChild().getFirstChild();
326        if (objCalledOn.getType() == TokenTypes.DOT) {
327            objCalledOn = objCalledOn.getLastChild();
328        }
329        final DetailAST expr = methodCall.findFirstToken(TokenTypes.ELIST).getFirstChild();
330        if (containsOneArgument(methodCall)
331                && containsAllSafeTokens(expr)
332                && isCalledOnStringFieldOrVariable(objCalledOn)) {
333            final String methodName = methodCall.getFirstChild().getLastChild().getText();
334            if (EQUALS.equals(methodName)) {
335                log(methodCall, MSG_EQUALS_AVOID_NULL);
336            }
337            else {
338                log(methodCall, MSG_EQUALS_IGNORE_CASE_AVOID_NULL);
339            }
340        }
341    }
342
343    /**
344     * Verify that method call has one argument.
345     *
346     * @param methodCall METHOD_CALL DetailAST
347     * @return true if method call has one argument.
348     */
349    private static boolean containsOneArgument(DetailAST methodCall) {
350        final DetailAST elist = methodCall.findFirstToken(TokenTypes.ELIST);
351        return elist.getChildCount() == 1;
352    }
353
354    /**
355     * Looks for all "safe" Token combinations in the argument
356     * expression branch.
357     *
358     * @param expr the argument expression
359     * @return - true if any child matches the set of tokens, false if not
360     */
361    private static boolean containsAllSafeTokens(final DetailAST expr) {
362        DetailAST arg = expr.getFirstChild();
363        arg = skipVariableAssign(arg);
364
365        boolean argIsNotNull = false;
366        if (arg.getType() == TokenTypes.PLUS) {
367            DetailAST child = arg.getFirstChild();
368            while (child != null
369                    && !argIsNotNull) {
370                argIsNotNull = child.getType() == TokenTypes.STRING_LITERAL
371                        || child.getType() == TokenTypes.IDENT;
372                child = child.getNextSibling();
373            }
374        }
375        else {
376            argIsNotNull = arg.getType() == TokenTypes.STRING_LITERAL;
377        }
378
379        return argIsNotNull;
380    }
381
382    /**
383     * Skips over an inner assign portion of an argument expression.
384     *
385     * @param currentAST current token in the argument expression
386     * @return the next relevant token
387     */
388    private static DetailAST skipVariableAssign(final DetailAST currentAST) {
389        DetailAST result = currentAST;
390        while (result.getType() == TokenTypes.LPAREN) {
391            result = result.getNextSibling();
392        }
393        if (result.getType() == TokenTypes.ASSIGN) {
394            result = result.getFirstChild().getNextSibling();
395        }
396        return result;
397    }
398
399    /**
400     * Determine, whether equals method is called on a field of String type.
401     *
402     * @param objCalledOn object ast.
403     * @return true if the object is of String type.
404     */
405    private boolean isCalledOnStringFieldOrVariable(DetailAST objCalledOn) {
406        final boolean result;
407        final DetailAST previousSiblingAst = objCalledOn.getPreviousSibling();
408        if (previousSiblingAst == null) {
409            result = isStringFieldOrVariable(objCalledOn);
410        }
411        else {
412            if (previousSiblingAst.getType() == TokenTypes.LITERAL_THIS) {
413                result = isStringFieldOrVariableFromThisInstance(objCalledOn);
414            }
415            else {
416                final String className = previousSiblingAst.getText();
417                result = isStringFieldOrVariableFromClass(objCalledOn, className);
418            }
419        }
420        return result;
421    }
422
423    /**
424     * Whether the field or the variable is of String type.
425     *
426     * @param objCalledOn the field or the variable to check.
427     * @return true if the field or the variable is of String type.
428     */
429    private boolean isStringFieldOrVariable(DetailAST objCalledOn) {
430        boolean result = false;
431        final String name = objCalledOn.getText();
432        FieldFrame frame = currentFrame;
433        while (frame != null) {
434            final DetailAST field = frame.findField(name);
435            if (field != null
436                    && (frame.isClassOrEnumOrEnumConstDef()
437                            || checkLineNo(field, objCalledOn))) {
438                result = STRING.equals(getFieldType(field));
439                break;
440            }
441            frame = frame.getParent();
442        }
443        return result;
444    }
445
446    /**
447     * Whether the field or the variable from THIS instance is of String type.
448     *
449     * @param objCalledOn the field or the variable from THIS instance to check.
450     * @return true if the field or the variable from THIS instance is of String type.
451     */
452    private boolean isStringFieldOrVariableFromThisInstance(DetailAST objCalledOn) {
453        final String name = objCalledOn.getText();
454        final DetailAST field = getObjectFrame(currentFrame).findField(name);
455        return STRING.equals(getFieldType(field));
456    }
457
458    /**
459     * Whether the field or the variable from the specified class is of String type.
460     *
461     * @param objCalledOn the field or the variable from the specified class to check.
462     * @param className the name of the class to check in.
463     * @return true if the field or the variable from the specified class is of String type.
464     */
465    private boolean isStringFieldOrVariableFromClass(DetailAST objCalledOn,
466            final String className) {
467        boolean result = false;
468        final String name = objCalledOn.getText();
469        FieldFrame frame = getObjectFrame(currentFrame);
470        while (frame != null) {
471            if (className.equals(frame.getFrameName())) {
472                final DetailAST field = frame.findField(name);
473                result = STRING.equals(getFieldType(field));
474                break;
475            }
476            frame = getObjectFrame(frame.getParent());
477        }
478        return result;
479    }
480
481    /**
482     * Get the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF.
483     *
484     * @param frame to start the search from.
485     * @return the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF.
486     */
487    private static FieldFrame getObjectFrame(FieldFrame frame) {
488        FieldFrame objectFrame = frame;
489        while (objectFrame != null && !objectFrame.isClassOrEnumOrEnumConstDef()) {
490            objectFrame = objectFrame.getParent();
491        }
492        return objectFrame;
493    }
494
495    /**
496     * Check whether the field is declared before the method call in case of
497     * methods and initialization blocks.
498     *
499     * @param field field to check.
500     * @param objCalledOn object equals method called on.
501     * @return true if the field is declared before the method call.
502     */
503    private static boolean checkLineNo(DetailAST field, DetailAST objCalledOn) {
504        boolean result = false;
505        if (CheckUtil.isBeforeInSource(field, objCalledOn)) {
506            result = true;
507        }
508        return result;
509    }
510
511    /**
512     * Get field type.
513     *
514     * @param field to get the type from.
515     * @return type of the field.
516     */
517    private static String getFieldType(DetailAST field) {
518        String fieldType = null;
519        final DetailAST identAst = field.findFirstToken(TokenTypes.TYPE)
520                .findFirstToken(TokenTypes.IDENT);
521        if (identAst != null) {
522            fieldType = identAst.getText();
523        }
524        return fieldType;
525    }
526
527    /**
528     * Holds the names of fields of a type.
529     */
530    private static class FieldFrame {
531
532        /** Parent frame. */
533        private final FieldFrame parent;
534
535        /** Set of frame's children. */
536        private final Set<FieldFrame> children = new HashSet<>();
537
538        /** Set of fields. */
539        private final Set<DetailAST> fields = new HashSet<>();
540
541        /** Set of equals calls. */
542        private final Set<DetailAST> methodCalls = new HashSet<>();
543
544        /** Name of the class, enum or enum constant declaration. */
545        private String frameName;
546
547        /** Whether the frame is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF. */
548        private boolean classOrEnumOrEnumConstDef;
549
550        /**
551         * Creates new frame.
552         *
553         * @param parent parent frame.
554         */
555        /* package */ FieldFrame(FieldFrame parent) {
556            this.parent = parent;
557        }
558
559        /**
560         * Set the frame name.
561         *
562         * @param frameName value to set.
563         */
564        public void setFrameName(String frameName) {
565            this.frameName = frameName;
566        }
567
568        /**
569         * Getter for the frame name.
570         *
571         * @return frame name.
572         */
573        public String getFrameName() {
574            return frameName;
575        }
576
577        /**
578         * Getter for the parent frame.
579         *
580         * @return parent frame.
581         */
582        public FieldFrame getParent() {
583            return parent;
584        }
585
586        /**
587         * Getter for frame's children.
588         *
589         * @return children of this frame.
590         */
591        public Set<FieldFrame> getChildren() {
592            return Collections.unmodifiableSet(children);
593        }
594
595        /**
596         * Add child frame to this frame.
597         *
598         * @param child frame to add.
599         */
600        public void addChild(FieldFrame child) {
601            children.add(child);
602        }
603
604        /**
605         * Add field to this FieldFrame.
606         *
607         * @param field the ast of the field.
608         */
609        public void addField(DetailAST field) {
610            if (field.findFirstToken(TokenTypes.IDENT) != null) {
611                fields.add(field);
612            }
613        }
614
615        /**
616         * Sets isClassOrEnum.
617         *
618         * @param value value to set.
619         */
620        public void setClassOrEnumOrEnumConstDef(boolean value) {
621            classOrEnumOrEnumConstDef = value;
622        }
623
624        /**
625         * Getter for classOrEnumOrEnumConstDef.
626         *
627         * @return classOrEnumOrEnumConstDef.
628         */
629        public boolean isClassOrEnumOrEnumConstDef() {
630            return classOrEnumOrEnumConstDef;
631        }
632
633        /**
634         * Add method call to this frame.
635         *
636         * @param methodCall METHOD_CALL ast.
637         */
638        public void addMethodCall(DetailAST methodCall) {
639            methodCalls.add(methodCall);
640        }
641
642        /**
643         * Determines whether this FieldFrame contains the field.
644         *
645         * @param name name of the field to check.
646         * @return true if this FieldFrame contains instance field field.
647         */
648        public DetailAST findField(String name) {
649            DetailAST resultField = null;
650            for (DetailAST field: fields) {
651                if (getFieldName(field).equals(name)) {
652                    resultField = field;
653                    break;
654                }
655            }
656            return resultField;
657        }
658
659        /**
660         * Getter for frame's method calls.
661         *
662         * @return method calls of this frame.
663         */
664        public Set<DetailAST> getMethodCalls() {
665            return Collections.unmodifiableSet(methodCalls);
666        }
667
668        /**
669         * Get the name of the field.
670         *
671         * @param field to get the name from.
672         * @return name of the field.
673         */
674        private static String getFieldName(DetailAST field) {
675            return field.findFirstToken(TokenTypes.IDENT).getText();
676        }
677
678    }
679
680}