001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2014  Oliver Burn
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 antlr.collections.AST;
023import com.puppycrawl.tools.checkstyle.api.Check;
024import com.puppycrawl.tools.checkstyle.api.DetailAST;
025import com.puppycrawl.tools.checkstyle.api.TokenTypes;
026
027/**
028 * <p>
029 * Checks that any combination of String literals with optional
030 * assignment is on the left side of an equals() comparison.
031 * </p>
032 *
033 * <p>
034 * Rationale: Calling the equals() method on String literals
035 * will avoid a potential NullPointerException.  Also, it is
036 * pretty common to see null check right before equals comparisons
037 * which is not necessary in the below example.
038 *
039 * For example:
040 *
041 * <pre>
042 *  <code>
043 *    String nullString = null;
044 *    nullString.equals(&quot;My_Sweet_String&quot;);
045 *  </code>
046 * </pre>
047 * should be refactored to
048 *
049 * <pre>
050 *  <code>
051 *    String nullString = null;
052 *    &quot;My_Sweet_String&quot;.equals(nullString);
053 *  </code>
054 * </pre>
055 *
056 *
057 * <p>
058 * Limitations: If the equals method is overridden or
059 * a covariant equals method is defined and the implementation
060 * is incorrect (where s.equals(t) does not return the same result
061 * as t.equals(s)) then rearranging the called on object and
062 * parameter may have unexpected results
063 *
064 * <br>
065 *
066 * Java's Autoboxing feature has an affect
067 * on how this check is implemented. Pre Java 5 all IDENT + IDENT
068 * object concatenations would not cause a NullPointerException even
069 * if null.  Those situations could have been included in this check.
070 * They would simply act as if they surrounded by String.valueOf()
071 * which would concatenate the String null.
072 *
073 * <p>
074 * The following example will cause a
075 * NullPointerException as a result of what autoboxing does.
076 * <pre>
077 * Integer i = null, j = null;
078 * String number = "5"
079 * number.equals(i + j);
080 * </pre>
081 *
082 *
083 * Since, it is difficult to determine what kind of Object is being
084 * concatenated all ident concatenation is considered unsafe.
085 *
086 * @author Travis Schneeberger
087 * version 1.0
088 */
089public class EqualsAvoidNullCheck extends Check
090{
091    /** Whether to process equalsIgnoreCase() invocations. */
092    private boolean ignoreEqualsIgnoreCase;
093
094    @Override
095    public int[] getDefaultTokens()
096    {
097        return new int[] {TokenTypes.METHOD_CALL};
098    }
099
100    @Override
101    public void visitToken(final DetailAST methodCall)
102    {
103        final DetailAST dot = methodCall.getFirstChild();
104        if (dot.getType() != TokenTypes.DOT) {
105            return;
106        }
107
108        final DetailAST objCalledOn = dot.getFirstChild();
109
110        //checks for calling equals on String literal and
111        //anon object which cannot be null
112        //Also, checks if calling using strange inner class
113        //syntax outter.inner.equals(otherObj) by looking
114        //for the dot operator which cannot be improved
115        if ((objCalledOn.getType() == TokenTypes.STRING_LITERAL)
116                || (objCalledOn.getType() == TokenTypes.LITERAL_NEW)
117                || (objCalledOn.getType() == TokenTypes.DOT))
118        {
119            return;
120        }
121
122        final DetailAST method = objCalledOn.getNextSibling();
123        final DetailAST expr = dot.getNextSibling().getFirstChild();
124
125        if ("equals".equals(method.getText())
126            && containsOneArg(expr) && containsAllSafeTokens(expr))
127        {
128            log(methodCall.getLineNo(), methodCall.getColumnNo(),
129                "equals.avoid.null");
130        }
131
132        if (!ignoreEqualsIgnoreCase
133            && "equalsIgnoreCase".equals(method.getText())
134            && containsOneArg(expr) && containsAllSafeTokens(expr))
135        {
136            log(methodCall.getLineNo(), methodCall.getColumnNo(),
137                "equalsIgnoreCase.avoid.null");
138        }
139    }
140
141    /**
142     * Checks if a method contains no arguments
143     * starting at with the argument expression.
144     *
145     * @param expr the argument expression
146     * @return true if the method contains no args, false if not
147     */
148    private boolean containsNoArgs(final AST expr)
149    {
150        return (expr == null);
151    }
152
153    /**
154     * Checks if a method contains multiple arguments
155     * starting at with the argument expression.
156     *
157     * @param expr the argument expression
158     * @return true if the method contains multiple args, false if not
159     */
160    private boolean containsMultiArgs(final AST expr)
161    {
162        final AST comma = expr.getNextSibling();
163        return (comma != null) && (comma.getType() == TokenTypes.COMMA);
164    }
165
166    /**
167     * Checks if a method contains a single argument
168     * starting at with the argument expression.
169     *
170     * @param expr the argument expression
171     * @return true if the method contains a single arg, false if not
172     */
173    private boolean containsOneArg(final AST expr)
174    {
175        return !containsNoArgs(expr) && !containsMultiArgs(expr);
176    }
177
178    /**
179     * <p>
180     * Looks for all "safe" Token combinations in the argument
181     * expression branch.
182     * </p>
183     *
184     * <p>
185     * See class documentation for details on autoboxing's affect
186     * on this method implementation.
187     * </p>
188     *
189     * @param expr the argument expression
190     * @return - true if any child matches the set of tokens, false if not
191     */
192    private boolean containsAllSafeTokens(final DetailAST expr)
193    {
194        DetailAST arg = expr.getFirstChild();
195
196        if (arg.branchContains(TokenTypes.METHOD_CALL)) {
197            return false;
198        }
199        arg = skipVariableAssign(arg);
200
201        //Plus assignment can have ill affects
202        //do not want to recommend moving expression
203        //See example:
204        //String s = "SweetString";
205        //s.equals(s += "SweetString"); //false
206        //s = "SweetString";
207        //(s += "SweetString").equals(s); //true
208        //arg = skipVariablePlusAssign(arg);
209
210        if ((arg).branchContains(TokenTypes.PLUS_ASSIGN)
211                || (arg).branchContains(TokenTypes.IDENT))
212        {
213            return false;
214        }
215
216        //must be just String literals if got here
217        return true;
218    }
219
220    /**
221     * Skips over an inner assign portion of an argument expression.
222     * @param currentAST current token in the argument expression
223     * @return the next relevant token
224     */
225    private DetailAST skipVariableAssign(final DetailAST currentAST)
226    {
227        if ((currentAST.getType() == TokenTypes.ASSIGN)
228                && (currentAST.getFirstChild().getType() == TokenTypes.IDENT))
229        {
230            return currentAST.getFirstChild().getNextSibling();
231        }
232        return currentAST;
233    }
234
235    /**
236     * Whether to ignore checking {@code String.equalsIgnoreCase(String)}.
237     * @param newValue whether to ignore checking
238     *    {@code String.equalsIgnoreCase(String)}.
239     */
240    public void setIgnoreEqualsIgnoreCase(boolean newValue)
241    {
242        ignoreEqualsIgnoreCase = newValue;
243    }
244}