| //////////////////////////////////////////////////////////////////////////////// |
| // checkstyle: Checks Java source code for adherence to a set of rules. |
| // Copyright (C) 2001-2017 the original author or authors. |
| // |
| // This library is free software; you can redistribute it and/or |
| // modify it under the terms of the GNU Lesser General Public |
| // License as published by the Free Software Foundation; either |
| // version 2.1 of the License, or (at your option) any later version. |
| // |
| // This library is distributed in the hope that it will be useful, |
| // but WITHOUT ANY WARRANTY; without even the implied warranty of |
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| // Lesser General Public License for more details. |
| // |
| // You should have received a copy of the GNU Lesser General Public |
| // License along with this library; if not, write to the Free Software |
| // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| package com.puppycrawl.tools.checkstyle.checks.coding; |
| |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Set; |
| |
| import com.puppycrawl.tools.checkstyle.FileStatefulCheck; |
| import com.puppycrawl.tools.checkstyle.api.AbstractCheck; |
| import com.puppycrawl.tools.checkstyle.api.DetailAST; |
| import com.puppycrawl.tools.checkstyle.api.TokenTypes; |
| |
| /** |
| * Checks that any combination of String literals |
| * is on the left side of an equals() comparison. |
| * Also checks for String literals assigned to some field |
| * (such as {@code someString.equals(anotherString = "text")}). |
| * |
| * <p>Rationale: Calling the equals() method on String literals |
| * will avoid a potential NullPointerException. Also, it is |
| * pretty common to see null check right before equals comparisons |
| * which is not necessary in the below example. |
| * |
| * <p>For example: |
| * |
| * <pre> |
| * {@code |
| * String nullString = null; |
| * nullString.equals("My_Sweet_String"); |
| * } |
| * </pre> |
| * should be refactored to |
| * |
| * <pre> |
| * {@code |
| * String nullString = null; |
| * "My_Sweet_String".equals(nullString); |
| * } |
| * </pre> |
| * |
| * @author Travis Schneeberger |
| * @author Vladislav Lisetskiy |
| */ |
| @FileStatefulCheck |
| public class EqualsAvoidNullCheck extends AbstractCheck { |
| |
| /** |
| * A key is pointing to the warning message text in "messages.properties" |
| * file. |
| */ |
| public static final String MSG_EQUALS_AVOID_NULL = "equals.avoid.null"; |
| |
| /** |
| * A key is pointing to the warning message text in "messages.properties" |
| * file. |
| */ |
| public static final String MSG_EQUALS_IGNORE_CASE_AVOID_NULL = "equalsIgnoreCase.avoid.null"; |
| |
| /** Method name for comparison. */ |
| private static final String EQUALS = "equals"; |
| |
| /** Type name for comparison. */ |
| private static final String STRING = "String"; |
| |
| /** Whether to process equalsIgnoreCase() invocations. */ |
| private boolean ignoreEqualsIgnoreCase; |
| |
| /** Stack of sets of field names, one for each class of a set of nested classes. */ |
| private FieldFrame currentFrame; |
| |
| @Override |
| public int[] getDefaultTokens() { |
| return getRequiredTokens(); |
| } |
| |
| @Override |
| public int[] getAcceptableTokens() { |
| return getRequiredTokens(); |
| } |
| |
| @Override |
| public int[] getRequiredTokens() { |
| return new int[] { |
| TokenTypes.METHOD_CALL, |
| TokenTypes.CLASS_DEF, |
| TokenTypes.METHOD_DEF, |
| TokenTypes.LITERAL_IF, |
| TokenTypes.LITERAL_FOR, |
| TokenTypes.LITERAL_WHILE, |
| TokenTypes.LITERAL_DO, |
| TokenTypes.LITERAL_CATCH, |
| TokenTypes.LITERAL_TRY, |
| TokenTypes.VARIABLE_DEF, |
| TokenTypes.PARAMETER_DEF, |
| TokenTypes.CTOR_DEF, |
| TokenTypes.SLIST, |
| TokenTypes.ENUM_DEF, |
| TokenTypes.ENUM_CONSTANT_DEF, |
| TokenTypes.LITERAL_NEW, |
| }; |
| } |
| |
| /** |
| * Whether to ignore checking {@code String.equalsIgnoreCase(String)}. |
| * @param newValue whether to ignore checking |
| * {@code String.equalsIgnoreCase(String)}. |
| */ |
| public void setIgnoreEqualsIgnoreCase(boolean newValue) { |
| ignoreEqualsIgnoreCase = newValue; |
| } |
| |
| @Override |
| public void beginTree(DetailAST rootAST) { |
| currentFrame = new FieldFrame(null); |
| } |
| |
| @Override |
| public void visitToken(final DetailAST ast) { |
| switch (ast.getType()) { |
| case TokenTypes.VARIABLE_DEF: |
| case TokenTypes.PARAMETER_DEF: |
| currentFrame.addField(ast); |
| break; |
| case TokenTypes.METHOD_CALL: |
| processMethodCall(ast); |
| break; |
| case TokenTypes.SLIST: |
| processSlist(ast); |
| break; |
| case TokenTypes.LITERAL_NEW: |
| processLiteralNew(ast); |
| break; |
| default: |
| processFrame(ast); |
| } |
| } |
| |
| @Override |
| public void leaveToken(DetailAST ast) { |
| final int astType = ast.getType(); |
| if (astType != TokenTypes.VARIABLE_DEF |
| && astType != TokenTypes.PARAMETER_DEF |
| && astType != TokenTypes.METHOD_CALL |
| && astType != TokenTypes.SLIST |
| && astType != TokenTypes.LITERAL_NEW |
| || astType == TokenTypes.LITERAL_NEW |
| && ast.findFirstToken(TokenTypes.OBJBLOCK) != null) { |
| currentFrame = currentFrame.getParent(); |
| } |
| else if (astType == TokenTypes.SLIST) { |
| leaveSlist(ast); |
| } |
| } |
| |
| @Override |
| public void finishTree(DetailAST ast) { |
| traverseFieldFrameTree(currentFrame); |
| } |
| |
| /** |
| * Determine whether SLIST begins static or non-static block and add it as |
| * a frame in this case. |
| * @param ast SLIST ast. |
| */ |
| private void processSlist(DetailAST ast) { |
| final int parentType = ast.getParent().getType(); |
| if (parentType == TokenTypes.SLIST |
| || parentType == TokenTypes.STATIC_INIT |
| || parentType == TokenTypes.INSTANCE_INIT) { |
| final FieldFrame frame = new FieldFrame(currentFrame); |
| currentFrame.addChild(frame); |
| currentFrame = frame; |
| } |
| } |
| |
| /** |
| * Determine whether SLIST begins static or non-static block. |
| * @param ast SLIST ast. |
| */ |
| private void leaveSlist(DetailAST ast) { |
| final int parentType = ast.getParent().getType(); |
| if (parentType == TokenTypes.SLIST |
| || parentType == TokenTypes.STATIC_INIT |
| || parentType == TokenTypes.INSTANCE_INIT) { |
| currentFrame = currentFrame.getParent(); |
| } |
| } |
| |
| /** |
| * Process CLASS_DEF, METHOD_DEF, LITERAL_IF, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO, |
| * LITERAL_CATCH, LITERAL_TRY, CTOR_DEF, ENUM_DEF, ENUM_CONSTANT_DEF. |
| * @param ast processed ast. |
| */ |
| private void processFrame(DetailAST ast) { |
| final FieldFrame frame = new FieldFrame(currentFrame); |
| final int astType = ast.getType(); |
| if (astType == TokenTypes.CLASS_DEF |
| || astType == TokenTypes.ENUM_DEF |
| || astType == TokenTypes.ENUM_CONSTANT_DEF) { |
| frame.setClassOrEnumOrEnumConstDef(true); |
| frame.setFrameName(ast.findFirstToken(TokenTypes.IDENT).getText()); |
| } |
| currentFrame.addChild(frame); |
| currentFrame = frame; |
| } |
| |
| /** |
| * Add the method call to the current frame if it should be processed. |
| * @param methodCall METHOD_CALL ast. |
| */ |
| private void processMethodCall(DetailAST methodCall) { |
| final DetailAST dot = methodCall.getFirstChild(); |
| if (dot.getType() == TokenTypes.DOT) { |
| final String methodName = dot.getLastChild().getText(); |
| if (EQUALS.equals(methodName) |
| || !ignoreEqualsIgnoreCase && "equalsIgnoreCase".equals(methodName)) { |
| currentFrame.addMethodCall(methodCall); |
| } |
| } |
| } |
| |
| /** |
| * Determine whether LITERAL_NEW is an anonymous class definition and add it as |
| * a frame in this case. |
| * @param ast LITERAL_NEW ast. |
| */ |
| private void processLiteralNew(DetailAST ast) { |
| if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) { |
| final FieldFrame frame = new FieldFrame(currentFrame); |
| currentFrame.addChild(frame); |
| currentFrame = frame; |
| } |
| } |
| |
| /** |
| * Traverse the tree of the field frames to check all equals method calls. |
| * @param frame to check method calls in. |
| */ |
| private void traverseFieldFrameTree(FieldFrame frame) { |
| for (FieldFrame child: frame.getChildren()) { |
| if (!child.getChildren().isEmpty()) { |
| traverseFieldFrameTree(child); |
| } |
| currentFrame = child; |
| child.getMethodCalls().forEach(this::checkMethodCall); |
| } |
| } |
| |
| /** |
| * Check whether the method call should be violated. |
| * @param methodCall method call to check. |
| */ |
| private void checkMethodCall(DetailAST methodCall) { |
| DetailAST objCalledOn = methodCall.getFirstChild().getFirstChild(); |
| if (objCalledOn.getType() == TokenTypes.DOT) { |
| objCalledOn = objCalledOn.getLastChild(); |
| } |
| final DetailAST expr = methodCall.findFirstToken(TokenTypes.ELIST).getFirstChild(); |
| if (isObjectValid(objCalledOn) |
| && containsOneArgument(methodCall) |
| && containsAllSafeTokens(expr) |
| && isCalledOnStringFieldOrVariable(objCalledOn)) { |
| final String methodName = methodCall.getFirstChild().getLastChild().getText(); |
| if (EQUALS.equals(methodName)) { |
| log(methodCall.getLineNo(), methodCall.getColumnNo(), |
| MSG_EQUALS_AVOID_NULL); |
| } |
| else { |
| log(methodCall.getLineNo(), methodCall.getColumnNo(), |
| MSG_EQUALS_IGNORE_CASE_AVOID_NULL); |
| } |
| } |
| } |
| |
| /** |
| * Check whether the object equals method is called on is not a String literal |
| * and not too complex. |
| * @param objCalledOn the object equals method is called on ast. |
| * @return true if the object is valid. |
| */ |
| private static boolean isObjectValid(DetailAST objCalledOn) { |
| boolean result = true; |
| final DetailAST previousSibling = objCalledOn.getPreviousSibling(); |
| if (previousSibling != null |
| && previousSibling.getType() == TokenTypes.DOT) { |
| result = false; |
| } |
| if (isStringLiteral(objCalledOn)) { |
| result = false; |
| } |
| return result; |
| } |
| |
| /** |
| * Checks for calling equals on String literal and |
| * anon object which cannot be null. |
| * @param objCalledOn object AST |
| * @return if it is string literal |
| */ |
| private static boolean isStringLiteral(DetailAST objCalledOn) { |
| return objCalledOn.getType() == TokenTypes.STRING_LITERAL |
| || objCalledOn.getType() == TokenTypes.LITERAL_NEW; |
| } |
| |
| /** |
| * Verify that method call has one argument. |
| * |
| * @param methodCall METHOD_CALL DetailAST |
| * @return true if method call has one argument. |
| */ |
| private static boolean containsOneArgument(DetailAST methodCall) { |
| final DetailAST elist = methodCall.findFirstToken(TokenTypes.ELIST); |
| return elist.getChildCount() == 1; |
| } |
| |
| /** |
| * Looks for all "safe" Token combinations in the argument |
| * expression branch. |
| * @param expr the argument expression |
| * @return - true if any child matches the set of tokens, false if not |
| */ |
| private static boolean containsAllSafeTokens(final DetailAST expr) { |
| DetailAST arg = expr.getFirstChild(); |
| arg = skipVariableAssign(arg); |
| |
| boolean argIsNotNull = false; |
| if (arg.getType() == TokenTypes.PLUS) { |
| DetailAST child = arg.getFirstChild(); |
| while (child != null |
| && !argIsNotNull) { |
| argIsNotNull = child.getType() == TokenTypes.STRING_LITERAL |
| || child.getType() == TokenTypes.IDENT; |
| child = child.getNextSibling(); |
| } |
| } |
| |
| return argIsNotNull |
| || !arg.branchContains(TokenTypes.IDENT) |
| && !arg.branchContains(TokenTypes.LITERAL_NULL); |
| } |
| |
| /** |
| * Skips over an inner assign portion of an argument expression. |
| * @param currentAST current token in the argument expression |
| * @return the next relevant token |
| */ |
| private static DetailAST skipVariableAssign(final DetailAST currentAST) { |
| DetailAST result = currentAST; |
| if (currentAST.getType() == TokenTypes.ASSIGN |
| && currentAST.getFirstChild().getType() == TokenTypes.IDENT) { |
| result = currentAST.getFirstChild().getNextSibling(); |
| } |
| return result; |
| } |
| |
| /** |
| * Determine, whether equals method is called on a field of String type. |
| * @param objCalledOn object ast. |
| * @return true if the object is of String type. |
| */ |
| private boolean isCalledOnStringFieldOrVariable(DetailAST objCalledOn) { |
| final boolean result; |
| final DetailAST previousSiblingAst = objCalledOn.getPreviousSibling(); |
| if (previousSiblingAst == null) { |
| result = isStringFieldOrVariable(objCalledOn); |
| } |
| else { |
| if (previousSiblingAst.getType() == TokenTypes.LITERAL_THIS) { |
| result = isStringFieldOrVariableFromThisInstance(objCalledOn); |
| } |
| else { |
| final String className = previousSiblingAst.getText(); |
| result = isStringFieldOrVariableFromClass(objCalledOn, className); |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Whether the field or the variable is of String type. |
| * @param objCalledOn the field or the variable to check. |
| * @return true if the field or the variable is of String type. |
| */ |
| private boolean isStringFieldOrVariable(DetailAST objCalledOn) { |
| boolean result = false; |
| final String name = objCalledOn.getText(); |
| FieldFrame frame = currentFrame; |
| while (frame != null) { |
| final DetailAST field = frame.findField(name); |
| if (field != null |
| && (frame.isClassOrEnumOrEnumConstDef() |
| || checkLineNo(field, objCalledOn))) { |
| result = STRING.equals(getFieldType(field)); |
| break; |
| } |
| frame = frame.getParent(); |
| } |
| return result; |
| } |
| |
| /** |
| * Whether the field or the variable from THIS instance is of String type. |
| * @param objCalledOn the field or the variable from THIS instance to check. |
| * @return true if the field or the variable from THIS instance is of String type. |
| */ |
| private boolean isStringFieldOrVariableFromThisInstance(DetailAST objCalledOn) { |
| boolean result = false; |
| final String name = objCalledOn.getText(); |
| final DetailAST field = getObjectFrame(currentFrame).findField(name); |
| if (field != null) { |
| result = STRING.equals(getFieldType(field)); |
| } |
| return result; |
| } |
| |
| /** |
| * Whether the field or the variable from the specified class is of String type. |
| * @param objCalledOn the field or the variable from the specified class to check. |
| * @param className the name of the class to check in. |
| * @return true if the field or the variable from the specified class is of String type. |
| */ |
| private boolean isStringFieldOrVariableFromClass(DetailAST objCalledOn, |
| final String className) { |
| boolean result = false; |
| final String name = objCalledOn.getText(); |
| FieldFrame frame = getObjectFrame(currentFrame); |
| while (frame != null) { |
| if (className.equals(frame.getFrameName())) { |
| final DetailAST field = frame.findField(name); |
| if (field != null) { |
| result = STRING.equals(getFieldType(field)); |
| } |
| break; |
| } |
| frame = getObjectFrame(frame.getParent()); |
| } |
| return result; |
| } |
| |
| /** |
| * Get the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF. |
| * @param frame to start the search from. |
| * @return the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF. |
| */ |
| private static FieldFrame getObjectFrame(FieldFrame frame) { |
| FieldFrame objectFrame = frame; |
| while (objectFrame != null && !objectFrame.isClassOrEnumOrEnumConstDef()) { |
| objectFrame = objectFrame.getParent(); |
| } |
| return objectFrame; |
| } |
| |
| /** |
| * Check whether the field is declared before the method call in case of |
| * methods and initialization blocks. |
| * @param field field to check. |
| * @param objCalledOn object equals method called on. |
| * @return true if the field is declared before the method call. |
| */ |
| private static boolean checkLineNo(DetailAST field, DetailAST objCalledOn) { |
| boolean result = false; |
| // Required for pitest coverage. We should specify columnNo passing condition |
| // in such a way, so that the minimal possible distance between field and |
| // objCalledOn will be the maximal condition to pass this check. |
| // The minimal distance between objCalledOn and field (of type String) initialization |
| // is calculated as follows: |
| // String(6) + space(1) + variableName(1) + assign(1) + |
| // anotherStringVariableName(1) + semicolon(1) = 11 |
| // Example: length of "String s=d;" is 11 symbols. |
| final int minimumSymbolsBetween = 11; |
| if (field.getLineNo() < objCalledOn.getLineNo() |
| || field.getLineNo() == objCalledOn.getLineNo() |
| && field.getColumnNo() + minimumSymbolsBetween <= objCalledOn.getColumnNo()) { |
| result = true; |
| } |
| return result; |
| } |
| |
| /** |
| * Get field type. |
| * @param field to get the type from. |
| * @return type of the field. |
| */ |
| private static String getFieldType(DetailAST field) { |
| String fieldType = null; |
| final DetailAST identAst = field.findFirstToken(TokenTypes.TYPE) |
| .findFirstToken(TokenTypes.IDENT); |
| if (identAst != null) { |
| fieldType = identAst.getText(); |
| } |
| return fieldType; |
| } |
| |
| /** |
| * Holds the names of fields of a type. |
| */ |
| private static class FieldFrame { |
| /** Parent frame. */ |
| private final FieldFrame parent; |
| |
| /** Set of frame's children. */ |
| private final Set<FieldFrame> children = new HashSet<>(); |
| |
| /** Set of fields. */ |
| private final Set<DetailAST> fields = new HashSet<>(); |
| |
| /** Set of equals calls. */ |
| private final Set<DetailAST> methodCalls = new HashSet<>(); |
| |
| /** Name of the class, enum or enum constant declaration. */ |
| private String frameName; |
| |
| /** Whether the frame is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF. */ |
| private boolean classOrEnumOrEnumConstDef; |
| |
| /** |
| * Creates new frame. |
| * @param parent parent frame. |
| */ |
| FieldFrame(FieldFrame parent) { |
| this.parent = parent; |
| } |
| |
| /** |
| * Set the frame name. |
| * @param frameName value to set. |
| */ |
| public void setFrameName(String frameName) { |
| this.frameName = frameName; |
| } |
| |
| /** |
| * Getter for the frame name. |
| * @return frame name. |
| */ |
| public String getFrameName() { |
| return frameName; |
| } |
| |
| /** |
| * Getter for the parent frame. |
| * @return parent frame. |
| */ |
| public FieldFrame getParent() { |
| return parent; |
| } |
| |
| /** |
| * Getter for frame's children. |
| * @return children of this frame. |
| */ |
| public Set<FieldFrame> getChildren() { |
| return Collections.unmodifiableSet(children); |
| } |
| |
| /** |
| * Add child frame to this frame. |
| * @param child frame to add. |
| */ |
| public void addChild(FieldFrame child) { |
| children.add(child); |
| } |
| |
| /** |
| * Add field to this FieldFrame. |
| * @param field the ast of the field. |
| */ |
| public void addField(DetailAST field) { |
| fields.add(field); |
| } |
| |
| /** |
| * Sets isClassOrEnum. |
| * @param value value to set. |
| */ |
| public void setClassOrEnumOrEnumConstDef(boolean value) { |
| classOrEnumOrEnumConstDef = value; |
| } |
| |
| /** |
| * Getter for classOrEnumOrEnumConstDef. |
| * @return classOrEnumOrEnumConstDef. |
| */ |
| public boolean isClassOrEnumOrEnumConstDef() { |
| return classOrEnumOrEnumConstDef; |
| } |
| |
| /** |
| * Add method call to this frame. |
| * @param methodCall METHOD_CALL ast. |
| */ |
| public void addMethodCall(DetailAST methodCall) { |
| methodCalls.add(methodCall); |
| } |
| |
| /** |
| * Determines whether this FieldFrame contains the field. |
| * @param name name of the field to check. |
| * @return true if this FieldFrame contains instance field field. |
| */ |
| public DetailAST findField(String name) { |
| DetailAST resultField = null; |
| for (DetailAST field: fields) { |
| if (getFieldName(field).equals(name)) { |
| resultField = field; |
| break; |
| } |
| } |
| return resultField; |
| } |
| |
| /** |
| * Getter for frame's method calls. |
| * @return method calls of this frame. |
| */ |
| public Set<DetailAST> getMethodCalls() { |
| return Collections.unmodifiableSet(methodCalls); |
| } |
| |
| /** |
| * Get the name of the field. |
| * @param field to get the name from. |
| * @return name of the field. |
| */ |
| private static String getFieldName(DetailAST field) { |
| return field.findFirstToken(TokenTypes.IDENT).getText(); |
| } |
| } |
| } |