Convert 'ReplaceCastWithVariableAction' to 'CastCanBeReplacedWithVariableInspection' and support pattern variables

IDEA-302310

GitOrigin-RevId: 2b364e79c3b2f43fe4729677491c3676ba39d493
This commit is contained in:
Andrey.Cherkasov
2022-09-26 17:31:54 +00:00
committed by intellij-monorepo-bot
parent 8909ecc23b
commit 6ace002822
39 changed files with 353 additions and 201 deletions
@@ -0,0 +1,187 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInspection;
import com.intellij.openapi.progress.ProgressIndicatorProvider;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.controlFlow.*;
import com.intellij.psi.util.JavaPsiPatternUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.psiutils.CommentTracker;
import com.siyeh.ig.psiutils.EquivalenceChecker;
import com.siyeh.ig.psiutils.InstanceOfUtils;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Objects;
/**
* @author Danila Ponomarenko
*/
public class CastCanBeReplacedWithVariableInspection extends AbstractBaseJavaLocalInspectionTool
implements CleanupLocalInspectionTool {
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
return new JavaElementVisitor() {
@Override
public void visitTypeCastExpression(@NotNull PsiTypeCastExpression typeCastExpression) {
final PsiMethod method = PsiTreeUtil.getParentOfType(typeCastExpression, PsiMethod.class);
if (method == null) {
return;
}
final PsiExpression operand = PsiUtil.skipParenthesizedExprDown(typeCastExpression.getOperand());
if (!(operand instanceof PsiReferenceExpression operandReference)) {
return;
}
final PsiElement resolved = operandReference.resolve();
if (!(resolved instanceof PsiParameter) && !(resolved instanceof PsiLocalVariable)) {
return;
}
final PsiVariable replacement = findReplacement(method, (PsiVariable)resolved, typeCastExpression);
if (replacement == null) {
return;
}
final String variableName = replacement.getName();
final String castExpressionText = typeCastExpression.getText();
final LocalQuickFix fix = new ReplaceCastWithVariableFix(castExpressionText, replacement);
holder.registerProblem(typeCastExpression,
InspectionGadgetsBundle.message("inspection.cast.can.be.replaced.with.variable.message",
variableName, castExpressionText), fix);
}
};
}
@Nullable
private static PsiVariable findReplacement(@NotNull PsiMethod method,
@NotNull PsiVariable castedVar,
@NotNull PsiTypeCastExpression expression) {
final TextRange expressionTextRange = expression.getTextRange();
if (InstanceOfUtils.isUncheckedCast(expression)) return null;
PsiExpression operand = Objects.requireNonNull(PsiUtil.skipParenthesizedExprDown(expression.getOperand()));
PsiType castType = Objects.requireNonNull(expression.getCastType()).getType();
List<PsiTypeCastExpression> found =
SyntaxTraverser.psiTraverser(method)
.filter(PsiTypeCastExpression.class)
.filter(cast -> EquivalenceChecker.getCanonicalPsiEquivalence().expressionsAreEquivalent(cast.getOperand(), operand))
.filter(cast -> {
PsiTypeElement typeElement = cast.getCastType();
return typeElement != null && InstanceOfUtils.typeCompatible(typeElement.getType(), castType, operand);
})
.toList();
PsiResolveHelper resolveHelper = PsiResolveHelper.getInstance(method.getProject());
final PsiCodeBlock methodBody = method.getBody();
if (methodBody == null) return null;
for (PsiTypeCastExpression occurrence : found) {
ProgressIndicatorProvider.checkCanceled();
final TextRange occurrenceTextRange = occurrence.getTextRange();
if (occurrence == expression || occurrenceTextRange.getEndOffset() >= expressionTextRange.getStartOffset()) {
continue;
}
final PsiLocalVariable variable = getVariable(occurrence);
if (variable != null &&
resolveHelper.resolveReferencedVariable(variable.getName(), expression) == variable &&
!isChangedBetween(castedVar, methodBody, occurrence, expression) &&
!isChangedBetween(variable, methodBody, occurrence, expression)) {
return variable;
}
}
PsiInstanceOfExpression instanceOf = InstanceOfUtils.findPatternCandidate(expression);
if (instanceOf != null) {
PsiPattern pattern = instanceOf.getPattern();
PsiPatternVariable patternVariable = JavaPsiPatternUtil.getPatternVariable(pattern);
if (patternVariable != null &&
!isChangedBetween(castedVar, methodBody, instanceOf, expression) &&
!isChangedBetween(patternVariable, methodBody, instanceOf, expression)) {
return patternVariable;
}
}
return null;
}
private static boolean isChangedBetween(@NotNull final PsiVariable variable,
@NotNull final PsiElement scope,
@NotNull final PsiElement start,
@NotNull final PsiElement end) {
if (variable.hasModifierProperty(PsiModifier.FINAL)) {
return false;
}
ControlFlow controlFlow;
try {
controlFlow = ControlFlowFactory.getInstance(variable.getProject()).getControlFlow(scope, new LocalsControlFlowPolicy(scope), true);
}
catch (AnalysisCanceledException ignored) {
controlFlow = ControlFlow.EMPTY;
}
int startOffset = controlFlow.getEndOffset(start) + 1;
int endOffset = controlFlow.getEndOffset(end);
return ControlFlowUtil.getWrittenVariables(controlFlow, startOffset, endOffset, true).contains(variable);
}
@Nullable
private static PsiLocalVariable getVariable(@NotNull PsiExpression occurrence) {
final PsiElement parent = PsiUtil.skipParenthesizedExprUp(occurrence.getParent());
if (parent instanceof PsiLocalVariable localVariable) {
return localVariable;
}
if (parent instanceof PsiAssignmentExpression assignmentExpression &&
assignmentExpression.getLExpression() instanceof PsiReferenceExpression referenceExpression &&
referenceExpression.resolve() instanceof PsiLocalVariable localVariable) {
return localVariable;
}
return null;
}
private static class ReplaceCastWithVariableFix implements LocalQuickFix {
private final @NotNull String myText;
private final @NotNull String myVariableName;
private ReplaceCastWithVariableFix(@NotNull String text, @NotNull PsiVariable variable) {
myText = text;
myVariableName = Objects.requireNonNull(variable.getName());
}
@Nls(capitalization = Nls.Capitalization.Sentence)
@NotNull
@Override
public String getName() {
return CommonQuickFixBundle.message("fix.replace.x.with.y", myText, myVariableName);
}
@Nls(capitalization = Nls.Capitalization.Sentence)
@NotNull
@Override
public String getFamilyName() {
return InspectionGadgetsBundle.message("inspection.cast.can.be.replaced.with.variable.family.name");
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
final PsiElement element = descriptor.getPsiElement();
if (element instanceof PsiTypeCastExpression typeCastExpression) {
final PsiElement toReplace =
typeCastExpression.getParent() instanceof PsiParenthesizedExpression ? typeCastExpression.getParent() : typeCastExpression;
new CommentTracker().replaceAndRestoreComments(toReplace, myVariableName);
}
}
}
}
@@ -1952,12 +1952,6 @@
<bundleName>messages.JavaBundle</bundleName>
<categoryKey>intention.category.strings</categoryKey>
</intentionAction>
<intentionAction>
<language>JAVA</language>
<className>com.intellij.codeInsight.intention.impl.ReplaceCastWithVariableAction</className>
<bundleName>messages.JavaBundle</bundleName>
<categoryKey>intention.category.declaration</categoryKey>
</intentionAction>
<intentionAction>
<language>JAVA</language>
<className>com.intellij.codeInsight.intention.impl.SortContentAction</className>
@@ -1,176 +0,0 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.intention.impl;
import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction;
import com.intellij.java.JavaBundle;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.progress.ProgressIndicatorProvider;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.IncorrectOperationException;
import com.siyeh.ig.psiutils.CommentTracker;
import com.siyeh.ig.psiutils.EquivalenceChecker;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
/**
* @author Danila Ponomarenko
*/
public class ReplaceCastWithVariableAction extends PsiElementBaseIntentionAction {
private String myReplaceVariableName = "";
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) {
final PsiTypeCastExpression typeCastExpression = PsiTreeUtil.getParentOfType(element, PsiTypeCastExpression.class);
final PsiMethod method = PsiTreeUtil.getParentOfType(element, PsiMethod.class);
if (typeCastExpression == null || method == null) {
return false;
}
final PsiExpression operand = PsiUtil.skipParenthesizedExprDown(typeCastExpression.getOperand());
if (!(operand instanceof PsiReferenceExpression)) {
return false;
}
final PsiReferenceExpression operandReference = (PsiReferenceExpression)operand;
final PsiElement resolved = operandReference.resolve();
if (!(resolved instanceof PsiParameter) && !(resolved instanceof PsiLocalVariable)) {
return false;
}
final PsiLocalVariable replacement = findReplacement(method, (PsiVariable)resolved, typeCastExpression);
if (replacement == null) {
return false;
}
myReplaceVariableName = replacement.getName();
setText(JavaBundle.message("intention.replace.cast.with.var.text", typeCastExpression.getText(), myReplaceVariableName));
return true;
}
@Override
public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element) throws IncorrectOperationException {
final PsiTypeCastExpression typeCastExpression = PsiTreeUtil.getParentOfType(element, PsiTypeCastExpression.class);
if (typeCastExpression == null) {
return;
}
final PsiElement toReplace = typeCastExpression.getParent() instanceof PsiParenthesizedExpression ? typeCastExpression.getParent() : typeCastExpression;
final PsiElementFactory factory = JavaPsiFacade.getElementFactory(project);
new CommentTracker().replaceAndRestoreComments(toReplace, factory.createExpressionFromText(myReplaceVariableName, toReplace));
}
@Nullable
private static PsiLocalVariable findReplacement(@NotNull PsiMethod method,
@NotNull PsiVariable castedVar,
@NotNull PsiTypeCastExpression expression) {
final TextRange expressionTextRange = expression.getTextRange();
PsiExpression operand = PsiUtil.skipParenthesizedExprDown(expression.getOperand());
List<PsiTypeCastExpression> found =
SyntaxTraverser.psiTraverser(method)
.filter(PsiTypeCastExpression.class)
.filter(cast -> EquivalenceChecker.getCanonicalPsiEquivalence().expressionsAreEquivalent(cast.getOperand(), operand))
.toList();
PsiResolveHelper resolveHelper = PsiResolveHelper.getInstance(method.getProject());
for (PsiTypeCastExpression occurrence : found) {
ProgressIndicatorProvider.checkCanceled();
final TextRange occurrenceTextRange = occurrence.getTextRange();
if (occurrence == expression || occurrenceTextRange.getEndOffset() >= expressionTextRange.getStartOffset()) {
continue;
}
final PsiLocalVariable variable = getVariable(occurrence);
final PsiCodeBlock methodBody = method.getBody();
if (variable != null && methodBody != null &&
resolveHelper.resolveReferencedVariable(variable.getName(), expression) == variable &&
!isChangedBetween(castedVar, methodBody, occurrence, expression) &&
!isChangedBetween(variable, methodBody, occurrence, expression)) {
return variable;
}
}
return null;
}
private static boolean isChangedBetween(@NotNull final PsiVariable variable,
@NotNull final PsiElement scope,
@NotNull final PsiElement start,
@NotNull final PsiElement end) {
if (variable.hasModifierProperty(PsiModifier.FINAL)) {
return false;
}
final Ref<Boolean> result = new Ref<>();
scope.accept(
new JavaRecursiveElementWalkingVisitor() {
private boolean inScope;
@Override
public void visitElement(@NotNull PsiElement element) {
if (element == start) {
inScope = true;
}
if (element == end) {
inScope = false;
stopWalking();
}
super.visitElement(element);
}
@Override
public void visitAssignmentExpression(@NotNull PsiAssignmentExpression expression) {
if (inScope && expression.getLExpression() instanceof PsiReferenceExpression) {
final PsiReferenceExpression referenceExpression = (PsiReferenceExpression)expression.getLExpression();
if (variable.equals(referenceExpression.resolve())) {
result.set(true);
stopWalking();
}
}
super.visitAssignmentExpression(expression);
}
}
);
return result.get() == Boolean.TRUE;
}
@Nullable
private static PsiLocalVariable getVariable(@NotNull PsiExpression occurrence) {
final PsiElement parent = PsiUtil.skipParenthesizedExprUp(occurrence.getParent());
if (parent instanceof PsiLocalVariable) {
return (PsiLocalVariable)parent;
}
if (parent instanceof PsiAssignmentExpression) {
final PsiAssignmentExpression assignmentExpression = (PsiAssignmentExpression)parent;
if (assignmentExpression.getLExpression() instanceof PsiReferenceExpression) {
final PsiReferenceExpression referenceExpression = (PsiReferenceExpression)assignmentExpression.getLExpression();
final PsiElement resolved = referenceExpression.resolve();
if (resolved instanceof PsiLocalVariable) {
return (PsiLocalVariable)resolved;
}
}
}
return null;
}
@NotNull
@Override
public String getFamilyName() {
return JavaBundle.message("intention.replace.cast.with.var.family");
}
}
@@ -0,0 +1,21 @@
<html>
<body>
Reports type cast operations that can be replaced with existing local or pattern variables with the same value.
<p>Example:</p>
<pre><code>
void foo(Object obj) {
String s = (String) obj;
System.out.println(((String) obj).trim());
}
</code></pre>
<p>After the quick-fix is applied:</p>
<pre><code>
void foo(Object obj) {
String s = (String) obj;
System.out.println(s.trim());
}
</code></pre>
<!-- tooltip end -->
<p><small>New in 2022.3</small></p>
</body>
</html>
@@ -1,2 +0,0 @@
FooBar foobar = (FooBar) foo;
return foobar.baz;
@@ -1,2 +0,0 @@
FooBar foobar = (FooBar) foo;
return <spot>((FooBar) foo)</spot>.baz;
@@ -1,5 +0,0 @@
<html>
<body>
Replaces a type cast expression with an existing local variable with the same value.
</body>
</html>
@@ -0,0 +1,8 @@
// "Replace '(String) obj' with 's'" "true-preview"
class X {
void foo(Object obj) {
String s = (String) obj;
System.out.println(s.trim());
}
}
@@ -0,0 +1,12 @@
// "Replace '(String) o' with 's1'" "true-preview"
class C {
void foo(Object o) {
String s1 = (String) o;
if (Math.random() > 0.5) {
o = null;
return;
}
String s2 = s1;
}
}
@@ -0,0 +1,12 @@
// "Replace '(String) o' with 's1'" "true-preview"
class C {
void foo(Object o) {
String s1 = (String) o;
if (Math.random() > 0.5) {
s1 = null;
return;
}
String s2 = s1;
}
}
@@ -0,0 +1,8 @@
// "Replace '(String) obj' with 's1'" "false"
class X {
void test(Object obj) {
String s1 = (String) obj, s2 = s1 = "blah blah blah";
String s3 = (String) ob<caret>j;
}
}
@@ -0,0 +1,9 @@
// "Replace '(String) obj' with 's1'" "false"
class X {
void test(Object obj, String s) {
if (obj instanceof String s1 && (s1 = "blah blah blah").equals(s)) {
String s2 = (String) ob<caret>j;
}
}
}
@@ -0,0 +1,8 @@
// "Replace '(Integer) obj' with 's'" "false"
class X {
void test(Object obj) {
String s = (String) obj;
Integer i = ((Integer) ob<caret>j).intValue();
}
}
@@ -0,0 +1,8 @@
// "Replace '(String) obj' with 's'" "true-preview"
class X {
void foo(Object obj) {
String s = (String) obj;
System.out.println(((String) ob<caret>j).trim());
}
}
@@ -0,0 +1,12 @@
// "Replace '(String) o' with 's1'" "true-preview"
class C {
void foo(Object o) {
String s1 = (String) o;
if (Math.random() > 0.5) {
o = null;
return;
}
String s2 = (String) o<caret>;
}
}
@@ -0,0 +1,12 @@
// "Replace '(String) o' with 's1'" "true-preview"
class C {
void foo(Object o) {
String s1 = (String) o;
if (Math.random() > 0.5) {
s1 = null;
return;
}
String s2 = (String) o<caret>;
}
}
@@ -0,0 +1,10 @@
// "Replace '(ArrayList<Integer>) obj' with 'arrayList'" "false"
import java.util.*;
class X {
void test(List obj) {
ArrayList arrayList = (ArrayList) obj;
Integer list = ((ArrayList<Integer>) ob<caret>j).get(0);
}
}
@@ -0,0 +1,26 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.codeInspection;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.codeInspection.CastCanBeReplacedWithVariableInspection;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import org.jetbrains.annotations.NotNull;
public class CastCanBeReplacedWithVariableInspectionTest extends LightQuickFixParameterizedTestCase {
@Override
protected LocalInspectionTool @NotNull [] configureLocalInspectionTools() {
return new LocalInspectionTool[]{new CastCanBeReplacedWithVariableInspection()};
}
@Override
protected String getBasePath() {
return "/inspection/castCanBeReplacedWithVariable";
}
@Override
protected @NotNull LightProjectDescriptor getProjectDescriptor() {
return LightJavaCodeInsightFixtureTestCase.JAVA_16;
}
}
@@ -859,8 +859,6 @@ intention.move.field.assignment.to.declaration=Move assignment to field declarat
intention.move.initializer.to.constructor=Move initializer to constructor
intention.move.initializer.to.set.up=Move initializer to setUp method
intention.override.method.text=Override method ''{0}''
intention.replace.cast.with.var.family=Replace cast with variable
intention.replace.cast.with.var.text=Replace ''{0}'' with ''{1}''
intention.replace.concatenation.with.formatted.output.family=Replace concatenation with formatted output
intention.replace.concatenation.with.formatted.output.text=Replace '+' with 'java.text.MessageFormat.format()'
intention.split.declaration.assignment.text=Split into declaration and assignment
@@ -2303,6 +2303,10 @@ inspection.pattern.variable.can.be.used.existing.message=Existing pattern variab
inspection.pattern.variable.can.be.used.existing.fix.family.name=Replace with existing pattern variable
inspection.pattern.variable.can.be.used.existing.fix.name=Replace ''{0}'' with existing pattern variable ''{1}''
inspection.cast.can.be.replaced.with.variable.display.name=Cast can be replaced with variable
inspection.cast.can.be.replaced.with.variable.message=Variable ''{0}'' can be used instead of ''{1}''
inspection.cast.can.be.replaced.with.variable.family.name=Replace cast with variable
array.hash.code.fix.family.name=Replace with 'Arrays.hashCode()' call
objects.hash.fix.family.name=Wrap with 'Arrays.hashCode()'
unqualified.static.access.fix.family.name=Qualify static access
@@ -183,16 +183,20 @@ public final class InstanceOfUtils {
*/
@Nullable
public static PsiInstanceOfExpression findPatternCandidate(@NotNull PsiTypeCastExpression cast) {
PsiTypeElement castType = cast.getCastType();
if (castType == null) return null;
PsiExpression castOperand = cast.getOperand();
if (castOperand == null) return null;
PsiType type = castOperand.getType();
if (type == null) return null;
if (JavaGenericsUtil.isUncheckedCast(castType.getType(), type)) return null;
if (isUncheckedCast(cast)) return null;
return findCorrespondingInstanceOf(cast);
}
public static boolean isUncheckedCast(@NotNull PsiTypeCastExpression cast) {
PsiTypeElement castType = cast.getCastType();
if (castType == null) return true;
PsiExpression castOperand = cast.getOperand();
if (castOperand == null) return true;
PsiType type = castOperand.getType();
if (type == null) return true;
return JavaGenericsUtil.isUncheckedCast(castType.getType(), type);
}
/**
* @param cast a cast expression to find parent instanceof for
* @return an instanceof expression that checks for the same raw type as the cast.
@@ -376,7 +380,7 @@ public final class InstanceOfUtils {
return null;
}
private static boolean typeCompatible(@NotNull PsiType instanceOfType, @NotNull PsiType castType, @NotNull PsiExpression castOperand) {
public static boolean typeCompatible(@NotNull PsiType instanceOfType, @NotNull PsiType castType, @NotNull PsiExpression castOperand) {
if (instanceOfType.equals(castType)) return true;
if (castType instanceof PsiClassType) {
PsiClassType rawType = ((PsiClassType)castType).rawType();
@@ -2769,6 +2769,10 @@
bundle="messages.InspectionGadgetsBundle" key="inspection.pattern.variable.can.be.used.display.name"
groupBundle="messages.InspectionsBundle" groupKey="group.names.language.level.specific.issues.and.migration.aids16"
implementationClass="com.intellij.codeInspection.PatternVariableCanBeUsedInspection" cleanupTool="true"/>
<localInspection groupPath="Java" language="JAVA" shortName="CastCanBeReplacedWithVariable" enabledByDefault="true" level="WARNING"
bundle="messages.InspectionGadgetsBundle" key="inspection.cast.can.be.replaced.with.variable.display.name"
groupBundle="messages.InspectionsBundle" groupKey="group.names.verbose.or.redundant.code.constructs"
implementationClass="com.intellij.codeInspection.CastCanBeReplacedWithVariableInspection" cleanupTool="true"/>
<localInspection groupPathKey="group.path.names.java.language.level.specific.issues.and.migration.aids" language="JAVA" shortName="VariableTypeCanBeExplicit" enabledByDefault="true" level="INFORMATION"
bundle="messages.InspectionGadgetsBundle" key="variable.type.can.be.explicit.display.name"
groupBundle="messages.InspectionsBundle" groupKey="group.names.language.level.specific.issues.and.migration.aids10"