Bug 571184 - [AutoRefactor immigration #62/149] [cleanup & saveaction]
Object::equals on non null

Given:
boolean result = text.equals("foo");
boolean result2 = text.equals(text1 + text2);
boolean result3 = object.equals(DayOfWeek.MONDAY);
boolean result4 = text.equalsIgnoreCase("foo");

When:
Clean up the code enabling "Avoid Object::equals or
String::equalsIgnoreCase on null objects"

Then:
boolean result = "foo".equals(text);
boolean result2 = (text1 + text2).equals(text);
boolean result3 = DayOfWeek.MONDAY.equals(object);
boolean result4 = "foo".equalsIgnoreCase(text);

Change-Id: I795af204f8dff26c9ffd0e447b434c472bf050d7
Signed-off-by: Fabrice Tiercelin <fabrice.tiercelin@yahoo.fr>
diff --git a/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/InvertEqualsCleanUpCore.java b/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/InvertEqualsCleanUpCore.java
new file mode 100644
index 0000000..913c43b
--- /dev/null
+++ b/org.eclipse.jdt.core.manipulation/common/org/eclipse/jdt/internal/ui/fix/InvertEqualsCleanUpCore.java
@@ -0,0 +1,85 @@
+/*******************************************************************************
+ * Copyright (c) 2021 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.List;
+import java.util.Map;
+
+import org.eclipse.core.runtime.CoreException;
+
+import org.eclipse.jdt.core.dom.CompilationUnit;
+import org.eclipse.jdt.core.manipulation.CleanUpContextCore;
+import org.eclipse.jdt.core.manipulation.CleanUpRequirementsCore;
+import org.eclipse.jdt.core.manipulation.ICleanUpFixCore;
+
+import org.eclipse.jdt.internal.corext.fix.CleanUpConstants;
+import org.eclipse.jdt.internal.corext.fix.InvertEqualsFixCore;
+
+public class InvertEqualsCleanUpCore extends AbstractCleanUpCore {
+	public InvertEqualsCleanUpCore(final Map<String, String> options) {
+		super(options);
+	}
+
+	public InvertEqualsCleanUpCore() {
+	}
+
+	@Override
+	public CleanUpRequirementsCore getRequirementsCore() {
+		return new CleanUpRequirementsCore(requireAST(), false, false, null);
+	}
+
+	public boolean requireAST() {
+		return isEnabled(CleanUpConstants.INVERT_EQUALS);
+	}
+
+	@Override
+	public ICleanUpFixCore createFixCore(final CleanUpContextCore context) throws CoreException {
+		CompilationUnit compilationUnit= context.getAST();
+
+		if (compilationUnit == null || !isEnabled(CleanUpConstants.INVERT_EQUALS)) {
+			return null;
+		}
+
+		return InvertEqualsFixCore.createCleanUp(compilationUnit);
+	}
+
+	@Override
+	public String[] getStepDescriptions() {
+		List<String> result= new ArrayList<>();
+
+		if (isEnabled(CleanUpConstants.INVERT_EQUALS)) {
+			result.add(MultiFixMessages.InvertEqualsCleanUp_description);
+		}
+
+		return result.toArray(new String[0]);
+	}
+
+	@Override
+	public String getPreview() {
+		if (isEnabled(CleanUpConstants.INVERT_EQUALS)) {
+			return "" //$NON-NLS-1$
+					+ "boolean result = \"foo\".equals(text);\n" //$NON-NLS-1$
+					+ "boolean result2 = (text1 + text2).equals(text);\n" //$NON-NLS-1$
+					+ "boolean result3 = DayOfWeek.MONDAY.equals(object);\n" //$NON-NLS-1$
+					+ "boolean result4 = \"foo\".equalsIgnoreCase(text);\n"; //$NON-NLS-1$
+		}
+
+		return "" //$NON-NLS-1$
+				+ "boolean result = text.equals(\"foo\");\n" //$NON-NLS-1$
+				+ "boolean result2 = text.equals(text1 + text2);\n" //$NON-NLS-1$
+				+ "boolean result3 = object.equals(DayOfWeek.MONDAY);\n" //$NON-NLS-1$
+				+ "boolean result4 = text.equalsIgnoreCase(\"foo\");\n"; //$NON-NLS-1$
+	}
+}
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 4f4c332..226520f 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
@@ -172,6 +172,7 @@
 	public static String AddAllCleanup_description;
 	public static String ObjectsEqualsCleanup_description;
 
+	public static String InvertEqualsCleanUp_description;
 	public static String CheckSignOfBitwiseOperation_description;
 	public static String StandardComparisonCleanUp_description;
 
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 1b336b4..b339d0a 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
@@ -153,6 +153,8 @@
 UnloopedWhileCleanUp_description=Convert loop into if
 AddAllCleanup_description=Add elements in collections without loop
 ObjectsEqualsCleanup_description=Use Objects.equals() in the equals method implementation
+
+InvertEqualsCleanUp_description=Avoid Object::equals or String::equalsIgnoreCase on null objects
 CheckSignOfBitwiseOperation_description=Use != 0 instead of > 0 when comparing the result of a bitwise expression
 StandardComparisonCleanUp_description=Compare to zero
 SwitchExpressionsCleanUp_ConvertToSwitchExpressions_description=Convert to switch expression where possible
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 54b8516..820f743 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
@@ -970,6 +970,18 @@
 	public static final String PRECOMPILE_REGEX= "cleanup.precompile_regex"; //$NON-NLS-1$
 
 	/**
+	 * Invert calls to <code>Object.equals(Object)</code> and <code>String.equalsIgnoreCase(String)</code> when it is known that the second operand is not null and the first can be null.
+	 * <p>
+	 * Possible values: {TRUE, FALSE}
+	 * <p>
+	 *
+	 * @see CleanUpOptionsCore#TRUE
+	 * @see CleanUpOptionsCore#FALSE
+	 * @since 4.19
+	 */
+	public static final String INVERT_EQUALS= "cleanup.invert_equals"; //$NON-NLS-1$
+
+	/**
 	 * Check for sign of bitwise operation.
 	 * <p>
 	 * Possible values: {TRUE, FALSE}
diff --git a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/FixMessages.java b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/FixMessages.java
index bef87cf..518a44e 100644
--- a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/FixMessages.java
+++ b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/FixMessages.java
@@ -115,6 +115,8 @@
 	public static String CodeStyleFix_ChangeAccessUsingDeclaring_description;
 	public static String CodeStyleFix_QualifyMethodWithDeclClass_description;
 	public static String CodeStyleFix_QualifyFieldWithDeclClass_description;
+
+	public static String InvertEqualsFix_invert;
 	public static String StandardComparisonFix_compare_to_zero;
 
 	public static String SerialVersion_group_description;
diff --git a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/FixMessages.properties b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/FixMessages.properties
index 9948059..3366950 100644
--- a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/FixMessages.properties
+++ b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/FixMessages.properties
@@ -78,6 +78,8 @@
 CodeStyleFix_QualifyMethodWithDeclClass_description=Qualify method call with declaring type
 CodeStyleFix_QualifyFieldWithDeclClass_description=Qualify field access with declaring type
 CodeStyleFix_removeThis_groupDescription=Remove 'this' qualifier
+
+InvertEqualsFix_invert=Avoid Object::equals or String::equalsIgnoreCase on null objects
 StandardComparisonFix_compare_to_zero=Compare to zero
 CodeStyleFix_ChangeAccessToStaticUsingInstanceType_description=Change access to static using ''{0}'' (instance type)
 
diff --git a/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/InvertEqualsFixCore.java b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/InvertEqualsFixCore.java
new file mode 100644
index 0000000..a80bcb9
--- /dev/null
+++ b/org.eclipse.jdt.core.manipulation/core extension/org/eclipse/jdt/internal/corext/fix/InvertEqualsFixCore.java
@@ -0,0 +1,145 @@
+/*******************************************************************************
+ * Copyright (c) 2021 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.fix;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.core.runtime.CoreException;
+
+import org.eclipse.text.edits.TextEditGroup;
+
+import org.eclipse.jdt.core.dom.AST;
+import org.eclipse.jdt.core.dom.ASTNode;
+import org.eclipse.jdt.core.dom.ASTVisitor;
+import org.eclipse.jdt.core.dom.CompilationUnit;
+import org.eclipse.jdt.core.dom.Expression;
+import org.eclipse.jdt.core.dom.IBinding;
+import org.eclipse.jdt.core.dom.IVariableBinding;
+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.ThisExpression;
+import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
+import org.eclipse.jdt.core.dom.rewrite.TargetSourceRangeComputer;
+import org.eclipse.jdt.core.manipulation.ICleanUpFixCore;
+
+import org.eclipse.jdt.internal.corext.dom.ASTNodeFactory;
+import org.eclipse.jdt.internal.corext.dom.ASTNodes;
+import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
+
+import org.eclipse.jdt.internal.ui.fix.MultiFixMessages;
+
+public class InvertEqualsFixCore extends CompilationUnitRewriteOperationsFixCore {
+	public static final class InvertEqualsFinder extends ASTVisitor {
+		private List<InvertEqualsFixOperation> fResult;
+
+		public InvertEqualsFinder(List<InvertEqualsFixOperation> ops) {
+			fResult= ops;
+		}
+
+		@Override
+		public boolean visit(final MethodInvocation visited) {
+			Expression expression= visited.getExpression();
+			InfixExpression concatenation= ASTNodes.as(expression, InfixExpression.class);
+
+			if (expression == null
+					|| ASTNodes.is(expression, ThisExpression.class)
+					|| isConstant(expression)
+					|| concatenation != null && ASTNodes.hasOperator(concatenation, InfixExpression.Operator.PLUS) && ASTNodes.hasType(concatenation, String.class.getCanonicalName())) {
+				return true;
+			}
+
+			if (ASTNodes.usesGivenSignature(visited, Object.class.getCanonicalName(), "equals", Object.class.getCanonicalName()) //$NON-NLS-1$
+					|| ASTNodes.usesGivenSignature(visited, String.class.getCanonicalName(), "equalsIgnoreCase", String.class.getCanonicalName())) { //$NON-NLS-1$
+				Expression arg0= (Expression) visited.arguments().get(0);
+				InfixExpression concatenationArgument= ASTNodes.as(arg0, InfixExpression.class);
+
+				if (isConstant(arg0) && arg0.resolveTypeBinding() != null && !arg0.resolveTypeBinding().isPrimitive()
+						|| ASTNodes.is(arg0, ThisExpression.class)
+						|| concatenationArgument != null && ASTNodes.hasOperator(concatenationArgument, InfixExpression.Operator.PLUS) && ASTNodes.hasType(concatenationArgument, String.class.getCanonicalName())) {
+					fResult.add(new InvertEqualsFixOperation(expression, arg0));
+					return false;
+				}
+			}
+
+			return true;
+		}
+
+		private static boolean isConstant(final Expression expression) {
+			if (expression != null && expression.resolveConstantExpressionValue() != null) {
+				return true;
+			}
+
+			if (expression instanceof Name) {
+				IBinding binding= ((Name) expression).resolveBinding();
+
+				if (binding instanceof IVariableBinding) {
+					return ((IVariableBinding) binding).isEnumConstant();
+				}
+			}
+
+			return false;
+		}
+	}
+
+	public static class InvertEqualsFixOperation extends CompilationUnitRewriteOperation {
+		private final Expression expression;
+		private final Expression arg0;
+
+		public InvertEqualsFixOperation(final Expression expression, final Expression arg0) {
+			this.expression= expression;
+			this.arg0= arg0;
+		}
+
+		@Override
+		public void rewriteAST(final CompilationUnitRewrite cuRewrite, final LinkedProposalModelCore linkedModel) throws CoreException {
+			ASTRewrite rewrite= cuRewrite.getASTRewrite();
+			AST ast= cuRewrite.getRoot().getAST();
+			TextEditGroup group= createTextEditGroup(MultiFixMessages.InvertEqualsCleanUp_description, cuRewrite);
+			rewrite.setTargetSourceRangeComputer(new TargetSourceRangeComputer() {
+				@Override
+				public SourceRange computeSourceRange(final ASTNode nodeWithComment) {
+					if (Boolean.TRUE.equals(nodeWithComment.getProperty(ASTNodes.UNTOUCH_COMMENT))) {
+						return new SourceRange(nodeWithComment.getStartPosition(), nodeWithComment.getLength());
+					}
+
+					return super.computeSourceRange(nodeWithComment);
+				}
+			});
+
+
+			ASTNodes.replaceButKeepComment(rewrite, expression, ASTNodeFactory.parenthesizeIfNeeded(ast, ASTNodes.createMoveTarget(rewrite, arg0)), group);
+			ASTNodes.replaceButKeepComment(rewrite, arg0, ASTNodes.createMoveTarget(rewrite, ASTNodes.getUnparenthesedExpression(expression)), group);
+		}
+	}
+
+
+	public static ICleanUpFixCore createCleanUp(final CompilationUnit compilationUnit) {
+		List<InvertEqualsFixOperation> operations= new ArrayList<>();
+		InvertEqualsFinder finder= new InvertEqualsFinder(operations);
+		compilationUnit.accept(finder);
+
+		if (operations.isEmpty()) {
+			return null;
+		}
+
+		CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation[] ops= operations.toArray(new CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation[0]);
+		return new InvertEqualsFixCore(FixMessages.InvertEqualsFix_invert, compilationUnit, ops);
+	}
+
+	protected InvertEqualsFixCore(final String name, final CompilationUnit compilationUnit, final CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation[] fixRewriteOperations) {
+		super(name, compilationUnit, fixRewriteOperations);
+	}
+}
diff --git a/org.eclipse.jdt.ui.tests/performance/org/eclipse/jdt/ui/tests/performance/views/CleanUpPerfTest.java b/org.eclipse.jdt.ui.tests/performance/org/eclipse/jdt/ui/tests/performance/views/CleanUpPerfTest.java
index c0613cb..59677b9 100644
--- a/org.eclipse.jdt.ui.tests/performance/org/eclipse/jdt/ui/tests/performance/views/CleanUpPerfTest.java
+++ b/org.eclipse.jdt.ui.tests/performance/org/eclipse/jdt/ui/tests/performance/views/CleanUpPerfTest.java
@@ -76,6 +76,7 @@
 import org.eclipse.jdt.internal.ui.fix.EvaluateNullableCleanUp;
 import org.eclipse.jdt.internal.ui.fix.ExpressionsCleanUp;
 import org.eclipse.jdt.internal.ui.fix.HashCleanUp;
+import org.eclipse.jdt.internal.ui.fix.InvertEqualsCleanUp;
 import org.eclipse.jdt.internal.ui.fix.ImportsCleanUp;
 import org.eclipse.jdt.internal.ui.fix.Java50CleanUp;
 import org.eclipse.jdt.internal.ui.fix.JoinCleanUp;
@@ -815,6 +816,22 @@
 	}
 
 	@Test
+	public void testInvertEqualsCleanUp() throws Exception {
+		CleanUpRefactoring cleanUpRefactoring= new CleanUpRefactoring();
+		addAllCUs(cleanUpRefactoring, MyTestSetup.fJProject1.getChildren());
+
+		Map<String, String> node= getNullSettings();
+
+		node.put(CleanUpConstants.INVERT_EQUALS, CleanUpOptions.TRUE);
+
+		storeSettings(node);
+
+		cleanUpRefactoring.addCleanUp(new InvertEqualsCleanUp());
+
+		doCleanUp(cleanUpRefactoring);
+	}
+
+	@Test
 	public void testStandardComparisonCleanUp() throws Exception {
 		CleanUpRefactoring cleanUpRefactoring= new CleanUpRefactoring();
 		addAllCUs(cleanUpRefactoring, MyTestSetup.fJProject1.getChildren());
diff --git a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/CleanUpTest.java b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/CleanUpTest.java
index acc96c8..45a77b9 100644
--- a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/CleanUpTest.java
+++ b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/CleanUpTest.java
@@ -18304,6 +18304,142 @@
 	}
 
 	@Test
+	public void testInvertEquals() throws Exception {
+		// Given
+		IPackageFragment pack= fSourceFolder.createPackageFragment("test1", false, null);
+		String given= "" //
+				+ "package test1;\n" //
+				+ "\n" //
+				+ "public class E {\n" //
+				+ "    public static interface Itf {\n" //
+				+ "        int primitiveConstant = 1;\n" //
+				+ "        String objConstant = \"fkjfkjf\";\n" //
+				+ "        String objNullConstant = null;\n" //
+				+ "        MyEnum enumConstant = MyEnum.NOT_NULL;\n" //
+				+ "        MyEnum enumNullConstant = null;\n" //
+				+ "    }\n" //
+				+ "\n" //
+				+ "    private static enum MyEnum {\n" //
+				+ "        NOT_NULL\n" //
+				+ "    }\n" //
+				+ "\n" //
+				+ "    public boolean invertEquals(Object obj, String text1, String text2) {\n" //
+				+ "        // Keep this comment\n" //
+				+ "        return obj.equals(\"\")\n" //
+				+ "                && obj.equals(Itf.objConstant)\n" //
+				+ "                && obj.equals(\"\" + Itf.objConstant)\n" //
+				+ "                && obj.equals(MyEnum.NOT_NULL)\n" //
+				+ "                && obj.equals(text1 + text2)\n" //
+				+ "                && obj.equals(this);\n" //
+				+ "    }\n" //
+				+ "\n" //
+				+ "    public boolean invertEqualsIgnoreCase(String s) {\n" //
+				+ "        // Keep this comment\n" //
+				+ "        return s.equalsIgnoreCase(\"\")\n" //
+				+ "                && s.equalsIgnoreCase(Itf.objConstant)\n" //
+				+ "                && s.equalsIgnoreCase(\"\" + Itf.objConstant);\n" //
+				+ "    }\n" //
+				+ "}\n";
+
+		String expected= "" //
+				+ "package test1;\n" //
+				+ "\n" //
+				+ "public class E {\n" //
+				+ "    public static interface Itf {\n" //
+				+ "        int primitiveConstant = 1;\n" //
+				+ "        String objConstant = \"fkjfkjf\";\n" //
+				+ "        String objNullConstant = null;\n" //
+				+ "        MyEnum enumConstant = MyEnum.NOT_NULL;\n" //
+				+ "        MyEnum enumNullConstant = null;\n" //
+				+ "    }\n" //
+				+ "\n" //
+				+ "    private static enum MyEnum {\n" //
+				+ "        NOT_NULL\n" //
+				+ "    }\n" //
+				+ "\n" //
+				+ "    public boolean invertEquals(Object obj, String text1, String text2) {\n" //
+				+ "        // Keep this comment\n" //
+				+ "        return \"\".equals(obj)\n" //
+				+ "                && Itf.objConstant.equals(obj)\n" //
+				+ "                && (\"\" + Itf.objConstant).equals(obj)\n" //
+				+ "                && MyEnum.NOT_NULL.equals(obj)\n" //
+				+ "                && (text1 + text2).equals(obj)\n" //
+				+ "                && this.equals(obj);\n" //
+				+ "    }\n" //
+				+ "\n" //
+				+ "    public boolean invertEqualsIgnoreCase(String s) {\n" //
+				+ "        // Keep this comment\n" //
+				+ "        return \"\".equalsIgnoreCase(s)\n" //
+				+ "                && Itf.objConstant.equalsIgnoreCase(s)\n" //
+				+ "                && (\"\" + Itf.objConstant).equalsIgnoreCase(s);\n" //
+				+ "    }\n" //
+				+ "}\n";
+
+		// When
+		ICompilationUnit cu= pack.createCompilationUnit("E.java", given, false, null);
+		enable(CleanUpConstants.INVERT_EQUALS);
+
+		// Then
+		assertNotEquals("The class must be changed", given, expected);
+		assertGroupCategoryUsed(new ICompilationUnit[] { cu }, new HashSet<>(Arrays.asList(MultiFixMessages.InvertEqualsCleanUp_description)));
+		assertRefactoringResultAsExpected(new ICompilationUnit[] { cu }, new String[] { expected });
+	}
+
+	@Test
+	public void testDoNotInvertEquals() throws Exception {
+		IPackageFragment pack= fSourceFolder.createPackageFragment("test1", false, null);
+		String sample= "" //
+				+ "package test1;\n" //
+				+ "\n" //
+				+ "public class E {\n" //
+				+ "    public static interface Itf {\n" //
+				+ "        int primitiveConstant = 1;\n" //
+				+ "        String objConstant = \"fkjfkjf\";\n" //
+				+ "        String objNullConstant = null;\n" //
+				+ "        MyEnum enumConstant = MyEnum.NOT_NULL;\n" //
+				+ "        MyEnum enumNullConstant = null;\n" //
+				+ "    }\n" //
+				+ "\n" //
+				+ "    private static enum MyEnum {\n" //
+				+ "        NOT_NULL\n" //
+				+ "    }\n" //
+				+ "\n" //
+				+ "    private int primitiveField;\n" //
+				+ "\n" //
+				+ "    public boolean doNotInvertEqualsOnInstance() {\n" //
+				+ "        return equals(\"\");\n" //
+				+ "    }\n" //
+				+ "\n" //
+				+ "    public boolean doNotInvertEqualsOnThis() {\n" //
+				+ "        return this.equals(\"\");\n" //
+				+ "    }\n" //
+				+ "\n" //
+				+ "    public boolean doNotInvertEqualsWhenParameterIsNull(Object obj) {\n" //
+				+ "        return obj.equals(Itf.objNullConstant) && obj.equals(Itf.enumNullConstant);\n" //
+				+ "    }\n" //
+				+ "\n" //
+				+ "    public boolean doNotInvertEqualsWithPrimitiveParameter(Object obj) {\n" //
+				+ "        return obj.equals(1)\n" //
+				+ "            && obj.equals(Itf.primitiveConstant)\n" //
+				+ "            && obj.equals(primitiveField);\n" //
+				+ "    }\n" //
+				+ "\n" //
+				+ "    public boolean doNotInvertEqualsIgnoreCaseWhenParameterIsNull(String s) {\n" //
+				+ "        return s.equalsIgnoreCase(Itf.objNullConstant);\n" //
+				+ "    }\n" //
+				+ "\n" //
+				+ "    public boolean doNotInvertEqualsOnOperationThatIsNotConcatenation(Integer number, Integer i1, Integer i2) {\n" //
+				+ "        return number.equals(i1 + i2);\n" //
+				+ "    }\n" //
+				+ "}\n";
+		ICompilationUnit cu= pack.createCompilationUnit("E.java", sample, false, null);
+
+		enable(CleanUpConstants.INVERT_EQUALS);
+
+		assertRefactoringHasNoChange(new ICompilationUnit[] { cu });
+	}
+
+	@Test
 	public void testStandardComparison() throws Exception {
 		// Given
 		IPackageFragment pack= fSourceFolder.createPackageFragment("test1", false, null);
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 76a3ed1..ea50e45 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
@@ -151,7 +151,8 @@
 		options.setOption(MODERNIZE_HASH, CleanUpOptions.FALSE);
 		options.setOption(USE_OBJECTS_EQUALS, CleanUpOptions.FALSE);
 
-		// Code fixing
+		// Source fixing
+		options.setOption(INVERT_EQUALS, CleanUpOptions.FALSE);
 		options.setOption(STANDARD_COMPARISON, CleanUpOptions.FALSE);
 		options.setOption(CHECK_SIGN_OF_BITWISE_OPERATION, CleanUpOptions.FALSE);
 
@@ -313,7 +314,8 @@
 
 		options.setOption(CLEANUP_ON_SAVE_ADDITIONAL_OPTIONS, CleanUpOptions.FALSE);
 
-		// Code fixing
+		// Source fixing
+		options.setOption(INVERT_EQUALS, CleanUpOptions.FALSE);
 		options.setOption(STANDARD_COMPARISON, CleanUpOptions.FALSE);
 		options.setOption(CHECK_SIGN_OF_BITWISE_OPERATION, CleanUpOptions.FALSE);
 
diff --git a/org.eclipse.jdt.ui/plugin.xml b/org.eclipse.jdt.ui/plugin.xml
index dbf508f..eac7ceb 100644
--- a/org.eclipse.jdt.ui/plugin.xml
+++ b/org.eclipse.jdt.ui/plugin.xml
@@ -7318,9 +7318,14 @@
             runAfter="org.eclipse.jdt.ui.cleanup.collection_cloning">
       </cleanUp>
       <cleanUp
+            class="org.eclipse.jdt.internal.ui.fix.InvertEqualsCleanUp"
+            id="org.eclipse.jdt.ui.cleanup.invert_equals"
+            runAfter="org.eclipse.jdt.ui.cleanup.map_cloning">
+      </cleanUp>
+      <cleanUp
             class="org.eclipse.jdt.internal.ui.fix.StandardComparisonCleanUp"
             id="org.eclipse.jdt.ui.cleanup.standard_comparison"
-            runAfter="org.eclipse.jdt.ui.cleanup.map_cloning">
+            runAfter="org.eclipse.jdt.ui.cleanup.invert_equals">
       </cleanUp>
       <cleanUp
             class="org.eclipse.jdt.internal.ui.fix.BitwiseConditionalExpressionCleanup"
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/InvertEqualsCleanUp.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/InvertEqualsCleanUp.java
new file mode 100644
index 0000000..72514f7
--- /dev/null
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/InvertEqualsCleanUp.java
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * Copyright (c) 2021 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.Map;
+
+import org.eclipse.core.runtime.CoreException;
+
+import org.eclipse.jdt.core.manipulation.ICleanUpFixCore;
+
+import org.eclipse.jdt.ui.cleanup.CleanUpContext;
+import org.eclipse.jdt.ui.cleanup.CleanUpOptions;
+import org.eclipse.jdt.ui.cleanup.CleanUpRequirements;
+import org.eclipse.jdt.ui.cleanup.ICleanUpFix;
+
+/**
+ * A fix that inverts calls to <code>Object.equals(Object)</code> and <code>String.equalsIgnoreCase(String)</code>:
+ * <ul>
+ * <li>It avoids useless null pointer exception,</li>
+ * <li>The caller must be nullable,</li>
+ * <li>The parameter must not be nullable,</li>
+ * <li>Beware! By avoiding null pointer exception, the behavior may change!</li>
+ * </ul>
+ */
+public class InvertEqualsCleanUp extends AbstractCleanUp {
+	private InvertEqualsCleanUpCore coreCleanUp= new InvertEqualsCleanUpCore();
+
+	public InvertEqualsCleanUp(final Map<String, String> options) {
+		setOptions(options);
+	}
+
+	public InvertEqualsCleanUp() {
+	}
+
+	@Override
+	public void setOptions(final CleanUpOptions options) {
+		coreCleanUp.setOptions(options);
+	}
+
+	@Override
+	public CleanUpRequirements getRequirements() {
+		return new CleanUpRequirements(coreCleanUp.getRequirementsCore());
+	}
+
+	@Override
+	public ICleanUpFix createFix(final CleanUpContext context) throws CoreException {
+		ICleanUpFixCore fixCore= coreCleanUp.createFixCore(context);
+		return fixCore != null ? new CleanUpFixWrapper(fixCore) : null;
+	}
+
+	@Override
+	public String[] getStepDescriptions() {
+		return coreCleanUp.getStepDescriptions();
+	}
+
+	@Override
+	public String getPreview() {
+		return coreCleanUp.getPreview();
+	}
+}
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 952348e..8ec3ba6 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
@@ -163,6 +163,7 @@
 
 	public static String SourceFixingTabPage_GroupName_standardCode;
 
+	public static String SourceFixingTabPage_CheckboxName_InvertEquals;
 	public static String SourceFixingTabPage_CheckboxName_StandardComparison;
 	public static String SourceFixingTabPage_CheckboxName_CheckSignOfBitwiseOperation;
 
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 dec0307..6e3afdc 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
@@ -140,6 +140,7 @@
 
 SourceFixingTabPage_GroupName_standardCode=Code standardization
 
+SourceFixingTabPage_CheckboxName_InvertEquals=Avoid Object::equals or String::equalsIgnoreCase on null objects
 SourceFixingTabPage_CheckboxName_StandardComparison=Compare to zero
 SourceFixingTabPage_CheckboxName_CheckSignOfBitwiseOperation=&Compare with != 0 for bitwise expressions
 
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/SourceFixingTabPage.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/SourceFixingTabPage.java
index 096e5ef..ad5d312 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/SourceFixingTabPage.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/preferences/cleanup/SourceFixingTabPage.java
@@ -27,6 +27,7 @@
 
 import org.eclipse.jdt.internal.ui.fix.AbstractCleanUp;
 import org.eclipse.jdt.internal.ui.fix.BitwiseConditionalExpressionCleanup;
+import org.eclipse.jdt.internal.ui.fix.InvertEqualsCleanUp;
 import org.eclipse.jdt.internal.ui.fix.StandardComparisonCleanUp;
 
 public final class SourceFixingTabPage extends AbstractCleanUpTabPage {
@@ -35,6 +36,7 @@
 	@Override
 	protected AbstractCleanUp[] createPreviewCleanUps(final Map<String, String> values) {
 		return new AbstractCleanUp[] {
+				new InvertEqualsCleanUp(values),
 				new StandardComparisonCleanUp(values),
 				new BitwiseConditionalExpressionCleanup(values)
 		};
@@ -49,6 +51,9 @@
 
 		Group standardCodeGroup= createGroup(numColumns, composite, CleanUpMessages.SourceFixingTabPage_GroupName_standardCode);
 
+		final CheckboxPreference invertEqualsPref= createCheckboxPref(standardCodeGroup, numColumns, CleanUpMessages.SourceFixingTabPage_CheckboxName_InvertEquals, CleanUpConstants.INVERT_EQUALS, CleanUpModifyDialog.FALSE_TRUE);
+		registerPreference(invertEqualsPref);
+
 		final CheckboxPreference standardComparisonPref= createCheckboxPref(standardCodeGroup, numColumns, CleanUpMessages.SourceFixingTabPage_CheckboxName_StandardComparison, CleanUpConstants.STANDARD_COMPARISON, CleanUpModifyDialog.FALSE_TRUE);
 		registerPreference(standardComparisonPref);