diff --git a/java/java-analysis-api/resources/messages/JavaAnalysisBundle.properties b/java/java-analysis-api/resources/messages/JavaAnalysisBundle.properties index ee17bfbf1234..9b9787c0c640 100644 --- a/java/java-analysis-api/resources/messages/JavaAnalysisBundle.properties +++ b/java/java-analysis-api/resources/messages/JavaAnalysisBundle.properties @@ -662,5 +662,4 @@ error.unnamed.variable.without.initializer=Unnamed variable declaration must hav intention.family.name.move.members.into.class=Move members into class chooser.popup.title.select.class.to.move.members.to=Select Target Class intention.family.name.move.members.to=Move members to {0} -inspection.message.can.be.replaced.with.long.hashcode=Can be replaced with 'Long.hashCode()' -inspection.name.can.be.replaced.with.long.hashcode='Long.hashCode()' can be used +inspection.name.can.be.replaced.with.long.hashcode=Standard 'hashCode()' method can be used diff --git a/java/java-impl-inspections/src/com/intellij/codeInspection/UseHashCodeMethodInspection.java b/java/java-impl-inspections/src/com/intellij/codeInspection/UseHashCodeMethodInspection.java index e50dfbca176e..b2e779358046 100644 --- a/java/java-impl-inspections/src/com/intellij/codeInspection/UseHashCodeMethodInspection.java +++ b/java/java-impl-inspections/src/com/intellij/codeInspection/UseHashCodeMethodInspection.java @@ -7,10 +7,11 @@ import com.intellij.modcommand.PsiUpdateModCommandQuickFix; import com.intellij.openapi.project.Project; import com.intellij.psi.*; import com.intellij.psi.util.PsiPrecedenceUtil; +import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.PsiUtil; -import com.siyeh.ig.psiutils.CommentTracker; -import com.siyeh.ig.psiutils.EquivalenceChecker; -import com.siyeh.ig.psiutils.ExpressionUtils; +import com.intellij.util.containers.ContainerUtil; +import com.siyeh.ig.callMatcher.CallMatcher; +import com.siyeh.ig.psiutils.*; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -18,6 +19,9 @@ import org.jetbrains.annotations.Nullable; import java.util.Objects; public class UseHashCodeMethodInspection extends AbstractBaseJavaLocalInspectionTool { + private static final CallMatcher DOUBLE_TO_LONG_BITS = + CallMatcher.staticCall("java.lang.Double", "doubleToLongBits").parameterTypes("double"); + @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { @@ -25,18 +29,46 @@ public class UseHashCodeMethodInspection extends AbstractBaseJavaLocalInspection return new JavaElementVisitor() { @Override public void visitTypeCastExpression(@NotNull PsiTypeCastExpression expression) { - if (getHashCodeOperand(expression) != null) { - holder.registerProblem(expression, JavaAnalysisBundle.message("inspection.message.can.be.replaced.with.long.hashcode"), - new ReplaceWithLongHashCodeFix()); + HashCodeModel model = getHashCodeModel(expression); + if (model != null) { + PsiClass containingClass = ClassUtils.getContainingClass(expression); + if (containingClass != null && containingClass.getQualifiedName() != null && + containingClass.getQualifiedName().startsWith("java.lang.")) { + // Avoid suggesting inside JDK sources + return; + } + holder.registerProblem(expression, + JavaAnalysisBundle.message("inspection.can.be.replaced.with.message", model.type + ".hashCode()"), + new ReplaceWithLongHashCodeFix(model.type)); } } }; } - private static @Nullable PsiExpression getHashCodeOperand(PsiTypeCastExpression typeCastExpression) { - if (typeCastExpression == null) return null; - if (!PsiTypes.intType().equals(typeCastExpression.getType())) return null; - PsiExpression operand = PsiUtil.skipParenthesizedExprDown(typeCastExpression.getOperand()); + record HashCodeModel(@NotNull PsiExpression completeExpression, + @NotNull PsiExpression argument, + @Nullable PsiLocalVariable intermediateVariable, + @NotNull String type) { + @NotNull HashCodeModel tryReplaceDouble() { + PsiLocalVariable local = ExpressionUtils.resolveLocalVariable(argument); + if (local == null) return this; + if (!(PsiUtil.skipParenthesizedExprDown(local.getInitializer()) instanceof PsiMethodCallExpression call)) return this; + if (!DOUBLE_TO_LONG_BITS.matches(call)) return this; + if (!(local.getParent() instanceof PsiDeclarationStatement decl) || decl.getDeclaredElements().length != 1) return this; + PsiElement nextDeclaration = PsiTreeUtil.skipWhitespacesAndCommentsForward(decl); + if (!PsiTreeUtil.isAncestor(nextDeclaration, completeExpression, true)) return this; + if (ContainerUtil.exists(VariableAccessUtils.getVariableReferences(local, PsiUtil.getVariableCodeBlock(local, null)), + ref -> !PsiTreeUtil.isAncestor(completeExpression, ref, true))) { + return this; + } + return new HashCodeModel(completeExpression, call.getArgumentList().getExpressions()[0], local, "Double"); + } + } + + private static @Nullable HashCodeModel getHashCodeModel(PsiTypeCastExpression cast) { + if (cast == null) return null; + if (!PsiTypes.intType().equals(cast.getType())) return null; + PsiExpression operand = PsiUtil.skipParenthesizedExprDown(cast.getOperand()); if (!(operand instanceof PsiBinaryExpression binaryExpression)) return null; PsiJavaToken operationSign = binaryExpression.getOperationSign(); @@ -48,8 +80,8 @@ public class UseHashCodeMethodInspection extends AbstractBaseJavaLocalInspection if (leftOperand == null || rightOperand == null) return null; if (!PsiTypes.longType().equals(PsiPrimitiveType.getOptionallyUnboxedType(leftOperand.getType()))) return null; - if (isXorShift(leftOperand, rightOperand)) return leftOperand; - if (isXorShift(rightOperand, leftOperand)) return rightOperand; + if (isXorShift(leftOperand, rightOperand)) return new HashCodeModel(cast, leftOperand, null, "Long").tryReplaceDouble(); + if (isXorShift(rightOperand, leftOperand)) return new HashCodeModel(cast, rightOperand, null, "Long").tryReplaceDouble(); return null; } @@ -67,25 +99,39 @@ public class UseHashCodeMethodInspection extends AbstractBaseJavaLocalInspection } public static class ReplaceWithLongHashCodeFix extends PsiUpdateModCommandQuickFix { + private final String myType; + + public ReplaceWithLongHashCodeFix(String type) { + myType = type; + } + + @Override + public @NotNull String getName() { + return CommonQuickFixBundle.message("fix.replace.with.x", myType+".hashCode()"); + } + @Nls @NotNull @Override public String getFamilyName() { - return CommonQuickFixBundle.message("fix.replace.with.x", "Long.hashCode()"); + return CommonQuickFixBundle.message("fix.replace.with.x", "hashCode()"); } @Override protected void applyFix(@NotNull Project project, @NotNull PsiElement startElement, @NotNull ModPsiUpdater updater) { PsiTypeCastExpression element = (PsiTypeCastExpression)startElement; - PsiExpression operand = getHashCodeOperand(element); - if (operand != null) { - PsiType type = operand.getType(); - CommentTracker ct = new CommentTracker(); - String call = PsiTypes.longType().equals(type) - ? "Long.hashCode(" + ct.text(operand) + ")" - : ct.text(operand, PsiPrecedenceUtil.METHOD_CALL_PRECEDENCE) + ".hashCode()"; - ct.replace(element, call); + HashCodeModel model = getHashCodeModel(element); + if (model == null) return; + PsiExpression argument = model.argument(); + PsiType type = argument.getType(); + CommentTracker ct = new CommentTracker(); + String call = type instanceof PsiPrimitiveType + ? "java.lang." + model.type() + ".hashCode(" + ct.text(argument) + ")" + : ct.text(argument, PsiPrecedenceUtil.METHOD_CALL_PRECEDENCE) + ".hashCode()"; + if (model.intermediateVariable() != null) { + ct.delete(model.intermediateVariable()); } + ct.replaceAndRestoreComments(element, call); } } } \ No newline at end of file diff --git a/java/java-impl/src/inspectionDescriptions/UseHashCodeMethodInspection.html b/java/java-impl/src/inspectionDescriptions/UseHashCodeMethodInspection.html index ed32623af34c..830d942e8df1 100644 --- a/java/java-impl/src/inspectionDescriptions/UseHashCodeMethodInspection.html +++ b/java/java-impl/src/inspectionDescriptions/UseHashCodeMethodInspection.html @@ -1,9 +1,9 @@

-Informs you when bitwise operations can be replaced with the Long.hashCode method. - It detects instances of the pattern (int)(x ^ (x >>> 32)) where x is a variable of type long. - This improves readability of code. +Informs you when bitwise operations can be replaced with the Long.hashCode() or Double.hashCode() method. + It detects instances of the pattern (int)(x ^ (x >>> 32)) where x is a variable of type long or + the result of the previous Double.doubleToLongBits() call. This replacement shortens the code improves its readability.

Example: diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterBoxed.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterBoxed.java index a698c23cc4f6..9e802d9bbfa7 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterBoxed.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterBoxed.java @@ -3,6 +3,7 @@ public class Test { Long var = 1234567890123456789L; public void testMethod() { - int result = var.hashCode(); + /*shift amount*/ + int result = var.hashCode(); } } \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterBoxedExpr.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterBoxedExpr.java index ac3c928f64d9..25ca602d1028 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterBoxedExpr.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterBoxedExpr.java @@ -4,6 +4,7 @@ public class Test { Long var1 = 1234567890123456784L; public void testMethod(boolean f) { - int result = (f ? var : var1).hashCode(); + /*shift amount*/ + int result = (f ? var : var1).hashCode(); } } \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterDouble.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterDouble.java new file mode 100644 index 000000000000..baca2f6a4104 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterDouble.java @@ -0,0 +1,6 @@ +// "Replace with 'Double.hashCode()'" "true-preview" +public class Test { + void test(double d) { + System.out.println(Double.hashCode(d)); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterDoubleBoxed.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterDoubleBoxed.java new file mode 100644 index 000000000000..d60b0b805d99 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterDoubleBoxed.java @@ -0,0 +1,7 @@ +// "Replace with 'Double.hashCode()'" "true-preview" +public class Test { + void test(Double d) { + /*comment*/ + System.out.println(d.hashCode()); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterNoDouble.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterNoDouble.java new file mode 100644 index 000000000000..9909e1fab33a --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterNoDouble.java @@ -0,0 +1,8 @@ +// "Replace with 'Long.hashCode()'" "true-preview" +public class Test { + void test(double d) { + long l = Double.doubleToLongBits(d); + System.out.println(Long.hashCode(l)); + System.out.println(l); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterNoDouble2.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterNoDouble2.java new file mode 100644 index 000000000000..c3ef6b005134 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterNoDouble2.java @@ -0,0 +1,8 @@ +// "Replace with 'Long.hashCode()'" "true-preview" +public class Test { + void test(double d) { + long l = Double.doubleToLongBits(d); + d++; + System.out.println(Long.hashCode(l)); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterSimple.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterSimple.java index 75a1bb8333a2..6b7c0c6ee887 100644 --- a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterSimple.java +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/afterSimple.java @@ -3,6 +3,7 @@ public class Test { long var = 1234567890123456789L; public void testMethod() { - int result = Long.hashCode(var); + /*shift amount*/ + int result = Long.hashCode(var); } } \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/beforeDouble.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/beforeDouble.java new file mode 100644 index 000000000000..fd4d664ddeef --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/beforeDouble.java @@ -0,0 +1,7 @@ +// "Replace with 'Double.hashCode()'" "true-preview" +public class Test { + void test(double d) { + long l = Double.doubleToLongBits(d); + System.out.println((int) (l ^ (l >>> 32))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/beforeDoubleBoxed.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/beforeDoubleBoxed.java new file mode 100644 index 000000000000..4faa6ab75d32 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/beforeDoubleBoxed.java @@ -0,0 +1,7 @@ +// "Replace with 'Double.hashCode()'" "true-preview" +public class Test { + void test(Double d) { + long l = Double.doubleToLongBits(/*comment*/d); + System.out.println((int) (l ^ (l >>> 32))); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/beforeNoDouble.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/beforeNoDouble.java new file mode 100644 index 000000000000..c7e7526d621c --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/beforeNoDouble.java @@ -0,0 +1,8 @@ +// "Replace with 'Long.hashCode()'" "true-preview" +public class Test { + void test(double d) { + long l = Double.doubleToLongBits(d); + System.out.println((int) (l ^ (l >>> 32))); + System.out.println(l); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/beforeNoDouble2.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/beforeNoDouble2.java new file mode 100644 index 000000000000..cb654eb3c643 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/useHashCode/beforeNoDouble2.java @@ -0,0 +1,8 @@ +// "Replace with 'Long.hashCode()'" "true-preview" +public class Test { + void test(double d) { + long l = Double.doubleToLongBits(d); + d++; + System.out.println((int) (l ^ (l >>> 32))); + } +} \ No newline at end of file