Coverage Report - com.puppycrawl.tools.checkstyle.checks.coding.EqualsAvoidNullCheck
 
Classes in this File Line Coverage Branch Coverage Complexity
EqualsAvoidNullCheck
100%
31/31
78%
33/42
3.875
 
 1  
 ////////////////////////////////////////////////////////////////////////////////
 2  
 // checkstyle: Checks Java source code for adherence to a set of rules.
 3  
 // Copyright (C) 2001-2014  Oliver Burn
 4  
 //
 5  
 // This library is free software; you can redistribute it and/or
 6  
 // modify it under the terms of the GNU Lesser General Public
 7  
 // License as published by the Free Software Foundation; either
 8  
 // version 2.1 of the License, or (at your option) any later version.
 9  
 //
 10  
 // This library is distributed in the hope that it will be useful,
 11  
 // but WITHOUT ANY WARRANTY; without even the implied warranty of
 12  
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 13  
 // Lesser General Public License for more details.
 14  
 //
 15  
 // You should have received a copy of the GNU Lesser General Public
 16  
 // License along with this library; if not, write to the Free Software
 17  
 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 18  
 ////////////////////////////////////////////////////////////////////////////////
 19  
 
 20  
 package com.puppycrawl.tools.checkstyle.checks.coding;
 21  
 
 22  
 import antlr.collections.AST;
 23  
 import com.puppycrawl.tools.checkstyle.api.Check;
 24  
 import com.puppycrawl.tools.checkstyle.api.DetailAST;
 25  
 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
 26  
 
 27  
 /**
 28  
  * <p>
 29  
  * Checks that any combination of String literals with optional
 30  
  * assignment is on the left side of an equals() comparison.
 31  
  * </p>
 32  
  *
 33  
  * <p>
 34  
  * Rationale: Calling the equals() method on String literals
 35  
  * will avoid a potential NullPointerException.  Also, it is
 36  
  * pretty common to see null check right before equals comparisons
 37  
  * which is not necessary in the below example.
 38  
  *
 39  
  * For example:
 40  
  *
 41  
  * <pre>
 42  
  *  <code>
 43  
  *    String nullString = null;
 44  
  *    nullString.equals(&quot;My_Sweet_String&quot;);
 45  
  *  </code>
 46  
  * </pre>
 47  
  * should be refactored to
 48  
  *
 49  
  * <pre>
 50  
  *  <code>
 51  
  *    String nullString = null;
 52  
  *    &quot;My_Sweet_String&quot;.equals(nullString);
 53  
  *  </code>
 54  
  * </pre>
 55  
  * </p>
 56  
  *
 57  
  * <p>
 58  
  * Limitations: If the equals method is overridden or
 59  
  * a covariant equals method is defined and the implementation
 60  
  * is incorrect (where s.equals(t) does not return the same result
 61  
  * as t.equals(s)) then rearranging the called on object and
 62  
  * parameter may have unexpected results
 63  
  *
 64  
  * <br/>
 65  
  *
 66  
  * Java's Autoboxing feature has an affect
 67  
  * on how this check is implemented. Pre Java 5 all IDENT + IDENT
 68  
  * object concatenations would not cause a NullPointerException even
 69  
  * if null.  Those situations could have been included in this check.
 70  
  * They would simply act as if they surrounded by String.valueOf()
 71  
  * which would concatenate the String null.
 72  
  * </p>
 73  
  * <p>
 74  
  * The following example will cause a
 75  
  * NullPointerException as a result of what autoboxing does.
 76  
  * <pre>
 77  
  * Integer i = null, j = null;
 78  
  * String number = "5"
 79  
  * number.equals(i + j);
 80  
  * </pre>
 81  
  * </p>
 82  
  *
 83  
  * Since, it is difficult to determine what kind of Object is being
 84  
  * concatenated all ident concatenation is considered unsafe.
 85  
  *
 86  
  * @author Travis Schneeberger
 87  
  * version 1.0
 88  
  */
 89  2
 public class EqualsAvoidNullCheck extends Check
 90  
 {
 91  
     /** Whether to process equalsIgnoreCase() invocations. */
 92  
     private boolean mIgnoreEqualsIgnoreCase;
 93  
 
 94  
     @Override
 95  
     public int[] getDefaultTokens()
 96  
     {
 97  2
         return new int[] {TokenTypes.METHOD_CALL};
 98  
     }
 99  
 
 100  
     @Override
 101  
     public void visitToken(final DetailAST aMethodCall)
 102  
     {
 103  132
         final DetailAST dot = aMethodCall.getFirstChild();
 104  132
         if (dot.getType() != TokenTypes.DOT) {
 105  12
             return;
 106  
         }
 107  
 
 108  120
         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  120
         if ((objCalledOn.getType() == TokenTypes.STRING_LITERAL)
 116  
                 || (objCalledOn.getType() == TokenTypes.LITERAL_NEW)
 117  
                 || (objCalledOn.getType() == TokenTypes.DOT))
 118  
         {
 119  20
             return;
 120  
         }
 121  
 
 122  100
         final DetailAST method = objCalledOn.getNextSibling();
 123  100
         final DetailAST expr = dot.getNextSibling().getFirstChild();
 124  
 
 125  100
         if ("equals".equals(method.getText())
 126  
             && containsOneArg(expr) && containsAllSafeTokens(expr))
 127  
         {
 128  24
             log(aMethodCall.getLineNo(), aMethodCall.getColumnNo(),
 129  
                 "equals.avoid.null");
 130  
         }
 131  
 
 132  100
         if (!mIgnoreEqualsIgnoreCase
 133  
             && "equalsIgnoreCase".equals(method.getText())
 134  
             && containsOneArg(expr) && containsAllSafeTokens(expr))
 135  
         {
 136  12
             log(aMethodCall.getLineNo(), aMethodCall.getColumnNo(),
 137  
                 "equalsIgnoreCase.avoid.null");
 138  
         }
 139  100
     }
 140  
 
 141  
     /**
 142  
      * Checks if a method contains no arguments
 143  
      * starting at with the argument expression.
 144  
      *
 145  
      * @param aExpr the argument expression
 146  
      * @return true if the method contains no args, false if not
 147  
      */
 148  
     private boolean containsNoArgs(final AST aExpr)
 149  
     {
 150  58
         return (aExpr == null);
 151  
     }
 152  
 
 153  
     /**
 154  
      * Checks if a method contains multiple arguments
 155  
      * starting at with the argument expression.
 156  
      *
 157  
      * @param aExpr the argument expression
 158  
      * @return true if the method contains multiple args, false if not
 159  
      */
 160  
     private boolean containsMultiArgs(final AST aExpr)
 161  
     {
 162  58
         final AST comma = aExpr.getNextSibling();
 163  58
         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 aExpr the argument expression
 171  
      * @return true if the method contains a single arg, false if not
 172  
      */
 173  
     private boolean containsOneArg(final AST aExpr)
 174  
     {
 175  58
         return !containsNoArgs(aExpr) && !containsMultiArgs(aExpr);
 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 aExpr the argument expression
 190  
      * @return - true if any child matches the set of tokens, false if not
 191  
      */
 192  
     private boolean containsAllSafeTokens(final DetailAST aExpr)
 193  
     {
 194  58
         DetailAST arg = aExpr.getFirstChild();
 195  
 
 196  58
         if (arg.branchContains(TokenTypes.METHOD_CALL)) {
 197  4
             return false;
 198  
         }
 199  54
         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  54
         if ((arg).branchContains(TokenTypes.PLUS_ASSIGN)
 211  
                 || (arg).branchContains(TokenTypes.IDENT))
 212  
         {
 213  18
             return false;
 214  
         }
 215  
 
 216  
         //must be just String literals if got here
 217  36
         return true;
 218  
     }
 219  
 
 220  
     /**
 221  
      * Skips over an inner assign portion of an argument expression.
 222  
      * @param aCurrentAST current token in the argument expression
 223  
      * @return the next relevant token
 224  
      */
 225  
     private DetailAST skipVariableAssign(final DetailAST aCurrentAST)
 226  
     {
 227  54
         if ((aCurrentAST.getType() == TokenTypes.ASSIGN)
 228  
                 && (aCurrentAST.getFirstChild().getType() == TokenTypes.IDENT))
 229  
         {
 230  6
             return aCurrentAST.getFirstChild().getNextSibling();
 231  
         }
 232  48
         return aCurrentAST;
 233  
     }
 234  
 
 235  
     /**
 236  
      * Whether to ignore checking {@code String.equalsIgnoreCase(String)}.
 237  
      * @param aNewValue whether to ignore checking
 238  
      *    {@code String.equalsIgnoreCase(String)}.
 239  
      */
 240  
     public void setIgnoreEqualsIgnoreCase(boolean aNewValue)
 241  
     {
 242  1
         mIgnoreEqualsIgnoreCase = aNewValue;
 243  1
     }
 244  
 }