inspection to detect code replacable with putIfAbsent is rewritten

This commit is contained in:
Dmitry Batkovich
2016-02-05 12:59:00 +03:00
parent 1668d7132f
commit 84320f8d70
14 changed files with 514 additions and 3 deletions
@@ -0,0 +1,238 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInspection.java18api;
import com.intellij.codeInsight.daemon.QuickFixBundle;
import com.intellij.codeInspection.BaseJavaBatchLocalInspectionTool;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiUtil;
import com.siyeh.ig.psiutils.EquivalenceChecker;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* @author Dmitry Batkovich
*/
public class Java8CollectionsApiInspection extends BaseJavaBatchLocalInspectionTool {
private final static Logger LOG = Logger.getInstance(Java8CollectionsApiInspection.class);
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
if (!PsiUtil.isLanguageLevel8OrHigher(holder.getFile())) {
return PsiElementVisitor.EMPTY_VISITOR;
}
return new JavaElementVisitor() {
@Override
public void visitConditionalExpression(PsiConditionalExpression expression) {
final ConditionInfo conditionInfo = extractConditionInfo(expression.getCondition());
if (conditionInfo == null) return;
final PsiExpression thenExpression = expression.getThenExpression();
final PsiExpression elseExpression = expression.getElseExpression();
if (thenExpression == null || elseExpression == null) return;
analyzeCorrespondenceOfPutAndGet(conditionInfo.isInverted() ? thenExpression : elseExpression,
conditionInfo.isInverted() ? elseExpression : thenExpression,
conditionInfo.getQualifier(), conditionInfo.getContainsKey(),
holder, expression);
}
@Override
public void visitIfStatement(PsiIfStatement statement) {
final PsiExpression condition = statement.getCondition();
final ConditionInfo conditionInfo = extractConditionInfo(condition);
if (conditionInfo == null) return;
PsiStatement maybeGetBranch = conditionInfo.isInverted() ? statement.getElseBranch() : statement.getThenBranch();
if (maybeGetBranch instanceof PsiBlockStatement) {
final PsiStatement[] getBranchStatements = ((PsiBlockStatement)maybeGetBranch).getCodeBlock().getStatements();
if (getBranchStatements.length > 1) {
return;
}
maybeGetBranch = getBranchStatements.length == 0 ? null : getBranchStatements[0];
}
final PsiStatement branch = conditionInfo.isInverted() ? statement.getThenBranch() : statement.getElseBranch();
final PsiStatement maybePutStatement;
if (branch instanceof PsiBlockStatement) {
final PsiStatement[] statements = ((PsiBlockStatement)branch).getCodeBlock().getStatements();
if (statements.length == 0) return;
if (statements.length != 1) {
return;
}
maybePutStatement = statements[statements.length - 1];
}
else {
maybePutStatement = branch;
}
LOG.assertTrue(maybePutStatement != null);
analyzeCorrespondenceOfPutAndGet(maybePutStatement, maybeGetBranch, conditionInfo.getQualifier(), conditionInfo.getContainsKey(),
holder, statement);
}
};
}
@Nullable
private static ConditionInfo extractConditionInfo(PsiExpression condition) {
boolean inverted = false;
final PsiMethodCallExpression conditionMethodCall;
if (condition instanceof PsiPrefixExpression) {
final PsiPrefixExpression prefixExpression = (PsiPrefixExpression)condition;
if (JavaTokenType.EXCL.equals(prefixExpression.getOperationSign().getTokenType()) &&
prefixExpression.getOperand() instanceof PsiMethodCallExpression) {
conditionMethodCall = (PsiMethodCallExpression)prefixExpression.getOperand();
inverted = true;
}
else {
return null;
}
}
else if (condition instanceof PsiMethodCallExpression) {
conditionMethodCall = (PsiMethodCallExpression)condition;
}
else {
return null;
}
if (!isJavaUtilMapMethodWithName(conditionMethodCall, "containsKey")) {
return null;
}
final PsiExpression containsQualifier = conditionMethodCall.getMethodExpression().getQualifierExpression();
if (containsQualifier == null) {
return null;
}
final PsiExpression[] expressions = conditionMethodCall.getArgumentList().getExpressions();
if (expressions.length != 1) {
return null;
}
PsiExpression containsKey = expressions[0];
return new ConditionInfo(containsQualifier, containsKey, inverted);
}
private static void analyzeCorrespondenceOfPutAndGet(@NotNull PsiElement adjustedElseBranch,
@Nullable PsiElement adjustedThenBranch,
@NotNull PsiExpression containsQualifier,
@NotNull PsiExpression containsKey,
@NotNull ProblemsHolder holder,
@NotNull PsiElement context) {
final PsiElement maybePutMethodCall;
final PsiElement maybeGetMethodCall;
if (adjustedThenBranch == null) {
maybeGetMethodCall = null;
if (adjustedElseBranch instanceof PsiExpressionStatement) {
final PsiExpression expression = ((PsiExpressionStatement)adjustedElseBranch).getExpression();
if (expression instanceof PsiMethodCallExpression && isJavaUtilMapMethodWithName((PsiMethodCallExpression)expression, "put")) {
maybePutMethodCall = expression;
}
else {
maybePutMethodCall = null;
}
}
else {
maybePutMethodCall = null;
}
}
else {
if (adjustedElseBranch instanceof PsiStatement && adjustedThenBranch instanceof PsiStatement) {
final EquivalenceChecker.Decision decision = EquivalenceChecker.statementsAreEquivalentDecision((PsiStatement)adjustedElseBranch,
(PsiStatement)adjustedThenBranch);
maybePutMethodCall = decision.getLeftDiff();
maybeGetMethodCall = decision.getRightDiff();
}
else {
maybePutMethodCall = adjustedElseBranch;
maybeGetMethodCall = adjustedThenBranch;
}
}
if (maybePutMethodCall instanceof PsiMethodCallExpression &&
(maybeGetMethodCall == null || maybeGetMethodCall instanceof PsiMethodCallExpression)) {
final PsiMethodCallExpression putMethodCall = (PsiMethodCallExpression)maybePutMethodCall;
final PsiMethodCallExpression getMethodCall = (PsiMethodCallExpression)maybeGetMethodCall;
final PsiExpression putQualifier = putMethodCall.getMethodExpression().getQualifierExpression();
final PsiExpression getQualifier = getMethodCall == null ? null : getMethodCall.getMethodExpression().getQualifierExpression();
if ((getMethodCall == null || EquivalenceChecker.expressionsAreEquivalent(putQualifier, getQualifier)) &&
EquivalenceChecker.expressionsAreEquivalent(putQualifier, containsQualifier) &&
isJavaUtilMapMethodWithName(putMethodCall, "put") &&
(getMethodCall == null || isJavaUtilMapMethodWithName(getMethodCall, "get"))) {
PsiExpression getArgument;
if (getMethodCall != null) {
final PsiExpression[] arguments = getMethodCall.getArgumentList().getExpressions();
if (arguments.length != 1) {
return;
}
getArgument = arguments[0];
}
else {
getArgument = null;
}
final PsiExpression[] putArguments = putMethodCall.getArgumentList().getExpressions();
if (putArguments.length != 2) {
return;
}
PsiExpression putKeyArgument = putArguments[0];
if (EquivalenceChecker.expressionsAreEquivalent(containsKey, putKeyArgument) &&
(getArgument == null || EquivalenceChecker.expressionsAreEquivalent(getArgument, putKeyArgument))) {
holder.registerProblem(context, QuickFixBundle.message("java.8.collections.api.inspection.description"),
new ReplaceWithMapPutIfAbsentFix(putMethodCall));
}
}
}
}
private static boolean isJavaUtilMapMethodWithName(@NotNull PsiMethodCallExpression methodCallExpression, @NotNull String expectedName) {
if (!expectedName.equals(methodCallExpression.getMethodExpression().getReferenceName())) {
return false;
}
final PsiMethod method = methodCallExpression.resolveMethod();
if (method == null) return false;
PsiMethod[] superMethods = method.findDeepestSuperMethods();
if (superMethods.length == 0) {
superMethods = new PsiMethod[]{method};
}
for (PsiMethod psiMethod : superMethods) {
final PsiClass aClass = psiMethod.getContainingClass();
if (aClass != null && CommonClassNames.JAVA_UTIL_MAP.equals(aClass.getQualifiedName())) {
return true;
}
}
return false;
}
private static class ConditionInfo {
private final PsiExpression myQualifier;
private final PsiExpression myContainsKey;
private final boolean myInverted;
private ConditionInfo(PsiExpression qualifier, PsiExpression containsKey, boolean inverted) {
myQualifier = qualifier;
myContainsKey = containsKey;
myInverted = inverted;
}
public PsiExpression getQualifier() {
return myQualifier;
}
public PsiExpression getContainsKey() {
return myContainsKey;
}
public boolean isInverted() {
return myInverted;
}
}
}
@@ -0,0 +1,145 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInspection.java18api;
import com.intellij.codeInsight.daemon.QuickFixBundle;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
/**
* @author Dmitry Batkovich
*/
public class ReplaceWithMapPutIfAbsentFix implements LocalQuickFix {
private final SmartPsiElementPointer<PsiMethodCallExpression> myPutExpressionPointer;
public ReplaceWithMapPutIfAbsentFix(PsiMethodCallExpression putExpression) {
final SmartPointerManager smartPointerManager = SmartPointerManager.getInstance(putExpression.getProject());
myPutExpressionPointer = smartPointerManager.createSmartPsiElementPointer(putExpression);
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
final PsiElement conditionalOperator = descriptor.getPsiElement();
if (conditionalOperator == null) return;
final ConditionalOperatorHelper operatorHelper = getHelper(conditionalOperator);
final PsiMethodCallExpression putExpression = myPutExpressionPointer.getElement();
if (putExpression == null) return;
PsiElement putContainingBranch = null;
for (PsiElement branch : operatorHelper.getBranches(conditionalOperator)) {
if (branch != null && PsiTreeUtil.isAncestor(branch, putExpression, false)) {
putContainingBranch = branch;
break;
}
}
if (putContainingBranch == null) return;
final PsiExpression[] arguments = putExpression.getArgumentList().getExpressions();
final PsiElement qualifier = putExpression.getMethodExpression().getQualifier();
if (qualifier == null) {
return;
}
final PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(project);
final PsiElement putContainingElement = operatorHelper.getPutContainingElement(putExpression);
final Couple<String> boundText = getBoundText(putContainingElement, putExpression);
final PsiStatement newStatement = elementFactory.createStatementFromText(boundText.getFirst() + qualifier.getText() + ".putIfAbsent"
+ "(" + arguments[0].getText() + "," +
putExpression.getArgumentList().getExpressions()[1].getText() +
")" + boundText.getSecond(), conditionalOperator);
conditionalOperator.replace(newStatement);
}
private static Couple<String> getBoundText(@NotNull PsiElement parent, @NotNull PsiElement child) {
final TextRange childRange = child.getTextRange();
final int parentStartOffset = parent.getTextRange().getStartOffset();
final String parentText = parent.getText();
return Couple.of(parentText.substring(0, childRange.getStartOffset() - parentStartOffset),
parentText.substring(childRange.getEndOffset() - parentStartOffset));
}
@Nls
@NotNull
@Override
public String getFamilyName() {
return QuickFixBundle.message("java.8.collections.api.inspection.fix.family.name");
}
@Nls
@NotNull
@Override
public String getName() {
return QuickFixBundle.message("java.8.collections.api.inspection.fix.text", "putIfAbsent");
}
private static ConditionalOperatorHelper getHelper(PsiElement element) {
return element instanceof PsiConditionalExpression ? new ConditionalExpressionHelper() : new IfStatementHelper();
}
interface ConditionalOperatorHelper {
@NotNull
PsiElement[] getBranches(PsiElement element);
@NotNull
PsiElement getPutContainingElement(PsiElement putElement);
}
private static class ConditionalExpressionHelper implements ConditionalOperatorHelper {
@NotNull
@Override
public PsiElement[] getBranches(PsiElement element) {
final PsiConditionalExpression conditionalExpression = (PsiConditionalExpression)element;
return new PsiElement[]{conditionalExpression.getThenExpression(), conditionalExpression.getElseExpression()};
}
@NotNull
@Override
public PsiElement getPutContainingElement(PsiElement putElement) {
for (PsiElement element : getBranches(PsiTreeUtil.getParentOfType(putElement, PsiConditionalExpression.class))) {
if (PsiTreeUtil.isAncestor(element, putElement, false)) {
return element;
}
}
throw new AssertionError();
}
}
private static class IfStatementHelper implements ConditionalOperatorHelper {
@NotNull
@Override
public PsiElement[] getBranches(PsiElement element) {
final PsiIfStatement ifStatement = (PsiIfStatement)element;
return new PsiElement[]{ifStatement.getThenBranch(), ifStatement.getElseBranch()};
}
@NotNull
@Override
public PsiElement getPutContainingElement(PsiElement putElement) {
return PsiTreeUtil.getParentOfType(putElement, PsiStatement.class);
}
}
}
@@ -0,0 +1,7 @@
// "Replace with 'putIfAbsent' method call" "true"
import java.util.Map;
class Test {
void m1111(Map<String, Integer> map) {
int i = map.putIfAbsent("asd", 123);
}
}
@@ -0,0 +1,7 @@
// "Replace with 'putIfAbsent' method call" "true"
import java.util.Map;
class Test {
void m1111(Map<String, Integer> map) {
int i = map.putIfAbsent("asd", 123);
}
}
@@ -0,0 +1,10 @@
// "Replace with 'putIfAbsent' method call" "true"
import java.util.Map;
class Test{
Integer m2(Map<String, Integer> map) {
return map.putIfAbsent("asd", 123);
}
}
@@ -0,0 +1,7 @@
// "Replace with 'putIfAbsent' method call" "true"
import java.util.Map;
class Test {
void m1111(Map<String, Integer> map) {
int i = !map.containsKey("asd") ? map.<caret>put("asd", 123) : map.get("asd");
}
}
@@ -0,0 +1,7 @@
// "Replace with 'putIfAbsent' method call" "true"
import java.util.Map;
class Test {
void m1111(Map<String, Integer> map) {
int i = map.cont<caret>ainsKey("asd") ? map.get("asd") : map.put("asd", 123);
}
}
@@ -0,0 +1,10 @@
// "Replace with 'putIfAbsent' method call" "false"
import java.util.Map;
class Test {
void m1(Map<String, Integer> map) {
if (!map.contai<caret>nsKey("ads")) {
int i = 10;
map.put("ads", i);
}
}
}
@@ -0,0 +1,14 @@
// "Replace with 'putIfAbsent' method call" "true"
import java.util.Map;
class Test{
Integer m2(Map<String, Integer> map) {
if (!map.conta<caret>insKey("asd")) {
return map.put("asd", 123);
} else {
return map.get("asd");
}
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInspection;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.codeInspection.java18api.Java8CollectionsApiInspection;
import org.jetbrains.annotations.NotNull;
/**
* @author Dmitry Batkovich
*/
public class Java8CollectionsApiInspectionTest extends LightQuickFixParameterizedTestCase {
@NotNull
@Override
protected LocalInspectionTool[] configureLocalInspectionTools() {
return new LocalInspectionTool[]{
new Java8CollectionsApiInspection(),
};
}
public void test() throws Exception {
doAllTests();
}
@Override
protected String getBasePath() {
return "/inspection/java8CollectionsApi";
}
}
@@ -30,7 +30,7 @@ public class EquivalenceChecker {
private EquivalenceChecker() {}
private static final Decision EXACTLY_MATCHES = new Decision(true);
private static final Decision EXACTLY_UN_MATCHES = new Decision(false);
public static final Decision EXACTLY_UN_MATCHES = new Decision(false);
public static class Decision {
private final PsiElement myLeftDiff;
@@ -540,7 +540,11 @@ public class EquivalenceChecker {
@NotNull PsiReturnStatement statement2) {
final PsiExpression returnValue1 = statement1.getReturnValue();
final PsiExpression returnValue2 = statement2.getReturnValue();
return expressionsAreEquivalentDecision(returnValue1, returnValue2);
final Decision decision = expressionsAreEquivalentDecision(returnValue1, returnValue2);
if (decision.isExactUnMatches()) {
return new Decision(returnValue1, returnValue2);
}
return decision;
}
private static Decision throwstatementsAreEquivalentDecision(
@@ -0,0 +1,11 @@
<html>
<body>
Inspection detects usages of java's <b>Map</b> when they can be replaced with methods <b>putIfAbsent</b>. For example:
<code><pre>
if (!map.containsKey(aKey)) {
map.put(aKey, aValue);
}
</pre>
</code>
</body>
</html>
@@ -279,4 +279,7 @@ wrap.long.with.math.to.int.parameter.single.text=Wrap parameter using 'Math.toIn
wrap.long.with.math.to.int.parameter.multiple.text=Wrap {0, choice, 1#1st|2#2nd|3#3rd|4#{0,number}th} parameter using ''Math.toIntExact()''
add.exception.from.field.initializer.to.constructor.throws.text=Add exception to class {0, choice, 0#default constructor|1#constructor|2#constructors} signature
add.exception.from.field.initializer.to.constructor.throws.family.text=Add exception to class constructors signature
add.exception.from.field.initializer.to.constructor.throws.family.text=Add exception to class constructors signature
java.8.collections.api.inspection.description=If statement could be replaced with single method
java.8.collections.api.inspection.fix.family.name=Replace with single method call
java.8.collections.api.inspection.fix.text=Replace with ''{0}'' method call
+5
View File
@@ -770,6 +770,11 @@
groupKey="group.names.performance.issues" enabledByDefault="false" level="WARNING"
implementationClass="com.intellij.codeInspection.CollectionAddAllCanBeReplacedWithConstructorInspection"
displayName="Collection.addAll() can be replaced with parametrized constructor"/>
<localInspection groupPath="Java" language="JAVA" shortName="Java8CollectionsApi"
groupBundle="messages.InspectionsBundle"
groupKey="group.names.language.level.specific.issues.and.migration.aids" enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.codeInspection.java18api.Java8CollectionsApiInspection"
displayName="Collection usages can be replaced with single method"/>
<intentionAction>
<className>com.intellij.codeInsight.daemon.quickFix.RedundantLambdaParameterTypeIntention</className>