IDEA-195069 Quick Fix: "Transform to mutable collection" in response to "Immutable object is modified" inspection

This commit is contained in:
Tagir Valeev
2018-07-09 16:21:45 +07:00
parent 36e0c17ad4
commit 99387de7f5
10 changed files with 223 additions and 2 deletions

View File

@@ -331,17 +331,21 @@ public class DataFlowInspectionBase extends AbstractBaseJavaLocalInspectionTool
});
}
private static void reportMutabilityViolations(ProblemsHolder holder,
private void reportMutabilityViolations(ProblemsHolder holder,
Set<PsiElement> reportedAnchors,
Set<PsiElement> violations,
String message) {
for (PsiElement violation : violations) {
if (reportedAnchors.add(violation)) {
holder.registerProblem(violation, message);
holder.registerProblem(violation, message, createMutabilityViolationFix(holder, violation));
}
}
}
protected LocalQuickFix createMutabilityViolationFix(ProblemsHolder holder, PsiElement violation) {
return null;
}
private void reportNullabilityProblems(ProblemsHolder holder,
DataFlowInstructionVisitor visitor,
HashSet<PsiElement> reportedAnchors) {

View File

@@ -0,0 +1,115 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInspection;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightControlFlowUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.impl.PsiDiamondTypeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.ObjectUtils;
import com.siyeh.ig.psiutils.CommentTracker;
import com.siyeh.ig.psiutils.ExpressionUtils;
import com.siyeh.ig.psiutils.HighlightUtils;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class WrapWithMutableCollectionFix implements LocalQuickFix {
private final String myVariableName;
private final String myCollectionName;
private final boolean myOnTheFly;
public WrapWithMutableCollectionFix(String variableName, String collectionName, boolean onTheFly) {
myVariableName = variableName;
myCollectionName = collectionName;
myOnTheFly = onTheFly;
}
@Nls(capitalization = Nls.Capitalization.Sentence)
@NotNull
@Override
public String getName() {
return "Wrap '"+myVariableName+"' with '" + StringUtil.getShortName(myCollectionName) + "'";
}
@Nls(capitalization = Nls.Capitalization.Sentence)
@NotNull
@Override
public String getFamilyName() {
return "Wrap with mutable collection";
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiLocalVariable variable = getVariable(descriptor.getStartElement());
if (variable == null) return;
PsiExpression initializer = variable.getInitializer();
if (initializer == null) return;
PsiClassType type = ObjectUtils.tryCast(variable.getType(), PsiClassType.class);
if (type == null) return;
String typeParameters = "";
if (myCollectionName.equals(CommonClassNames.JAVA_UTIL_HASH_MAP)) {
PsiType keyParameter = PsiUtil.substituteTypeParameter(type, CommonClassNames.JAVA_UTIL_MAP, 0, false);
PsiType valueParameter = PsiUtil.substituteTypeParameter(type, CommonClassNames.JAVA_UTIL_MAP, 1, false);
if (keyParameter != null && valueParameter != null) {
typeParameters = "<"+keyParameter.getCanonicalText()+","+valueParameter.getCanonicalText()+">";
}
} else {
PsiType elementParameter = PsiUtil.substituteTypeParameter(type, CommonClassNames.JAVA_LANG_ITERABLE, 0, false);
if (elementParameter != null) {
typeParameters = "<" + elementParameter.getCanonicalText() + ">";
}
}
CommentTracker ct = new CommentTracker();
PsiElement replacement =
ct.replaceAndRestoreComments(initializer, "new " + myCollectionName + typeParameters + "(" + ct.text(initializer) + ")");
PsiDiamondTypeUtil.removeRedundantTypeArguments(replacement);
if (myOnTheFly) {
HighlightUtils.highlightElement(replacement);
}
}
@Nullable
public static WrapWithMutableCollectionFix createFix(@NotNull PsiElement anchor, boolean onTheFly) {
PsiLocalVariable variable = getVariable(anchor);
if (variable == null) return null;
PsiExpression initializer = variable.getInitializer();
if (initializer == null) return null;
String wrapper = getWrapperByType(variable.getType());
if (wrapper == null) return null;
PsiElement block = PsiUtil.getVariableCodeBlock(variable, null);
if (block == null) return null;
if (!HighlightControlFlowUtil.isEffectivelyFinal(variable, block, null)) return null;
return new WrapWithMutableCollectionFix(variable.getName(), wrapper, onTheFly);
}
@Nullable
private static PsiLocalVariable getVariable(@NotNull PsiElement anchor) {
if (anchor.getParent() instanceof PsiReferenceExpression && anchor.getParent().getParent() instanceof PsiCallExpression) {
anchor = ((PsiReferenceExpression)anchor.getParent()).getQualifierExpression();
}
if (!(anchor instanceof PsiExpression)) return null;
return ExpressionUtils.resolveLocalVariable(PsiUtil.skipParenthesizedExprDown((PsiExpression)anchor));
}
@Contract("null -> null")
@Nullable
private static String getWrapperByType(PsiType type) {
if(!(type instanceof PsiClassType)) return null;
PsiClass aClass = ((PsiClassType)type).resolve();
if (aClass == null) return null;
String name = aClass.getQualifiedName();
if (name == null) return null;
switch (name) {
case CommonClassNames.JAVA_LANG_ITERABLE:
case CommonClassNames.JAVA_UTIL_COLLECTION:
case CommonClassNames.JAVA_UTIL_LIST: return CommonClassNames.JAVA_UTIL_ARRAY_LIST;
case CommonClassNames.JAVA_UTIL_SET: return CommonClassNames.JAVA_UTIL_HASH_SET;
case CommonClassNames.JAVA_UTIL_MAP:
return CommonClassNames.JAVA_UTIL_HASH_MAP;
}
return null;
}
}

View File

@@ -69,6 +69,11 @@ public class DataFlowInspection extends DataFlowInspectionBase {
return new ReplaceWithTrivialLambdaFix(value);
}
@Override
protected LocalQuickFix createMutabilityViolationFix(ProblemsHolder holder, PsiElement violation) {
return WrapWithMutableCollectionFix.createFix(violation, holder.isOnTheFly());
}
@Override
protected LocalQuickFix createIntroduceVariableFix(final PsiExpression expression) {
return new IntroduceVariableFix(true);

View File

@@ -0,0 +1,9 @@
// "Wrap 'map' with 'HashMap'" "true"
import java.util.*;
class Test {
void testComparator() {
Map<String, Integer> map = new HashMap<>(Math.random() > 0.5 ? Collections.emptyMap() : Collections.singletonMap("foo", 1));
map.put(123, 456);
}
}

View File

@@ -0,0 +1,9 @@
// "Wrap 'integers' with 'HashSet'" "true"
import java.util.*;
class Test {
void testComparator() {
var integers = new HashSet<>(Set.of(4, 3, 2, 1));
integers.add(123);
}
}

View File

@@ -0,0 +1,9 @@
// "Wrap 'integers' with 'ArrayList'" "true"
import java.util.*;
class Test {
void testComparator() {
List<Integer> integers = new ArrayList<>(List.of(4, 3, 2, 1));
integers.sort(Comparator.naturalOrder());
}
}

View File

@@ -0,0 +1,9 @@
// "Wrap 'map' with 'HashMap'" "true"
import java.util.*;
class Test {
void testComparator() {
Map<String, Integer> map = Math.random() > 0.5 ? Collections.emptyMap() : Collections.singletonMap("foo", 1);
map.p<caret>ut(123, 456);
}
}

View File

@@ -0,0 +1,9 @@
// "Wrap 'integers' with 'HashSet'" "true"
import java.util.*;
class Test {
void testComparator() {
var integers = Set.of(4, 3, 2, 1);
integers.a<caret>dd(123);
}
}

View File

@@ -0,0 +1,9 @@
// "Wrap 'integers' with 'ArrayList'" "true"
import java.util.*;
class Test {
void testComparator() {
List<Integer> integers = List.of(4, 3, 2, 1);
integers.so<caret>rt(Comparator.naturalOrder());
}
}

View File

@@ -0,0 +1,43 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.java.codeInsight.daemon.quickFix;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.dataFlow.DataFlowInspection;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.testFramework.IdeaTestUtil;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.PsiTestUtil;
import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor;
import org.jetbrains.annotations.NotNull;
public class WrapWithMutableCollectionFixTest extends LightQuickFixParameterizedTestCase {
private static final DefaultLightProjectDescriptor PROJECT_DESCRIPTOR = new DefaultLightProjectDescriptor() {
@Override
public Sdk getSdk() {
return PsiTestUtil.addJdkAnnotations(IdeaTestUtil.getMockJdk9());
}
};
@NotNull
@Override
protected LocalInspectionTool[] configureLocalInspectionTools() {
return new LocalInspectionTool[]{new DataFlowInspection()};
}
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return PROJECT_DESCRIPTOR;
}
public void test() {
doAllTests();
}
@Override
protected String getBasePath() {
return "/codeInsight/daemonCodeAnalyzer/quickFix/wrapWithMutable";
}
}