diff options
| author | Fabrice Tiercelin | 2020-04-13 17:59:37 +0000 |
|---|---|---|
| committer | Fabrice Tiercelin | 2020-07-15 20:23:35 +0000 |
| commit | 258e2bf2d72ff5aa025b6350b9f0abbec3a3be13 (patch) | |
| tree | fe377b50d29dd5bc3e3c153aa39780c1b8746673 | |
| parent | 72fb4f4067a7501238b890f24ea476e3b0fdc2ea (diff) | |
| download | eclipse.jdt.ui-258e2bf2d72ff5aa025b6350b9f0abbec3a3be13.tar.gz eclipse.jdt.ui-258e2bf2d72ff5aa025b6350b9f0abbec3a3be13.tar.xz eclipse.jdt.ui-258e2bf2d72ff5aa025b6350b9f0abbec3a3be13.zip | |
Bug 562077 - [AutoRefactor immigration #16/133] [cleanup & saveaction]
Use Objects.equals() in the equals method implementation
Change-Id: Ie4463d947859f2cdcff45c55cf927d870bb6f8b8
Signed-off-by: Fabrice Tiercelin <fabrice.tiercelin@yahoo.fr>
12 files changed, 710 insertions, 4 deletions
diff --git a/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/MultiFixMessages.java b/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/MultiFixMessages.java index 88897598a3..0fec8b6585 100644 --- a/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/MultiFixMessages.java +++ b/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/MultiFixMessages.java @@ -127,6 +127,7 @@ public class MultiFixMessages extends NLS { public static String UseDirectlyMapMethodCleanup_description; public static String RedundantSemicolonsCleanup_description; public static String UnnecessaryArrayCreationCleanup_description; + public static String ObjectsEqualsCleanup_description; static { // initialize resource bundle diff --git a/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/MultiFixMessages.properties b/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/MultiFixMessages.properties index 07c1160716..aba66e673e 100644 --- a/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/MultiFixMessages.properties +++ b/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/MultiFixMessages.properties @@ -107,3 +107,4 @@ MergeConditionalBlocksCleanup_description=Merge conditions of if/else if/else th UseDirectlyMapMethodCleanup_description = Operate on Maps directly RedundantSemicolonsCleanup_description= Remove redundant semicolons UnnecessaryArrayCreationCleanup_description=Remove unnecessary array creation for varargs +ObjectsEqualsCleanup_description=Use Objects.equals() in the equals method implementation diff --git a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/dom/ASTNodes.java b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/dom/ASTNodes.java index dc9b9dbeb9..65198ddd99 100644 --- a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/dom/ASTNodes.java +++ b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/dom/ASTNodes.java @@ -595,6 +595,20 @@ public class ASTNodes { } /** + * Return true if the node changes nothing. + * + * @param node The node to visit. + * + * @return True if the node changes nothing. + */ + public static boolean isPassive(final ASTNode node) { + ExprActivityVisitor visitor= new ExprActivityVisitor(); + visitor.traverseNodeInterruptibly(node); + return ExprActivity.PASSIVE_WITHOUT_FALLING_THROUGH.equals(visitor.getActivityLevel()) + || ExprActivity.PASSIVE.equals(visitor.getActivityLevel()); + } + + /** * True if the method is static, false if it is not or null if it is unknown. * * @param method The method @@ -834,6 +848,78 @@ public class ASTNodes { } /** + * Return the items of an infix expression in the order it is specified. It reverses the operator if needed. + * + * @param <F> the required expression type + * @param <S> the required expression type + * @param node the supposed infix expression + * @param firstClass the class representing the required expression type + * @param secondClass the class representing the required expression type + * @return the items of an infix expression in the order it is specified. It reverses the operator if needed. + */ + public static <F extends Expression, S extends Expression> OrderedInfixExpression<F, S> orderedInfix(final Expression node, final Class<F> firstClass, final Class<S> secondClass) { + InfixExpression expression= as(node, InfixExpression.class); + + if (expression == null || expression.hasExtendedOperands()) { + return null; + } + + if (firstClass != null && firstClass.equals(secondClass)) { + F first= as(expression.getLeftOperand(), firstClass); + S second= as(expression.getRightOperand(), secondClass); + + if (first != null && second != null) { + return new OrderedInfixExpression<>(first, expression.getOperator(), second); + } + } else { + F leftFirst= as(expression.getLeftOperand(), firstClass); + S rightSecond= as(expression.getRightOperand(), secondClass); + + if (leftFirst != null && rightSecond != null) { + return new OrderedInfixExpression<>(leftFirst, expression.getOperator(), rightSecond); + } + + InfixExpression.Operator mirroredOperator= mirrorOperator(expression); + + if (mirroredOperator != null) { + F rightFirst= as(expression.getRightOperand(), firstClass); + S leftSecond= as(expression.getLeftOperand(), secondClass); + + if (rightFirst != null && leftSecond != null) { + return new OrderedInfixExpression<>(rightFirst, mirroredOperator, leftSecond); + } + } + } + + return null; + } + + private static InfixExpression.Operator mirrorOperator(final InfixExpression expression) { + if (Arrays.asList( + InfixExpression.Operator.AND, + InfixExpression.Operator.CONDITIONAL_AND, + InfixExpression.Operator.CONDITIONAL_OR, + InfixExpression.Operator.EQUALS, + InfixExpression.Operator.NOT_EQUALS, + InfixExpression.Operator.OR, + InfixExpression.Operator.PLUS, + InfixExpression.Operator.TIMES, + InfixExpression.Operator.XOR).contains(expression.getOperator())) { + return expression.getOperator(); + } else if (InfixExpression.Operator.GREATER.equals(expression.getOperator())) { + return InfixExpression.Operator.LESS; + } else if (InfixExpression.Operator.GREATER_EQUALS.equals(expression.getOperator())) { + return InfixExpression.Operator.LESS_EQUALS; + } else if (InfixExpression.Operator.LESS.equals(expression.getOperator())) { + return InfixExpression.Operator.GREATER; + } else if (InfixExpression.Operator.LESS_EQUALS.equals(expression.getOperator())) { + return InfixExpression.Operator.GREATER_EQUALS; + } + + return null; + } + + /** * Returns all the operands from the provided infix expressions. * * @param node the infix expression diff --git a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/dom/OrderedInfixExpression.java b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/dom/OrderedInfixExpression.java new file mode 100644 index 0000000000..10e2fe8740 --- /dev/null +++ b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/dom/OrderedInfixExpression.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright (c) 2020 Fabrice TIERCELIN and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Fabrice TIERCELIN - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.internal.corext.dom; + +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.InfixExpression; + +/** + * Ordered infix expression for commutative operations. + * + * @param <F> First operand + * @param <S> Second operand + */ +public class OrderedInfixExpression<F extends Expression, S extends Expression> { + private F firstOperand; + private InfixExpression.Operator operator; + private S secondOperand; + + /** + * Ordered infix expression. + * + * @param firstOperand first operand + * @param operator operator + * @param secondOperand second operand + */ + public OrderedInfixExpression(final F firstOperand, InfixExpression.Operator operator, final S secondOperand) { + this.firstOperand= firstOperand; + this.operator= operator; + this.secondOperand= secondOperand; + } + + /** + * Get the first operand. + * + * @return the first operand. + */ + public F getFirstOperand() { + return firstOperand; + } + + /** + * Get the operator. + * + * @return the operator. + */ + public InfixExpression.Operator getOperator() { + return operator; + } + + /** + * Get the second operand. + * + * @return the second operand. + */ + public S getSecondOperand() { + return secondOperand; + } +} diff --git a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstants.java b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstants.java index 7ceec707c9..617ca81d7c 100644 --- a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstants.java +++ b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstants.java @@ -1008,6 +1008,18 @@ public class CleanUpConstants { public static final String REMOVE_UNNECESSARY_ARRAY_CREATION= "cleanup.remove_unnecessary_array_creation"; //$NON-NLS-1$ /** + * Reduces the code of the equals method implementation by using Objects.equals(). + * <p> + * Possible values: {TRUE, FALSE} + * <p> + * + * @see CleanUpOptionsCore#TRUE + * @see CleanUpOptionsCore#FALSE + * @since 4.17 + */ + public static final String USE_OBJECTS_EQUALS= "cleanup.objects_equals"; //$NON-NLS-1$ + + /** * Controls whether missing annotations should be added to the code. For detailed settings use:<br> * {@link #ADD_MISSING_ANNOTATIONS_DEPRECATED}<br> {@value #ADD_MISSING_ANNOTATIONS_OVERRIDE} <br> * <br> diff --git a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/CleanUpTest1d7.java b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/CleanUpTest1d7.java index f7c5e885e0..b73af12e5b 100644 --- a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/CleanUpTest1d7.java +++ b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/CleanUpTest1d7.java @@ -32,9 +32,8 @@ import org.eclipse.jdt.ui.tests.core.rules.ProjectTestSetup; @RunWith(JUnit4.class) public class CleanUpTest1d7 extends CleanUpTestCase { - @Rule - public ProjectTestSetup projectsetup = new Java1d7ProjectTestSetup(); + public ProjectTestSetup projectsetup= new Java1d7ProjectTestSetup(); @Override protected IJavaProject getProject() { @@ -84,4 +83,232 @@ public class CleanUpTest1d7 extends CleanUpTestCase { assertRefactoringResultAsExpected(new ICompilationUnit[] { cu1 }, new String[] { expected1 }); } + @Test + public void testObjectsEquals() throws Exception { + IPackageFragment pack1= fSourceFolder.createPackageFragment("test1", false, null); + String sample= "" // + + "package test1;\n" // + + "\n" // + + "import java.util.Map;\n" // + + "import java.util.Observable;\n" // + + "\n" // + + "public class E1 {\n" // + + " private Map<Integer, String> textById;\n" // + + " private Observable anObservable;\n" // + + " private String aText;\n" // + + "\n" // + + " /* (non-Javadoc)\n" // + + " * @see java.lang.Object#equals(java.lang.Object)\n" // + + " */\n" // + + " @Override\n" // + + " public boolean equals(Object obj) {\n" // + + " if (this == obj)\n" // + + " return true;\n" // + + " if (obj == null)\n" // + + " return false;\n" // + + " if (getClass() != obj.getClass())\n" // + + " return false;\n" // + + " E1 other = (E1) obj;\n" // + + " if (aText == null) {\n" // + + " if (other.aText != null)\n" // + + " return false;\n" // + + " } else if (!aText.equals(other.aText))\n" // + + " return false;\n" // + + " if (null == anObservable) {\n" // + + " if (null != other.anObservable)\n" // + + " return false;\n" // + + " } else if (!anObservable.equals(other.anObservable))\n" // + + " return false;\n" // + + " if (this.textById == null) {\n" // + + " if (other.textById != null)\n" // + + " return false;\n" // + + " } else if (!this.textById.equals(other.textById)) {\n" // + + " return false;\n" // + + " }\n" // + + " return true;\n" // + + " }\n" // + + "}\n"; + ICompilationUnit cu1= pack1.createCompilationUnit("E1.java", sample, false, null); + + enable(CleanUpConstants.USE_OBJECTS_EQUALS); + + sample= "" // + + "package test1;\n" // + + "\n" // + + "import java.util.Map;\n" // + + "import java.util.Objects;\n" // + + "import java.util.Observable;\n" // + + "\n" // + + "public class E1 {\n" // + + " private Map<Integer, String> textById;\n" // + + " private Observable anObservable;\n" // + + " private String aText;\n" // + + "\n" // + + " /* (non-Javadoc)\n" // + + " * @see java.lang.Object#equals(java.lang.Object)\n" // + + " */\n" // + + " @Override\n" // + + " public boolean equals(Object obj) {\n" // + + " if (this == obj)\n" // + + " return true;\n" // + + " if (obj == null)\n" // + + " return false;\n" // + + " if (getClass() != obj.getClass())\n" // + + " return false;\n" // + + " E1 other = (E1) obj;\n" // + + " if (!Objects.equals(aText, other.aText)) {\n" // + + " return false;\n" // + + " }\n" // + + " if (!Objects.equals(anObservable, other.anObservable)) {\n" // + + " return false;\n" // + + " }\n" // + + " if (!Objects.equals(this.textById, other.textById)) {\n" // + + " return false;\n" // + + " }\n" // + + " return true;\n" // + + " }\n" // + + "}\n"; + String expected1= sample; + + assertRefactoringResultAsExpected(new ICompilationUnit[] { cu1 }, new String[] { expected1 }); + } + + @Test + public void testDoNotRefactorObjectsEquals() throws Exception { + IPackageFragment pack1= fSourceFolder.createPackageFragment("test1", false, null); + String sample= "" // + + "package test1;\n" // + + "\n" // + + "import java.util.Map;\n" // + + "import java.util.Observable;\n" // + + "\n" // + + "public class E1 {\n" // + + " private Map<Integer, String> textById;\n" // + + " private Observable anObservable;\n" // + + " private String aText;\n" // + + "\n" // + + " /* (non-Javadoc)\n" // + + " * @see java.lang.Object#equals(java.lang.Object)\n" // + + " */\n" // + + " @Override\n" // + + " public boolean equals(Object obj) {\n" // + + " if (this == obj)\n" // + + " return true;\n" // + + " if (obj == null)\n" // + + " return false;\n" // + + " if (getClass() != obj.getClass())\n" // + + " return false;\n" // + + " E1 other = (E1) obj;\n" // + + " if (aText == null) {\n" // + + " if (other.aText != null)\n" // + + " return true;\n" // + + " } else if (!aText.equals(other.aText))\n" // + + " return false;\n" // + + " if (null == anObservable) {\n" // + + " if (null != other.anObservable)\n" // + + " return false;\n" // + + " } else if (!anObservable.equals(other.anObservable))\n" // + + " return true;\n" // + + " if (this.textById == null) {\n" // + + " if (other.textById != null)\n" // + + " return false;\n" // + + " } else if (this.textById.equals(other.textById))\n" // + + " return false;\n" // + + " return true;\n" // + + " }\n" // + + "}\n"; + ICompilationUnit cu= pack1.createCompilationUnit("E1.java", sample, false, null); + + enable(CleanUpConstants.USE_OBJECTS_EQUALS); + + assertRefactoringHasNoChange(new ICompilationUnit[] { cu }); + } + + @Test + public void testObjectsEqualsWithImportConflict() throws Exception { + IPackageFragment pack1= fSourceFolder.createPackageFragment("test1", false, null); + String sample= "" // + + "package test1;\n" // + + "\n" // + + "import java.util.Map;\n" // + + "import java.util.Observable;\n" // + + "\n" // + + "public class Objects {\n" // + + " private Map<Integer, String> textById;\n" // + + " private Observable anObservable;\n" // + + " private String aText;\n" // + + "\n" // + + " /* (non-Javadoc)\n" // + + " * @see java.lang.Object#equals(java.lang.Object)\n" // + + " */\n" // + + " @Override\n" // + + " public boolean equals(Object obj) {\n" // + + " if (this == obj)\n" // + + " return true;\n" // + + " if (obj == null)\n" // + + " return false;\n" // + + " if (getClass() != obj.getClass())\n" // + + " return false;\n" // + + " Objects other = (Objects) obj;\n" // + + " if (aText == null) {\n" // + + " if (other.aText != null)\n" // + + " return false;\n" // + + " } else if (!aText.equals(other.aText))\n" // + + " return false;\n" // + + " if (null == anObservable) {\n" // + + " if (null != other.anObservable)\n" // + + " return false;\n" // + + " } else if (!anObservable.equals(other.anObservable))\n" // + + " return false;\n" // + + " if (this.textById == null) {\n" // + + " if (other.textById != null)\n" // + + " return false;\n" // + + " } else if (!this.textById.equals(other.textById))\n" // + + " return false;\n" // + + " return true;\n" // + + " }\n" // + + "}\n"; + ICompilationUnit cu1= pack1.createCompilationUnit("Objects.java", sample, false, null); + + enable(CleanUpConstants.USE_OBJECTS_EQUALS); + + sample= "" // + + "package test1;\n" // + + "\n" // + + "import java.util.Map;\n" // + + "import java.util.Observable;\n" // + + "\n" // + + "public class Objects {\n" // + + " private Map<Integer, String> textById;\n" // + + " private Observable anObservable;\n" // + + " private String aText;\n" // + + "\n" // + + " /* (non-Javadoc)\n" // + + " * @see java.lang.Object#equals(java.lang.Object)\n" // + + " */\n" // + + " @Override\n" // + + " public boolean equals(Object obj) {\n" // + + " if (this == obj)\n" // + + " return true;\n" // + + " if (obj == null)\n" // + + " return false;\n" // + + " if (getClass() != obj.getClass())\n" // + + " return false;\n" // + + " Objects other = (Objects) obj;\n" // + + " if (!java.util.Objects.equals(aText, other.aText)) {\n" // + + " return false;\n" // + + " }\n" // + + " if (!java.util.Objects.equals(anObservable, other.anObservable)) {\n" // + + " return false;\n" // + + " }\n" // + + " if (!java.util.Objects.equals(this.textById, other.textById)) {\n" // + + " return false;\n" // + + " }\n" // + + " return true;\n" // + + " }\n" // + + "}\n"; + String expected1= sample; + + assertRefactoringResultAsExpected(new ICompilationUnit[] { cu1 }, new String[] { expected1 }); + } } diff --git a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstantsOptions.java b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstantsOptions.java index d4f3436a09..5853fe215d 100644 --- a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstantsOptions.java +++ b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/CleanUpConstantsOptions.java @@ -95,6 +95,7 @@ public class CleanUpConstantsOptions extends CleanUpConstants { options.setOption(REMOVE_REDUNDANT_MODIFIERS, CleanUpOptions.FALSE); options.setOption(REMOVE_REDUNDANT_SEMICOLONS, CleanUpOptions.FALSE); options.setOption(REMOVE_UNNECESSARY_ARRAY_CREATION, CleanUpOptions.FALSE); + options.setOption(USE_OBJECTS_EQUALS, CleanUpOptions.FALSE); //Missing Code options.setOption(ADD_MISSING_ANNOTATIONS, CleanUpOptions.TRUE); @@ -196,6 +197,7 @@ public class CleanUpConstantsOptions extends CleanUpConstants { options.setOption(REMOVE_REDUNDANT_MODIFIERS, CleanUpOptions.FALSE); options.setOption(REMOVE_REDUNDANT_SEMICOLONS, CleanUpOptions.FALSE); options.setOption(REMOVE_UNNECESSARY_ARRAY_CREATION, CleanUpOptions.FALSE); + options.setOption(USE_OBJECTS_EQUALS, CleanUpOptions.FALSE); //Missing Code options.setOption(ADD_MISSING_ANNOTATIONS, CleanUpOptions.TRUE); diff --git a/org.eclipse.jdt.ui/plugin.xml b/org.eclipse.jdt.ui/plugin.xml index 4501c9b99c..ecd7fbaa94 100644 --- a/org.eclipse.jdt.ui/plugin.xml +++ b/org.eclipse.jdt.ui/plugin.xml @@ -7116,9 +7116,14 @@ runAfter="org.eclipse.jdt.ui.cleanup.unnecessary_semicolons"> </cleanUp> <cleanUp + class="org.eclipse.jdt.internal.ui.fix.ObjectsEqualsCleanUp" + id="org.eclipse.jdt.ui.cleanup.precompile_regex" + runAfter="org.eclipse.jdt.ui.cleanup.unnecessary_array_creation"> + </cleanUp> + <cleanUp class="org.eclipse.jdt.internal.ui.fix.StringCleanUp" id="org.eclipse.jdt.ui.cleanup.strings" - runAfter="org.eclipse.jdt.ui.cleanup.unnecessary_array_creation"> + runAfter="org.eclipse.jdt.ui.cleanup.precompile_regex"> </cleanUp> <cleanUp class="org.eclipse.jdt.internal.ui.fix.UnimplementedCodeCleanUp" diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/ObjectsEqualsCleanUp.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/ObjectsEqualsCleanUp.java new file mode 100644 index 0000000000..2d61d4b24b --- /dev/null +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/ObjectsEqualsCleanUp.java @@ -0,0 +1,296 @@ +/******************************************************************************* + * Copyright (c) 2020 Fabrice TIERCELIN and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Fabrice TIERCELIN - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.internal.ui.fix; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; + +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTMatcher; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.Block; +import org.eclipse.jdt.core.dom.BooleanLiteral; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.IfStatement; +import org.eclipse.jdt.core.dom.InfixExpression; +import org.eclipse.jdt.core.dom.MethodInvocation; +import org.eclipse.jdt.core.dom.Name; +import org.eclipse.jdt.core.dom.NullLiteral; +import org.eclipse.jdt.core.dom.PrefixExpression; +import org.eclipse.jdt.core.dom.ReturnStatement; +import org.eclipse.jdt.core.dom.Statement; +import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; +import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; +import org.eclipse.jdt.core.refactoring.CompilationUnitChange; + +import org.eclipse.jdt.internal.corext.dom.ASTNodes; +import org.eclipse.jdt.internal.corext.dom.OrderedInfixExpression; +import org.eclipse.jdt.internal.corext.fix.CleanUpConstants; +import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFix; +import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFix.CompilationUnitRewriteOperation; +import org.eclipse.jdt.internal.corext.fix.LinkedProposalModel; +import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite; +import org.eclipse.jdt.internal.corext.util.JavaModelUtil; + +import org.eclipse.jdt.ui.cleanup.CleanUpRequirements; +import org.eclipse.jdt.ui.cleanup.ICleanUpFix; +import org.eclipse.jdt.ui.text.java.IProblemLocation; + +/** + * A fix that reduces the code of the equals method implementation by using Objects.equals(). + */ +public class ObjectsEqualsCleanUp extends AbstractMultiFix implements ICleanUpFix { + private static final String EQUALS_METHOD= "equals"; //$NON-NLS-1$ + + public ObjectsEqualsCleanUp() { + this(Collections.emptyMap()); + } + + public ObjectsEqualsCleanUp(Map<String, String> options) { + super(options); + } + + @Override + public CleanUpRequirements getRequirements() { + boolean requireAST= isEnabled(CleanUpConstants.USE_OBJECTS_EQUALS); + return new CleanUpRequirements(requireAST, false, false, null); + } + + @Override + public String[] getStepDescriptions() { + if (isEnabled(CleanUpConstants.USE_OBJECTS_EQUALS)) { + return new String[] { MultiFixMessages.ObjectsEqualsCleanup_description }; + } + + return new String[0]; + } + + @Override + public String getPreview() { + if (isEnabled(CleanUpConstants.USE_OBJECTS_EQUALS)) { + return "" //$NON-NLS-1$ + + "if (!Objects.equals(aText, other.aText)) {\n" //$NON-NLS-1$ + + " return false;\n" //$NON-NLS-1$ + + "}\n\n\n\n\n"; //$NON-NLS-1$ + } else { + return "" //$NON-NLS-1$ + + "if (aText == null) {\n" //$NON-NLS-1$ + + " if (other.aText != null) {\n" //$NON-NLS-1$ + + " return false;\n" //$NON-NLS-1$ + + " }\n" //$NON-NLS-1$ + + "} else if (!aText.equals(other.aText)) {\n" //$NON-NLS-1$ + + " return false;\n" //$NON-NLS-1$ + + "}\n"; //$NON-NLS-1$ + } + } + + @Override + protected ICleanUpFix createFix(CompilationUnit unit) throws CoreException { + if (!isEnabled(CleanUpConstants.USE_OBJECTS_EQUALS) || !JavaModelUtil.is17OrHigher(unit.getJavaElement().getJavaProject())) { + return null; + } + + final List<CompilationUnitRewriteOperation> rewriteOperations= new ArrayList<>(); + + unit.accept(new ASTVisitor() { + + @Override + public boolean visit(final IfStatement node) { + if (node.getElseStatement() != null) { + InfixExpression condition= ASTNodes.as(node.getExpression(), InfixExpression.class); + List<Statement> thenStatements= ASTNodes.asList(node.getThenStatement()); + List<Statement> elseStatements= ASTNodes.asList(node.getElseStatement()); + + if (condition != null && !condition.hasExtendedOperands() + && ASTNodes.hasOperator(condition, InfixExpression.Operator.EQUALS, InfixExpression.Operator.NOT_EQUALS) + && thenStatements != null && thenStatements.size() == 1 && elseStatements != null && elseStatements.size() == 1) { + OrderedInfixExpression<Expression, NullLiteral> nullityTypedCondition= ASTNodes.orderedInfix(condition, Expression.class, NullLiteral.class); + + if (nullityTypedCondition != null && ASTNodes.isPassive(nullityTypedCondition.getFirstOperand())) { + return maybeReplaceCode(node, condition, thenStatements, elseStatements, nullityTypedCondition.getFirstOperand()); + } + } + } + + return true; + } + + private boolean maybeReplaceCode(final IfStatement node, final InfixExpression condition, + final List<Statement> thenStatements, final List<Statement> elseStatements, final Expression firstField) { + IfStatement checkNullityStatement; + IfStatement checkEqualsStatement; + + if (ASTNodes.hasOperator(condition, InfixExpression.Operator.EQUALS)) { + checkNullityStatement= ASTNodes.as(thenStatements.get(0), IfStatement.class); + checkEqualsStatement= ASTNodes.as(elseStatements.get(0), IfStatement.class); + } else { + checkEqualsStatement= ASTNodes.as(thenStatements.get(0), IfStatement.class); + checkNullityStatement= ASTNodes.as(elseStatements.get(0), IfStatement.class); + } + + if (checkNullityStatement != null && checkNullityStatement.getElseStatement() == null && checkEqualsStatement != null + && checkEqualsStatement.getElseStatement() == null) { + InfixExpression nullityCondition= ASTNodes.as(checkNullityStatement.getExpression(), InfixExpression.class); + List<Statement> nullityStatements= ASTNodes.asList(checkNullityStatement.getThenStatement()); + + PrefixExpression equalsCondition= ASTNodes.as(checkEqualsStatement.getExpression(), PrefixExpression.class); + List<Statement> equalsStatements= ASTNodes.asList(checkEqualsStatement.getThenStatement()); + + if (nullityCondition != null && !nullityCondition.hasExtendedOperands() + && ASTNodes.hasOperator(nullityCondition, InfixExpression.Operator.NOT_EQUALS) && nullityStatements != null + && nullityStatements.size() == 1 && equalsCondition != null + && ASTNodes.hasOperator(equalsCondition, PrefixExpression.Operator.NOT) && equalsStatements != null + && equalsStatements.size() == 1) { + return maybeReplaceEquals(node, firstField, nullityCondition, nullityStatements, equalsCondition, + equalsStatements); + } + } + + return true; + } + + private boolean maybeReplaceEquals(final IfStatement node, final Expression firstField, + final InfixExpression nullityCondition, final List<Statement> nullityStatements, + final PrefixExpression equalsCondition, final List<Statement> equalsStatements) { + OrderedInfixExpression<Expression, NullLiteral> nullityTypedCondition= ASTNodes.orderedInfix(nullityCondition, Expression.class, NullLiteral.class); + ReturnStatement returnStatement1= ASTNodes.as(nullityStatements.get(0), ReturnStatement.class); + ReturnStatement returnStatement2= ASTNodes.as(equalsStatements.get(0), ReturnStatement.class); + MethodInvocation equalsMethod= ASTNodes.as(equalsCondition.getOperand(), MethodInvocation.class); + + if (nullityTypedCondition != null && returnStatement1 != null && returnStatement2 != null && equalsMethod != null + && equalsMethod.getExpression() != null && EQUALS_METHOD.equals(equalsMethod.getName().getIdentifier()) + && equalsMethod.arguments() != null && equalsMethod.arguments().size() == 1) { + Expression secondField= nullityTypedCondition.getFirstOperand(); + + if (secondField != null + && (match(firstField, secondField, equalsMethod.getExpression(), + (ASTNode) equalsMethod.arguments().get(0)) + || match(secondField, firstField, equalsMethod.getExpression(), + (ASTNode) equalsMethod.arguments().get(0)))) { + BooleanLiteral returnFalse1= ASTNodes.as(returnStatement1.getExpression(), BooleanLiteral.class); + BooleanLiteral returnFalse2= ASTNodes.as(returnStatement2.getExpression(), BooleanLiteral.class); + + if (returnFalse1 != null && !returnFalse1.booleanValue() && returnFalse2 != null + && !returnFalse2.booleanValue()) { + rewriteOperations.add(new ObjectsEqualsOperation(node, firstField, secondField, returnStatement1)); + return false; + } + } + } + + return true; + } + + private boolean match(final Expression firstField, final Expression secondField, final Expression thisObject, + final ASTNode otherObject) { + ASTMatcher astMatcher= new ASTMatcher(); + return astMatcher.safeSubtreeMatch(thisObject, firstField) && astMatcher.safeSubtreeMatch(otherObject, secondField); + } + }); + + if (rewriteOperations.isEmpty()) { + return null; + } + + return new CompilationUnitRewriteOperationsFix(MultiFixMessages.ObjectsEqualsCleanup_description, unit, + rewriteOperations.toArray(new CompilationUnitRewriteOperation[0])); + } + + @Override + public CompilationUnitChange createChange(IProgressMonitor progressMonitor) throws CoreException { + return null; + } + + @Override + public boolean canFix(final ICompilationUnit compilationUnit, final IProblemLocation problem) { + return false; + } + + @Override + protected ICleanUpFix createFix(final CompilationUnit unit, final IProblemLocation[] problems) throws CoreException { + return null; + } + + private static class ObjectsEqualsOperation extends CompilationUnitRewriteOperation { + private final IfStatement node; + private final Expression firstField; + private final Expression secondField; + private final ReturnStatement returnStatement; + + public ObjectsEqualsOperation(final IfStatement node, final Expression firstField, + final Expression secondField, + final ReturnStatement returnStatement) { + this.node= node; + this.firstField= firstField; + this.secondField= secondField; + this.returnStatement= returnStatement; + } + + @Override + public void rewriteAST(final CompilationUnitRewrite cuRewrite, final LinkedProposalModel linkedModel) throws CoreException { + ASTRewrite rewrite= cuRewrite.getASTRewrite(); + AST ast= cuRewrite.getRoot().getAST(); + ImportRewrite importRewrite= cuRewrite.getImportRewrite(); + + String objectNameText= importRewrite.addImport(Objects.class.getCanonicalName()); + + MethodInvocation newEqualsMethod= ast.newMethodInvocation(); + newEqualsMethod.setExpression(newTypeName(ast, objectNameText)); + newEqualsMethod.setName(ast.newSimpleName(EQUALS_METHOD)); + newEqualsMethod.arguments().add(ASTNodes.createMoveTarget(rewrite, firstField)); + newEqualsMethod.arguments().add(ASTNodes.createMoveTarget(rewrite, secondField)); + + PrefixExpression notExpression= ast.newPrefixExpression(); + notExpression.setOperator(PrefixExpression.Operator.NOT); + notExpression.setOperand(newEqualsMethod); + + ReturnStatement copyOfReturnStatement= ASTNodes.createMoveTarget(rewrite, returnStatement); + + rewrite.replace(node.getExpression(), notExpression, null); + + if (node.getThenStatement() instanceof Block) { + rewrite.replace((ASTNode) ((Block) node.getThenStatement()).statements().get(0), copyOfReturnStatement, null); + } else { + rewrite.replace(node.getThenStatement(), copyOfReturnStatement, null); + } + + if (node.getElseStatement() != null) { + rewrite.remove(node.getElseStatement(), null); + } + } + + private Name newTypeName(final AST ast, final String objectNameText) { + Name qualifiedName= null; + + for (String packageName : objectNameText.split("\\.")) { //$NON-NLS-1$ + if (qualifiedName == null) { + qualifiedName= ast.newSimpleName(packageName); + } else { + qualifiedName= ast.newQualifiedName(qualifiedName, ast.newSimpleName(packageName)); + } + } + + return qualifiedName; + } + } +} diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/CleanUpMessages.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/CleanUpMessages.java index 8bcbb0965d..a3f8de8cbe 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/CleanUpMessages.java +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/CleanUpMessages.java @@ -128,6 +128,7 @@ public class CleanUpMessages extends NLS { public static String UnnecessaryCodeTabPage_CheckboxName_RedundantSemicolons; public static String UnnecessaryCodeTabPage_CheckboxName_RedundantSemicolons_description; public static String UnnecessaryCodeTabPage_CheckboxName_RedundantArrayCreation_description; + public static String UnnecessaryCodeTabPage_CheckboxName_ObjectsEquals; public static String UnnecessaryCodeTabPage_CheckboxName_UnnecessaryVarargsArrayCreation; public static String UnnecessaryCodeTabPage_CheckboxName_UnusedConstructors; public static String UnnecessaryCodeTabPage_CheckboxName_UnusedFields; diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/CleanUpMessages.properties b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/CleanUpMessages.properties index d066227fba..5a4a1ec1a2 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/CleanUpMessages.properties +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/CleanUpMessages.properties @@ -113,3 +113,4 @@ UnnecessaryCodeTabPage_CheckboxName_RedundantSemicolons=Remove redundant semicol UnnecessaryCodeTabPage_CheckboxName_RedundantSemicolons_description=Removes unnecessary semicolons on statements and declarations UnnecessaryCodeTabPage_CheckboxName_RedundantArrayCreation_description=Removes unnecessary array creation used to supply varargs in a method invocation UnnecessaryCodeTabPage_CheckboxName_UnnecessaryVarargsArrayCreation=Remove unnecessary &array creation (1.5 or higher) +UnnecessaryCodeTabPage_CheckboxName_ObjectsEquals=Use Objects.e&quals() in the equals method implementation (1.7 or higher) diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/UnnecessaryCodeTabPage.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/UnnecessaryCodeTabPage.java index 6a3b77aa16..50613f7d4d 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/UnnecessaryCodeTabPage.java +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/UnnecessaryCodeTabPage.java @@ -25,6 +25,7 @@ import org.eclipse.jdt.internal.ui.fix.AbstractCleanUp; import org.eclipse.jdt.internal.ui.fix.AutoboxingCleanUp; import org.eclipse.jdt.internal.ui.fix.MapMethodCleanUp; import org.eclipse.jdt.internal.ui.fix.MergeConditionalBlocksCleanUp; +import org.eclipse.jdt.internal.ui.fix.ObjectsEqualsCleanUp; import org.eclipse.jdt.internal.ui.fix.PushDownNegationCleanUp; import org.eclipse.jdt.internal.ui.fix.RedundantModifiersCleanUp; import org.eclipse.jdt.internal.ui.fix.RedundantSemicolonsCleanUp; @@ -52,7 +53,8 @@ public final class UnnecessaryCodeTabPage extends AbstractCleanUpTabPage { new MapMethodCleanUp(values), new RedundantModifiersCleanUp(values), new RedundantSemicolonsCleanUp(values), - new UnnecessaryArrayCreationCleanUp(values) + new UnnecessaryArrayCreationCleanUp(values), + new ObjectsEqualsCleanUp(values) }; } @@ -110,5 +112,8 @@ public final class UnnecessaryCodeTabPage extends AbstractCleanUpTabPage { CheckboxPreference arrayCreation= createCheckboxPref(unnecessaryGroup, numColumns, CleanUpMessages.UnnecessaryCodeTabPage_CheckboxName_UnnecessaryVarargsArrayCreation, CleanUpConstants.REMOVE_UNNECESSARY_ARRAY_CREATION, CleanUpModifyDialog.FALSE_TRUE); registerPreference(arrayCreation); + + CheckboxPreference objectsEquals= createCheckboxPref(unnecessaryGroup, numColumns, CleanUpMessages.UnnecessaryCodeTabPage_CheckboxName_ObjectsEquals, CleanUpConstants.USE_OBJECTS_EQUALS, CleanUpModifyDialog.FALSE_TRUE); + registerPreference(objectsEquals); } } |
